Instantiating UIActivity subclass - ios

When sharing an image from, eg Photos, I want my iOS app extension to appear in the list of apps available (next to Mail, Messages...) in order to receive the shared picture. Reading many sites reveals I have to subclass UIActivity and add it to an App Extension, what I did.
However I don't understand how I should instantiate my subclass, and it never gets called (No NSLog is displayed, item not in the sharing list).
How should I "instruct" iOS to use this subclass ?
myActivity.m:
#import "myActivity.h"
#implementation myActivity
- (NSString *)activityType {
// a unique identifier
return #"com.myapp.uniqueIdentifier";
}
- (NSString *)activityTitle {
// a title shown in the sharing menu
return #"Custom Activity";
}
- (UIImage *)activityImage {
// an image to go with our option
return [UIImage imageNamed:#"MyImage"];
}
+ (UIActivityCategory)activityCategory {
// which row our activity is shown in
// top row is sharing, bottom row is action
NSLog(#"UIActivityCategoryShare");
return UIActivityCategoryShare;
}
- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems {
// return YES for anything that our activity can deal with
for (id item in activityItems) {
// we can deal with strings and images
if ([item isKindOfClass:[UIImage class]]) {
return YES;
}
}
// for everything else, return NO
return NO;
}
- (void)prepareWithActivityItems:(NSArray *)activityItems {
// anything we need to prepare, now's the chance
// custom UI, long running calculations, etc
// also: grab a reference to the objects our user wants to share/action
self.activityItems = activityItems;
}
- (UIViewController *)activityViewController {
// return a custom UI if we need it,
// or the standard activity view controller if we don't
return nil;
}
- (void)performActivity {
// the main thing our activity does
// act upon each item here
for (id item in self.activityItems) {
NSLog(#"YEY - someone wants to use our activity!");
NSLog(#"They used this object: %#", [item description]);
}
// notify iOS that we're done here
// return YES if we were successful, or NO if we were not
[self activityDidFinish:YES];
}
#end
And myActivity.h:
#import <UIKit/UIKit.h>
#interface myActivity : UIActivity
#property (nonatomic, strong) NSArray *activityItems;
#end

Related

Creating an instance for other class

I have no idea how I should correctly name the title but I know exactly what my problem is (I will eventually edit the title later).
I am pretty new to Objective-C and I am still learning.
So, I have a class that contains a tableView (I will call it ClassA) and another with a normal UIView (ClassB). What I want to do, is to let a button appear when a row is selected.
I created in my ClassB.h file:
+(id)sharedInstance;
#property (retain, nonatomic) IBOutlet UIButton *btn;
-(void) showBtn :(BOOL) show;
And in my ClassB.m file:
#synthesize btn;
static ClassB *this = nil;
(+id) sharedInstance {
if(!this) {
#synchronized (self) {
this = [[ClassB alloc] init];
}
}
return this;
}
-(void)viewDidLoad {
[self showBtn:NO] //because I only want to let it appear when a row is selected.
[self.view addSubview:btn];
}
-(void) showBtn :(BOOL) show { // I called this method in classA.
if (show == NO) {
btn.hidden = YES;
} else {
btn.hidden = NO;
}
}
So when I launch my app, the button is hidden and stays hidden when I select a row. I debugged, and found that btn is nil when I called the method in ClassA. After some research, I found that the method is called for another instance, so here my question, what can I do, to get it called for the right instance?
EDIT
Here part of my ClassA.m
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger row = [indexPath row];
[[ClassB sharedInstance] showBtn:YES];
}
Observation: The ClassB is a UIViewController which is wrong. UIViewControllers have viewDidLoad.
Implementation Suggestion:
The correct implementation for the requirement would be that you create a custom cell with a button. Hide the button in awakeFromNib method. in didSelectRowAtIndex set the cell.button.isHidden = YES.
This should alone take care of the requirement mentioned above.

How to prevent a UIWindow to be a key window?

When I show an alert with UIAlertController, the alert itself presented in a new window. (for now at least) And when the alert window dismisses, system seems to set a random window key-window.
I am presenting a new "banner" window to render some banners over status-bar (AppStore compatibility is out of topic here), and usually, this "banner" window becomes next key window, and causes many problems on user input and first responder management.
So, I want to prevent this "banner" window to become a key window, but I cannot figure out how. For now, as a workaround, I am just re-setting my main window to be a key window again as soon as that "banner" window becomes key window. But it doesn't feel really good.
How can I prevent a window to become a key window?
As a workaround, we can set main window key again as soon as the "banner" window becomes a key like this.
class BannerWindow: UIWindow {
weak var mainWindow: UIWindow?
override func becomeKeyWindow() {
super.becomeKeyWindow()
mainWindow?.makeKeyWindow()
}
}
Faced with this too. It seems that it's enough to just make:
class BannerWindow: UIWindow {
override func makeKey() {
// Do nothing
}
}
This way you don't need to keep a reference to a previous keyWindow, which is especially cool if it might get changed.
For Objective-C it's:
#implementation BannerWindow
- (void)makeKeyWindow {
// Do nothing
}
#end
I've been trying to solve this problem for years. I finally reported a Radar for it: http://www.openradar.me/30064691
My workaround looks something like this:
// a UIWindow subclass that I use for my overlay windows
#implementation GFStatusLevelWindow
...
#pragma mark - Never become key
// http://www.openradar.me/30064691
// these don't actually help
- (BOOL)canBecomeFirstResponder
{
return NO;
}
- (BOOL)becomeFirstResponder
{
return NO;
}
- (void)becomeKeyWindow
{
LookbackStatusWindowBecameKey(self, #"become key window");
[[self class] findAndSetSuitableKeyWindow];
}
- (void)makeKeyWindow
{
LookbackStatusWindowBecameKey(self, #"make key window");
}
- (void)makeKeyAndVisible
{
LookbackStatusWindowBecameKey(self, #"make key and visible window");
}
#pragma mark - Private API overrides for status bar appearance
// http://www.openradar.me/15573442
- (BOOL)_canAffectStatusBarAppearance
{
return NO;
}
#pragma mark - Finding better key windows
static BOOL IsAllowedKeyWindow(UIWindow *window)
{
NSString *className = [[window class] description];
if([className isEqual:#"_GFRecordingIndicatorWindow"])
return NO;
if([className isEqual:#"UIRemoteKeyboardWindow"])
return NO;
if([window isKindOfClass:[GFStatusLevelWindow class]])
return NO;
return YES;
}
void LookbackStatusWindowBecameKey(GFStatusLevelWindow *self, NSString *where)
{
GFLog(GFError, #"This window should never be key window!! %# when in %#", self, where);
GFLog(GFError, #"To developer of %#: This is likely a bug in UIKit. If you can get a stack trace at this point (by setting a breakpoint at LookbackStatusWindowBecameKey) and sending that stack trace to nevyn#lookback.io or support#lookback.io, I will report it to Apple, and there will be rainbows, unicorns and a happier world for all :) thanks!", [[NSBundle mainBundle] gf_displayName]);
}
+ (UIWindow*)suitableWindowToMakeKeyExcluding:(UIWindow*)notThis
{
NSArray *windows = [UIApplication sharedApplication].windows;
NSInteger index = windows.count-1;
UIWindow *nextWindow = [windows objectAtIndex:index];
while((!IsAllowedKeyWindow(nextWindow) || nextWindow == notThis) && index >= 0) {
nextWindow = windows[--index];
}
return nextWindow;
}
+ (UIWindow*)findAndSetSuitableKeyWindow
{
UIWindow *nextWindow = [[self class] suitableWindowToMakeKeyExcluding:nil];
GFLog(GFError, #"Choosing this as key window instead: %#", nextWindow);
[nextWindow makeKeyWindow];
return nextWindow;
}

GMSSyncTileLayer tileForX:y:zoom: called only once

Summary:
I subclassed
GMSSyncTileLayer
and overwrote
tileForX:y:zoom:
but its only called once no matter how much I pan.
why?
DETAIL
We have implemented our own TileServer which is behind a secure webservice so we need to pass a login token to get the tiles.
The call is asynchronous POST which is quiet common for a webservice call.
Because I had to pass login token in the NSURLSession header I couldnt just pass GET urls to
GMSTileURLConstructor urls = http://<tileserver>/gettile?x=3&y=4&zoom=5
So I subclassed GMSSyncTileLayer
#interface SNSyncTileLayer : GMSSyncTileLayer
overwrote
- (UIImage *)tileForX:(NSUInteger)x y:(NSUInteger)y zoom:(NSUInteger)zoom {
When tileForX:y:Zoom: is called the first time I call a webservice to get the tile UIImage.
The UIImage returns on a delegate and is stored in a NSDictionary with key in format TILE_x_y_Zoom.
The call to the WS is asynch so - (UIImage *)tileForX:y:Zoom: always returns nil for that tile the first time its called.
What i've noticed is that tileForX:y:Zoom: is never called again no matter how much I pan back and forth.
For instance at the current zoom I pan across europe.
I see tileForX:y:Zoom: being called once and ws calls being mad and images being stored in my dictionary.
But if i keep panning at the same zoom I come back to Europe and tileForX:y:Zoom: is not called again.
Fix one - clear cache when new tile downloaded
I tried creating a delegate on SNSyncTileLayer and everytime a new tile downloaded it called:
[self.snSyncTileLayer clearTileCache];
But this wipes out ALL tiles and reloads them so as you pan you get a terrible flashing.
My only idea next is to measure how much the map has panned and if its more than half a width or height then to call clearTileCache.
SO the big question why isnt tileForX:y:Zoom: called everytime?
My overridden class
//
// SNSyncTileLayer.m
//
#import "SNSyncTileLayer.h"
#import "SNAppDelegate.h"
#import "GoogleTileRequest.h"
#import "SNGoogleTileRequest.h"
#import "GoogleTileImageResult.h"
#interface SNSyncTileLayer()<SNSeaNetWebServicesManagerDelegate>{
BOOL _debugOn;
}
#property (nonatomic, retain) NSMutableDictionary * tileImageCacheDict;
#end
#implementation SNSyncTileLayer
- (instancetype)init
{
self = [super init];
if (self) {
_tileImageCacheDict = [NSMutableDictionary dictionary];
_debugOn = TRUE;
}
return self;
}
- (UIImage *)tileForX:(NSUInteger)x y:(NSUInteger)y zoom:(NSUInteger)zoom {
if(_debugOn)ImportantLog(#"tileForX:(%lu) y:(%lu) zoom:(%lu)", (unsigned long)x,(unsigned long)y,(unsigned long)zoom);
UIImage *tileImage_ = nil;
//tileImage_ = [UIImage imageNamed:#"EmptyTile1.png"];
NSString * keyForTile_ = [NSString stringWithFormat:#"TILE_%lu_%lu_%lu", (unsigned long)x,(unsigned long)y,(unsigned long)zoom];
id dictObj_ = [self.tileImageCacheDict objectForKey:keyForTile_];
if (dictObj_) {
if([dictObj_ isMemberOfClass:[NSNull class]])
{
if(_debugOn)DebugLog(#"tile has been called before but image not downloaded yet:[%#]",keyForTile_);
}
else if([dictObj_ isMemberOfClass:[UIImage class]])
{
if(_debugOn)DebugLog(#"cached image found in dict_ return it:[%#]",keyForTile_);
tileImage_ = (UIImage *)dictObj_;
}
else{
ErrorLog(#"ITEM IN self.tileImageCacheDict not NSNull or UIImage:[%#]", dictObj_);
}
}else{
if(_debugOn)ImportantLog(#"tileForX: CACHED IMAGE NOT FOUND: DOWNLOAD IT[%#]",keyForTile_);
//-----------------------------------------------------------------------------------
//add in temp object - tyring to check if tileForX:Y:Zoom is called more than once
[self.tileImageCacheDict setObject:[NSNull null] forKey:keyForTile_];
//-----------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------
SNAppDelegate *appDelegate = (SNAppDelegate *)[[UIApplication sharedApplication] delegate];
GoogleTileRequest * googleTileRequest_ = [[GoogleTileRequest alloc]init];
googleTileRequest_.X = [NSNumber numberWithInteger:x];
googleTileRequest_.Y = [NSNumber numberWithInteger:y];
googleTileRequest_.Zoom = [NSNumber numberWithInteger:zoom];
#pragma mark TODO - NOW - thur11dec - load from settings
googleTileRequest_.MapType = #"Dark";
//for general errors
appDelegate.snSeaNetWebServicesManager.delegate = self;
//Request should know what class to return too
googleTileRequest_.delegateForRequest = self;
[appDelegate.snSeaNetWebServicesManager ITileController_GoogleTile:googleTileRequest_];
//-----------------------------------------------------------------------------------
return kGMSTileLayerNoTile;
//-----------------------------------------------------------------------------------
}
return tileImage_;
}
#pragma mark -
#pragma mark SNSeaNetWebServicesManagerDelegate
#pragma mark -
-(void) snSeaNetWebServicesManager:(SNSeaNetWebServicesManager *)SNSeaNetWebServicesManager
wsReponseReceivedForRequest:(SNWebServiceRequest *)snWebServiceRequest_
error:(NSError *)error
{
#pragma mark TODO - NOW - thur11dec2014
if(error){
ErrorLog(#"error:%#",error);
}else{
if(snWebServiceRequest_){
if([snWebServiceRequest_ isMemberOfClass:[SNGoogleTileRequest class]])
{
//Result is JSONModel ivar in Request
if(snWebServiceRequest_.resultObject){
GoogleTileImageResult * googleTileImageResult_= (GoogleTileImageResult *)snWebServiceRequest_.resultObject;
UIImage * responseImage_ = googleTileImageResult_.responseImage;
if(responseImage_){
//-----------------------------------------------------------------------------------
//build the key from the parameters
if(snWebServiceRequest_.bodyJsonModel){
NSDictionary *paramsDict = [snWebServiceRequest_.bodyJsonModel toDictionary];
if(paramsDict){
NSString *keyX_ = [paramsDict objectForKey:#"X"];
NSString *keyY_ = [paramsDict objectForKey:#"Y"];
NSString *keyZoom_ = [paramsDict objectForKey:#"Zoom"];
if(keyX_){
if(keyY_){
if(keyZoom_){
NSString * keyForTile_ = [NSString stringWithFormat:#"TILE_%#_%#_%#", keyX_,keyY_,keyZoom_];
if(_debugOn)ImportantLog(#"TILE DOWNLOADED ADD TO CACHE[%#]",keyForTile_);
[self.tileImageCacheDict setObject:responseImage_ forKey:keyForTile_];
//if(_debugOn)DebugLog(#"[[self.tileImageCacheDict allKeys]count]:%lu", (unsigned long)[[self.tileImageCacheDict allKeys]count]);
//-----------------------------------------------------------------------------------
//I ADDED THIS SO delegate could clearTileCache but causes flashing as ALL tiles get reloaded visible ones and ones downloaded but not on map
if(self.delegate){
if([self.delegate respondsToSelector:#selector(snSyncTileLayer:tileDownloadedForX:Y:Zoom:)]){
[self.delegate snSyncTileLayer:self
tileDownloadedForX:keyX_
Y:keyY_
Zoom:keyZoom_
];
}else {
ErrorLog(#"<%# %#:(%d)> %s delegate[%#] doesnt implement snSyncTileLayer:tileDownloadedForX:Y:Zoom:", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__ ,self.delegate);
}
}else{
ErrorLog(#"<%# %#:(%d)> %s self.delegate is nil", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__);
}
//-----------------------------------------------------------------------------------
}else{
ErrorLog(#"keyZoom_ is nil");
}
}else{
ErrorLog(#"keyY_ is nil");
}
}else{
ErrorLog(#"keyX_ is nil");
}
}else{
ErrorLog(#"paramsDict is nil");
}
}else{
ErrorLog(#"self.downloadingTasksDictionary is nil");
}
//-----------------------------------------------------------------------------------
}else{
ErrorLog(#"responseImage_ is nil");
}
}else{
ErrorLog(#"snWebServiceRequest_.resultJsonModel is nil");
}
}
else {
ErrorLog(#"UNHANDLED snWebServiceRequest_:%#", snWebServiceRequest_.class);
}
}else{
ErrorLog(#"snWebServiceRequest_ is nil");
}
}
}
#end
I haven't done this so I'm not sure, but my guess from reading the documentation is that you should be subclassing GMSTileLayer instead of GMSSyncTileLayer.
GMSSyncTileLayer is designed for cases where you are able to synchronously (ie immediately) return the tile for that location. By returning kGMSTileLayerNoTile, you are specifically indicating that 'there is no tile here', and so it never calls your class again for that location, as you've already responded that there is no tile there. (BTW, your description says you are returning nil, which indicates a transient error, but your code is actually returning kGMSTileLayerNoTile).
The GMSTileLayer class is designed for the asynchronous approach that you're using. If you subclass GMSTileLayer, your requestTileForX:y:zoom:receiver: method should start the background process to fetch the tile. When the tile request succeeds, then it is passed off to the GMSTileReceiver that was provided in that method (you should keep a copy of that receiver along with your request).

IOS UIMenuController UIMenuItem, how to determine item selected with generic selector method

With the following setup
....
MyUIMenuItem *someAction = [[MyUIMenuItem alloc]initWithTitle : #"Something" action : #selector(menuItemSelected:)];
MyUIMenuItem *someAction2 = [[MyUIMenuItem alloc]initWithTitle : #"Something2" action : #selector(menuItemSelected:)];
....
- (IBAction) menuItemSelected : (id) sender
{
UIMenuController *mmi = (UIMenuController*) sender;
}
How to figure out which menu item was selected.
And don't say that you need to have two methods... Thanks in advance.
Okay, I've solved this one. The solution isn't pretty, and the better option is "Apple fixes the problem", but this at least works.
First of all, prefix your UIMenuItem action selectors with "magic_". And don't make corresponding methods. (If you can do that, then you don't need this solution anyway).
I'm building my UIMenuItems thus:
NSArray *buttons = [NSArray arrayWithObjects:#"some", #"random", #"stuff", nil];
NSMutableArray *menuItems = [NSMutableArray array];
for (NSString *buttonText in buttons) {
NSString *sel = [NSString stringWithFormat:#"magic_%#", buttonText];
[menuItems addObject:[[UIMenuItem alloc]
initWithTitle:buttonText
action:NSSelectorFromString(sel)]];
}
[UIMenuController sharedMenuController].menuItems = menuItems;
Now your class that catches the button tap messages needs a few additions. (In my case the class is a subclass of UITextField. Yours might be something else.)
First up, the method that we've all been wanting to have but that didn't exist:
- (void)tappedMenuItem:(NSString *)buttonText {
NSLog(#"They tapped '%#'", buttonText);
}
Then the methods that make it possible:
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
NSString *sel = NSStringFromSelector(action);
NSRange match = [sel rangeOfString:#"magic_"];
if (match.location == 0) {
return YES;
}
return NO;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
if ([super methodSignatureForSelector:sel]) {
return [super methodSignatureForSelector:sel];
}
return [super methodSignatureForSelector:#selector(tappedMenuItem:)];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
NSString *sel = NSStringFromSelector([invocation selector]);
NSRange match = [sel rangeOfString:#"magic_"];
if (match.location == 0) {
[self tappedMenuItem:[sel substringFromIndex:6]];
} else {
[super forwardInvocation:invocation];
}
}
One would expect that the action associated with a given menu item would include a sender parameter that should point to the chosen menu item. Then you could simply examine the title of the item, or do as kforkarim suggests and subclass UIMenuItem to include a proeprty that you can use to identify the item. Unfortunately, according to this SO question, the sender parameter is always nil. That question is over a year old, so things may have changed -- take a look at what you get in that parameter.
Alternately, it looks like you'll need to a different action for each menu item. Of course, you could set it up so that all your actions call a common method, and if they all do something very similar that might make sense.
Turns out it's possible to obtain the UIButton object (which is actually UICalloutBarButton) that represents UIMenuItem if you subclass UIApplication and reimplement -sendAction:to:from:forEvent:. Although only -flash selector goes through UIApplication, it's enough.
#interface MyApplication : UIApplication
#end
#implementation MyApplication
- (BOOL)sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event
{
// target == sender condition is just an additional one
if (action == #selector(flash) && target == sender && [target isKindOfClass:NSClassFromString(#"UICalloutBarButton")]) {
NSLog(#"pressed menu item title: %#", [(UIButton *)target titleLabel].text);
}
return [super sendAction:action to:target from:sender forEvent:event];
}
#end
You can save target (or any data you need from it) in e.g. property and access it later from your UIMenuItem's action.
And to make your UIApplication subclass work, you must pass its name as a third parameter to UIApplicationMain():
int main(int argc, char *argv[])
{
#autoreleasepool {
return UIApplicationMain(argc, argv, NSStringFromClass([MyApplication class]), NSStringFromClass([YOUR_APP_DELEGATE class]));
}
}
This solution works on iOS 5.x-7.0 as of post date (didn't test on older versions).
ort11, you might want to create a property of myuimenuitem and set some sort of Tag. Thay way the object of sender could be recognized by its tag it. In Ibaction then you can set a switch statement that can correspond to each sender.tag and work throught that logic. I guess thats the simplest way to go.

Three20 TTTableViewController load more automatically

I've a TTTableViewController which follows TTTableViewController -> TTDataSource -> TTModel pattern. I've TTTableMoreButton and my list goes on to load more items when the user clicks on it.
How can I change the behaviour of this TTTableMoreButton? When the user came to the end of the list, I want it to behave as if it is clicked. In Facebook app, there is an implementation like this. I hope I could tell what I want.
Here is how to do it.
full disclosure: It is my code blog.
Here I've my own approach which i found out just before coneybeare's answer. I simply subclassed TTTableMoreButton and TTTableMoreButtonCell classes and in the "- (void)layoutSubviews" method, I detect that "Load More" button is appearing, and it should start loading more data if it is not already doing it.
I'm not sure which approach (coneybeaare's or mine) is the best and I'm looking forward for the comments about it.
AutoMoreTableItem.h
#interface AutoMoreTableItem : TTTableMoreButton {
}
#end
AutoMoreTableItem.m
#import "AutoMoreTableItem.h"
#implementation AutoMoreTableItem
#end
AutoMoreTableItemCell.h
#interface AutoMoreTableItemCell : TTTableMoreButtonCell {
}
#end
AutoMoreTableItemCell.m
#import "AutoMoreTableItemCell.h"
#import "AutoMoreTableItem.h"
#implementation AutoMoreTableItemCell
- (void)setObject:(id)object {
if (_item != object) {
[super setObject:object];
AutoMoreTableItem* item = object;
self.animating = item.isLoading;
self.textLabel.textColor = TTSTYLEVAR(moreLinkTextColor);
self.selectionStyle = TTSTYLEVAR(tableSelectionStyle);
self.accessoryType = UITableViewCellAccessoryNone;
}
}
- (void)layoutSubviews {
[super layoutSubviews];
AutoMoreTableItem* moreLink = self.object;
if(moreLink.isLoading ==YES) {
return;
}
if (moreLink.model) {
moreLink.isLoading = YES;
self.animating = YES;
[moreLink.model load:TTURLRequestCachePolicyDefault more:YES];
}
}
#end
And of course, in the datasource implementation:
- (Class)tableView:(UITableView*)tableView cellClassForObject:(id) object {
if([object isKindOfClass:[AutoMoreTableItem class]]){
return [AutoMoreTableItemCell class];
} else {
return [super tableView:tableView cellClassForObject:object];
}
}

Resources