Skip to content

Instantly share code, notes, and snippets.

@0xced
Created January 27, 2020 10:20
Show Gist options
  • Save 0xced/f704c1fefe75dd3f6c167cc5f8d24e9d to your computer and use it in GitHub Desktop.
Save 0xced/f704c1fefe75dd3f6c167cc5f8d24e9d to your computer and use it in GitHub Desktop.
[Work In Progress] Backport of the IBSegueAction feature for iOS < 13
//
// ⚠️ 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);
#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