At the moment we have a class structure like...
GenericFruitViewController
- AppleViewController
- PearViewController
- StrawberryViewController
Where the specific view controllers are subclasses and only change a small amount of implementation.
What I'd like to do is be able to swap out the GenericViewController at runtime. I want to change the way the generic view controller works and change some of the methods (this won't affect the subclassed overridden methods).
But I'd like to be able to switch this on/off (A/B testing).
At the moment we have a factory method that does something like...
- (GenericFruitViewController *)fruitControllerWithType:(FruitType)type
{
if (type == Apple) {
return [AppleViewController new];
}
return [GenericFruitViewController new];
}
What I'd like to do (ideally) is something like...
- (GenericFruitViewController *)fruitControllerWithType:(FruitType)type
{
// this is the new bit!
if (switchOnTheTesting) {
// swap GenericFruitViewController for my NewFruitViewController
}
// new bit ends
// existing code not changed
if (type == Apple) {
// this now returns a subclass of NewFruitVC if switched
return [AppleViewController new];
}
// this now returns NewFruitVC if switched
return [GenericFruitViewController new];
}
And by doing this it will then use my new VC whenever it refers to the GenericFruitVC.
Is that even possible?
Related
I'm trying to add keyboard shortcuts to my app and I'm having a problem with the action of the UIKeyCommand not being called.
I got a UIViewController that that is overriding the KeyCommands.
- (BOOL)becomeFirstResponder
{
return YES;
}
- (NSArray<UIKeyCommand *> *)keyCommands
{
return self.keyCommandManager.keyShortcutsArray;
}
I also have a KeyCommandManager class of NSObject which has two methods one that sets the keyShortcutsArray depending on the state of my app and the other one is the method that should be tigger by the UIKeyCommands.
- (void)setKeyShortcutsOfType:(ShortcutType)shortcutType
{
switch(shortcutType)
{
case PlaybackPreviewShortcut:
self.keyShortcutsArray = #[[UIKeyCommand keyCommandWithInput:#" " modifierFlags:0 action:#selector(keyShortcutActions:) discoverabilityTitle:#"Toggle playback preview"]];
break;
default:
self.keyShortcutsArray = #[];
break;
}
- (void)keyShortcutActions:(UIKeyCommand)sender
{
NSLog(#"#### This method is not being called by the space key shortcut");
}
Currently when a key is pressed the KeyCommand override method is getting the correct array. However the selector of those keys are not working and the keyShortcutActions method is not being called.
From Apple's docs:
The key commands you return from this method are applied to the entire responder chain. When an key combination is pressed that matches a key command object, UIKit walks the responder chain looking for an object that implements the corresponding action method. It calls that method on the first object it finds and then stops processing the event.
Your keyCommandManger instance of NSObject is not in the responder chain -- the view controller is.
If you put this method:
- (void)keyShortcutActions:(UIKeyCommand)sender
{
NSLog(#"#### This method IS being called (in view controller) by the space key shortcut");
}
you should see it being triggered.
If you want your "action" code to be contained in keyCommandManger, you could forward the event to your manager object. Or, you could try to change your manager class to inherit from UIResponder -- but reliably getting it into the chain is the tough part.
I have a #interface CustomViewController : UIViewController. I wonder if it is possible to do this:
-(CustomViewController*)parentViewController{
return /* bla bla return custom vc here*/;
}
As you can see, I want to simply override -(UIViewController*)parentViewController but to return an instance of CustomViewController.
Obviously, I can simply create another property like customParentViewContrller and everything will be fine. However, I really don't want to do this, because I will end up using two properties, which actually represent the same thing - not good.
Edit:
Let me clear this. I want to avoid type casting in future so that this code [(CustomViewController*)self.parentViewController someCustomProperty]
turns to this code
[self.parentViewController someCustomProperty].
Yeah you can..
Created a basic project and tested it out:
- (ViewController *)parentViewController {
return self;
}
Working fine.
I have an app with a Share button. I want to customize what content is shared based on the activity type. For example, Messages might get an image and text, whereas AirDrop would just get a file.
I actually have this working perfectly, and the code I'm using has worked fine in every version of iOS through iOS 10. But I've realized I'm returning nil where I'm not supposed to, so I'm trying to figure out how to fix that.
I do something like this to set up my activity view controller:
JUNActivityProvider *fileProvider = [[JUNActivityProvider alloc] initWithPlaceholderItem:[NSObject new]];
fileProvider.objectID = objectID;
fileProvider.fileURL = fileURL;
JUNActivityProvider *textProvider = [[JUNActivityProvider alloc] initWithPlaceholderItem:[NSString new]];
textProvider.objectID = objectID;
...
UIActivityViewController *activityController = [[UIActivityViewController alloc]
initWithActivityItems:#[fileProvider,imageProvider,textProvider,urlProvider,printFormatter]
applicationActivities:nil];
Then in JUNActivityProvider, I have an item method that customizes the return value based on the activityType:
- (id)item {
if (self.fileURL) {
if ([self.activityType isEqualToString:UIActivityTypeAirDrop]) {
// Create the file
return url;
}
} else if ([self.placeholderItem isKindOfClass:[UIImage class]]) {
if ([self.activityType isEqualToString:UIActivityTypeAirDrop] == NO &&
[self.activityType isEqualToString:UIActivityTypeMail] == NO &&
[self.activityType isEqualToString:UIActivityTypePrint] == NO) {
// Create the image
return image;
}
} else if ([self.placeholderItem isKindOfClass:[NSString class]]) {
if ([self.activityType isEqualToString:UIActivityTypeMail]) {
return #"example one";
} else if ([self.activityType isEqualToString:UIActivityTypeMessage] ||
[self.activityType isEqualToString:UIActivityTypeCopyToPasteboard]) {
return #"example two";
}
}
return nil;
}
That return return nil at the end is the problem. It works fine and does exactly what I want—when it's nil that item isn't shared. The written documentation doesn't say that it must return a value, but the header file does:
- (nonnull id)item; // called on secondary thread when user selects an activity. you must subclass and return a non-nil value.
I don't want to risk a crash by returning nil when a nonnull value is expected, so I need to fix this. As far as I can tell my only option is to stop using UIActivityItemProvider, and instead implement the UIActivityItemSource protocol on my own. That protocol includes the method activityViewController:itemForActivityType:, which clearly states that you can return nil there:
May be nil if multiple items were registered for a single activity type, so long as one of the items returns an actual value.
Perfect. But here's the problem: activityViewController:itemForActivityType: is called on the main thread, which is causing problems with one of my items in particular. Here's a summary of what's happening:
I need to call some methods that run asynchronously. In order to deal with that I've tried using a dispatch semaphore. That keeps the method from returning until I've had a chance to set the return value.
Since activityViewController:itemForActivityType: is called on the main thread, that locks up while it's working.
I need to draw a UIView into an image. If I try to do that work on the main thread, nothing happens until the semaphore times out. But if I don't do it on the main thread, it crashes.
I'm at a loss for how to deal with this. Basically I need to keep the method from returning until I'm ready, but I can't lock up the main thread since I need to do some work there. This seems… impossible? Is there any way to make this work?
After filing an enhancement request I was just about to give up and settle on either returning nil or [NSNull null]. But then I realized there is absolutely a solution to this problem.
While UIActivityItemProvider includes a bunch of its own functionality, it still very much implements the UIActivityItemSource protocol. I knew that. What I hadn't considered is that this means I can just override activityViewController:itemForActivityType: and return nil there when it's appropriate.
So the last line of my item method now looks like this:
return self.placeholderItem;
You could also return [NSNull null] here, or really any object. I chose the placeholderItem because it seems a little safer—at the very least I know it's returning an object of the expected type, in case anything about the implementation ever changes.
Then all I have to do is add my own implementation of activityViewController:itemForActivityType: (where we are allowed to return nil):
- (nullable id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(UIActivityType)activityType {
id item = [super activityViewController:activityViewController itemForActivityType:activityType];
if ([item isEqual:self.placeholderItem]) return nil;
return item;
}
Just call super to get the item, return nil if it's something you don't want to include, or return the item if it is. Note that if your placeholderItem might ever be equal to something you actually do want to share, you will need to change this implementation a bit—but the same basic concept should work.
I am trying to override the UIColor isEqual: method. I am doing so within a category method, however it does not seem to get called, either from NSArray's containsObject:, or even when called directly, as shown below.
It has been exposed as a method in the category's header file and I have also checked that the category has been imported to the implementation file I am working on.
Where it is being called directly:
UIColor *col = [UIColor eightBitColorWithRed:pxl.red green:pxl.green blue:pxl.blue];
int index = -1;
for (int i = 0; i < self.colorArrayM.count; i++) {
if ([col isEqual:((UIColor*)self.colorArrayM[i])]) {
index = i;
break;
}
}
And the category methods:
-(BOOL) isEqual:(id)otherColor {
if (otherColor == self)
return YES;
if (!otherColor || ![otherColor isKindOfClass:[self class]])
return NO;
return [self isEqualToColor:otherColor];
}
-(BOOL) isEqualToColor:(UIColor*)otherColor {
if (self == otherColor)
return YES;
unsigned char r0, r1, g0, g1, b0, b1;
[self eightBitRed:&r0 green:&g0 blue:&b0];
[otherColor eightBitRed:&r1 green:&g1 blue:&b1];
return r0 == r1 && g0 == g1 && b0 == b1;
}
The short answer is that categories are not intended to override existing methods:
Although the Objective-C language currently allows you to use a category to override methods the class inherits, or even methods declared in the class interface, you are strongly discouraged from doing so. A category is not a substitute for a subclass. There are several significant shortcomings to using a category to override methods:
When a category overrides an inherited
method, the method in the category
can, as usual, invoke the inherited
implementation via a message to super.
However, if a category overrides a
method that exists in the category's
class, there is no way to invoke the
original implementation.
A category cannot reliably override methods declared in another category of the same class.
This issue is of particular significance because many of the Cocoa classes are implemented using categories. A framework-defined method you try to override may itself have been implemented in a category, and so which implementation takes precedence is not defined.
The very presence of some category methods may cause behavior changes across all frameworks. For example, if you override the windowWillClose: delegate method in a category on NSObject, all window delegates in your program then respond using the category method; the behavior of all your instances of NSWindow may change. Categories you add on a framework class may cause mysterious changes in behavior and lead to crashes.
You will need to swizzle the original isEqual: method to use your own implementation. There is a great NSHipster article on swizzling that should get you started.
I want to reload + (BOOL)resolveClassMethod:(SEL)sel method to add my custom implementation of unresolved methods in super class, like this:
+ (BOOL)resolveClassMethod:(SEL)sel {
BOOL result = [super resolveClassMethod:sel];
if (result) {
return result;
}
/*my logic here*/
}
But I notice that, when I call +bundleForClass: method with this class it actually invokes resolveClassMethod: with bundleForClass: selector as parameter. And, of course, [super resolveClassMethod:sel]; return NO and it move to my logic. Code:
NSBundle *classBundle = [NSBundle bundleForClass:NSClassFromString(#"MyClass")];
Why code above invoke +resolveClassMethod: on MyClass? I aim to add implementation for my custom class methods to class, but not implementation of standard method like bundleForClass:, so how can I check methods like that to skip it.