Skip to content

Instantly share code, notes, and snippets.

@0xced
Created January 27, 2020 10:20
Show Gist options
  • Select an option

  • Save 0xced/f704c1fefe75dd3f6c167cc5f8d24e9d to your computer and use it in GitHub Desktop.

Select an option

Save 0xced/f704c1fefe75dd3f6c167cc5f8d24e9d to your computer and use it in GitHub Desktop.

Revisions

  1. 0xced created this gist Jan 27, 2020.
    9 changes: 9 additions & 0 deletions IBSegueActionBackport.h
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,9 @@
    //
    // ⚠️ 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);
    233 changes: 233 additions & 0 deletions IBSegueActionBackport.m
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,233 @@
    #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]