Created
January 27, 2020 10:20
-
-
Save 0xced/f704c1fefe75dd3f6c167cc5f8d24e9d to your computer and use it in GitHub Desktop.
[Work In Progress] Backport of the IBSegueAction feature for iOS < 13
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ⚠️ WORK IN PROGRESS, NOT FUNCTIONAL ⚠️ | |
// Backport of the IBSegueAction feature for iOS < 13 | |
// See https://sarunw.com/posts/better-dependency-injection-for-storyboards-in-ios13/ and https://useyourloaf.com/blog/better-storyboards-with-xcode-11/ | |
// | |
@import Foundation; | |
BOOL XCDBackportIBSegueAction(void); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import "IBSegueActionBackport.h" | |
@import ObjectiveC; | |
@import UIKit; | |
@interface NSObject (XCDBackport) | |
- (id) xcd_initWithCoder_UIClassSwapper:(NSCoder *)decoder; | |
- (id) xcd_initWithCoder_UIStoryboardSegueTemplate:(NSCoder *)decoder; | |
@end | |
@interface NSCoder (XCDBackport) | |
- (id) xcd_decodeObjectForKey:(NSString *)key; | |
@end | |
@interface NSDictionary (XCDBackport) | |
- (id) xcd_objectForKey:(NSString *)key; | |
@end | |
@interface NSObject (UIStoryboard) | |
- (id) nibForViewControllerWithIdentifier:(NSString *)identifier; | |
@end | |
@interface NSObject (UIStoryboardScene) | |
@property (readonly) UIViewController *sceneViewController; | |
@end | |
@interface NSObject (UIStoryboardSegueTemplate) | |
@property (readonly) UIViewController *viewController; | |
@end | |
@interface NSObject (UIViewController) | |
@property NSString *storyboardIdentifier; | |
@end | |
@interface UIStoryboard (XCDBackport) | |
- (nullable __kindof UIViewController *) xcd_instantiateInitialViewControllerWithCreator:(UIStoryboardViewControllerCreator)creator; | |
- (__kindof UIViewController *) xcd_instantiateViewControllerWithIdentifier:(NSString *)identifier creator:(UIStoryboardViewControllerCreator)creator; | |
- (__kindof UIViewController *) xcd_instantiateViewControllerWithIdentifier:(NSString *)identifier creator:(UIStoryboardViewControllerCreator)creator storyboardSegueTemplate:(id)storyboardSegueTemplate sender:(id)sender; | |
@end | |
static id instantiateOrFindDestinationViewControllerWithSenderIMP(id /* UIStoryboardSegueTemplate */ self, SEL _cmd, id sender) | |
{ | |
UIViewController *viewController = [self viewController]; | |
NSString *identifier = [self valueForKey:@"destinationViewControllerIdentifier"]; | |
return [viewController.storyboard xcd_instantiateViewControllerWithIdentifier:identifier creator:nil storyboardSegueTemplate:self sender:sender]; | |
} | |
static Class UIStoryboardScene = Nil; | |
BOOL XCDBackportIBSegueAction(void) | |
{ | |
Method instantiateViewControllerWithIdentifier_creator = class_getInstanceMethod(UIStoryboard.class, @selector(xcd_instantiateViewControllerWithIdentifier:creator:)); | |
if (!class_addMethod(UIStoryboard.class, @selector(instantiateViewControllerWithIdentifier:creator:), method_getImplementation(instantiateViewControllerWithIdentifier_creator), method_getTypeEncoding(instantiateViewControllerWithIdentifier_creator))) | |
return NO; // method already exists => running on iOS 13+ | |
Method instantiateInitialViewControllerWithCreator = class_getInstanceMethod(UIStoryboard.class, @selector(xcd_instantiateInitialViewControllerWithCreator:)); | |
if (!class_addMethod(UIStoryboard.class, @selector(instantiateInitialViewControllerWithCreator:), method_getImplementation(instantiateInitialViewControllerWithCreator), method_getTypeEncoding(instantiateInitialViewControllerWithCreator))) | |
return NO; // method already exists => running on iOS 13+ | |
UIStoryboardScene = NSClassFromString([@[ @"UI", @"Storyboard", @"Scene" ] componentsJoinedByString:@""]); | |
if (!UIStoryboardScene) | |
return NO; | |
Class UINibDecoder = NSClassFromString([@[ @"UI", @"Nib", @"Decoder" ] componentsJoinedByString:@""]); | |
if (!UINibDecoder) | |
return NO; | |
Method decodeObjectForKeyMethod = class_getInstanceMethod(UINibDecoder, @selector(decodeObjectForKey:)); | |
Method xcd_decodeObjectForKeyMethod = class_getInstanceMethod(NSCoder.class, @selector(xcd_decodeObjectForKey:)); | |
method_exchangeImplementations(decodeObjectForKeyMethod, xcd_decodeObjectForKeyMethod); | |
Class UIClassSwapper = NSClassFromString([@[ @"UI", @"Class", @"Swapper" ] componentsJoinedByString:@""]); | |
if (!UIClassSwapper) | |
return NO; | |
Method initWithCoderUIClassSwapperMethod = class_getInstanceMethod(UIClassSwapper, @selector(initWithCoder:)); | |
Method xcd_initWithCoderUIClassSwapperMethod = class_getInstanceMethod(NSObject.class, @selector(xcd_initWithCoder_UIClassSwapper:)); | |
method_exchangeImplementations(initWithCoderUIClassSwapperMethod, xcd_initWithCoderUIClassSwapperMethod); | |
Class UIStoryboardSegueTemplate = NSClassFromString([@[ @"UI", @"Storyboard", @"Segue", @"Template" ] componentsJoinedByString:@""]); | |
Method instantiateOrFindDestinationViewControllerWithSender = class_getInstanceMethod(UIStoryboardSegueTemplate, NSSelectorFromString([@[ @"instantiate", @"Or", @"Find", @"Destination", @"ViewController", @"With", @"Sender", @":" ] componentsJoinedByString:@""])); | |
if (!instantiateOrFindDestinationViewControllerWithSender) | |
return NO; | |
Method initWithCoderUIStoryboardSegueTemplateMethod = class_getInstanceMethod(UIStoryboardSegueTemplate, @selector(initWithCoder:)); | |
Method xcd_initWithCoderUIStoryboardSegueTemplateMethod = class_getInstanceMethod(NSObject.class, @selector(xcd_initWithCoder_UIStoryboardSegueTemplate:)); | |
method_exchangeImplementations(initWithCoderUIStoryboardSegueTemplateMethod, xcd_initWithCoderUIStoryboardSegueTemplateMethod); | |
method_setImplementation(instantiateOrFindDestinationViewControllerWithSender, (IMP)instantiateOrFindDestinationViewControllerWithSenderIMP); | |
return YES; | |
} | |
static NSMutableDictionary *options; | |
@implementation UIStoryboard (XCDBackport) | |
- (nullable __kindof UIViewController *) xcd_instantiateInitialViewControllerWithCreator:(UIStoryboardViewControllerCreator)creator | |
{ | |
NSString *designatedEntryPointIdentifier = [self valueForKey:@"designatedEntryPointIdentifier"]; | |
if (designatedEntryPointIdentifier) | |
return [self xcd_instantiateViewControllerWithIdentifier:designatedEntryPointIdentifier creator:creator storyboardSegueTemplate:nil sender:nil]; | |
return nil; | |
} | |
- (__kindof UIViewController *) xcd_instantiateViewControllerWithIdentifier:(NSString *)identifier creator:(UIStoryboardViewControllerCreator)creator | |
{ | |
return [self xcd_instantiateViewControllerWithIdentifier:identifier creator:creator storyboardSegueTemplate:nil sender:nil]; | |
} | |
- (__kindof UIViewController *) xcd_instantiateViewControllerWithIdentifier:(NSString *)identifier creator:(UIStoryboardViewControllerCreator)creator storyboardSegueTemplate:(id)storyboardSegueTemplate sender:(id)sender | |
{ | |
UINib *nib = [self nibForViewControllerWithIdentifier:identifier]; | |
if (nib) | |
{ | |
options = [NSMutableDictionary dictionaryWithObject:@{ @"UIStoryboardPlaceholder": self } forKey:UINibExternalObjects]; | |
if (storyboardSegueTemplate) | |
options[@"UINibSourceSegueTemplate"] = storyboardSegueTemplate; | |
if (sender) | |
options[@"UINibPerformSegueSender"] = sender; | |
if (creator) | |
options[@"UINibPerformSegueCreator"] = creator; | |
id scene = [[UIStoryboardScene alloc] init]; | |
[nib instantiateWithOwner:scene options:options]; | |
options = nil; | |
UIViewController *sceneViewController = [scene sceneViewController]; | |
NSAssert(sceneViewController != nil, @"Could not load the scene view controller for identifier '%@'", identifier); | |
if (sceneViewController.storyboardIdentifier == nil) { | |
sceneViewController.storyboardIdentifier = identifier; | |
} | |
return sceneViewController; | |
} | |
else | |
{ | |
NSString *reason = [NSString stringWithFormat:@"Storyboard (%@) doesn't contain a view controller with identifier '%@'", self, identifier]; | |
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; | |
} | |
} | |
@end | |
@interface XCDStoryboardDecodingContext : NSObject | |
@property (nonatomic,retain) id /* UIClassSwapper */ classSwapperTemplate; | |
@property (nonatomic,retain) id /* UIStoryboardSegueTemplate */ sourceSegueTemplate; | |
@property (nonatomic,retain) UIViewController * parentViewController; | |
@property (assign,nonatomic) long long childViewControllerIndex; | |
@property (nonatomic,retain) id sender; | |
@property (nonatomic,copy) id creator; | |
@end | |
@implementation XCDStoryboardDecodingContext | |
@end | |
@implementation NSCoder (XCDBackport) | |
static void const * const XCDStoryboardDecodingContextKey = &XCDStoryboardDecodingContextKey; | |
- (XCDStoryboardDecodingContext *) xcd_storyboardDecodingContext | |
{ | |
return objc_getAssociatedObject(self, XCDStoryboardDecodingContextKey); | |
} | |
- (void) xcd_createStoryboardDecodingContextIfNeeded | |
{ | |
if (self.xcd_storyboardDecodingContext == nil) | |
objc_setAssociatedObject(self, XCDStoryboardDecodingContextKey, [XCDStoryboardDecodingContext new], OBJC_ASSOCIATION_RETAIN_NONATOMIC); | |
} | |
- (id) xcd_decodeObjectForKey:(NSString *)key | |
{ | |
if (options && [key isEqualToString:@"UINibConnectionsKey"]) | |
return [self xcd_decodeObjectsWithSourceSegueTemplate:options[@"UINibSourceSegueTemplate"] creator:options[@"UINibPerformSegueCreator"] sender:options[@"UINibPerformSegueSender"] forKey:key]; | |
else | |
return [self xcd_decodeObjectForKey:key]; // swizzled => calls original implementation | |
} | |
- (id) xcd_decodeObjectsWithSourceSegueTemplate:(id)sourceSegueTemplate creator:(UIStoryboardViewControllerCreator)creator sender:(id)sender forKey:(NSString *)key | |
{ | |
id currentSourceSegueTemplate = self.xcd_storyboardDecodingContext.sourceSegueTemplate; | |
id currentSender = self.xcd_storyboardDecodingContext.sender; | |
id currentCreator = self.xcd_storyboardDecodingContext.creator; | |
[self xcd_createStoryboardDecodingContextIfNeeded]; | |
self.xcd_storyboardDecodingContext.sourceSegueTemplate = sourceSegueTemplate; | |
self.xcd_storyboardDecodingContext.sender = sender; | |
self.xcd_storyboardDecodingContext.creator = creator; | |
id result = [self xcd_decodeObjectForKey:key]; // swizzled => calls original implementation | |
self.xcd_storyboardDecodingContext.sourceSegueTemplate = currentSourceSegueTemplate; | |
self.xcd_storyboardDecodingContext.sender = currentSender; | |
self.xcd_storyboardDecodingContext.creator = currentCreator; | |
return result; | |
} | |
@end | |
@implementation NSObject (XCDBackport) | |
id (*performPrepareForChildViewController)(id, SEL, id, id, id) = (id (*)(id, SEL, id, id, id)) objc_msgSend; | |
static void const * const PrepareForChildViewControllerSelectorNameKey = &PrepareForChildViewControllerSelectorNameKey; | |
- (id) xcd_initWithCoder_UIClassSwapper:(NSCoder *)decoder | |
{ | |
// Note: I had aleady reversed -[UIClassSwapper initWithCoder:] on iOS < 13 which was a very small method, see https://gist.github.com/0xced/45daf79b62ad6a20be1c | |
// TODO: see actual implementation on iOS 13, the current implementation is not complete | |
id sourceSegueTemplate = decoder.xcd_storyboardDecodingContext.sourceSegueTemplate; | |
id selectorName = objc_getAssociatedObject(sourceSegueTemplate, PrepareForChildViewControllerSelectorNameKey); | |
SEL prepareForChildViewControllerSelector = NSSelectorFromString(selectorName); | |
UIViewController *viewController = [sourceSegueTemplate viewController]; | |
if ([viewController respondsToSelector:prepareForChildViewControllerSelector]) | |
{ | |
id sender = decoder.xcd_storyboardDecodingContext.sender; | |
id segueIdentifier = [sourceSegueTemplate identifier]; | |
return performPrepareForChildViewController(viewController, prepareForChildViewControllerSelector, decoder, sender, segueIdentifier); | |
} | |
return [self xcd_initWithCoder_UIClassSwapper:decoder]; | |
} | |
- (id) xcd_initWithCoder_UIStoryboardSegueTemplate:(NSCoder *)decoder | |
{ | |
id storyboardSegueTemplate = [self xcd_initWithCoder_UIStoryboardSegueTemplate:decoder]; | |
id selectorName = [decoder decodeObjectForKey:@"UICustomPrepareForChildViewControllersSegueName"]; | |
objc_setAssociatedObject(storyboardSegueTemplate, PrepareForChildViewControllerSelectorNameKey, selectorName, OBJC_ASSOCIATION_RETAIN_NONATOMIC); | |
return storyboardSegueTemplate; | |
} | |
@end | |
#warning TODO: swizzle -[UIViewController initWithCoder:] and call [decoder _initializeClassSwapperWithCurrentDecodingViewControllerIfNeeded:self] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment