$30 off During Our Annual Pro Sale. View Details »

Async View Controllers

Async View Controllers

This are the slides for the talk I gave at UIKonf 2015.

Tom Adriaenssen

May 18, 2015
Tweet

More Decks by Tom Adriaenssen

Other Decks in Programming

Transcript

  1. ASYNC VIEWCONTROLLERS
    Tom Adriaenssen
    @inferis ∴ http://inferis.org

    View Slide

  2. VIEW
    CONTROLLERS

    View Slide

  3. View Slide

  4. ASYNC

    View Slide

  5. ANATOMY OF ASYNC DATA
    1. start slow call
    2. show hey, keep yer pants on indicator
    3. wait for call to finish
    4. remove pants indicator
    5. show your actual data

    View Slide

  6. SOME
    HISTORY

    View Slide

  7. View Slide

  8. View Slide

  9. View Slide

  10. MASSIVE
    VIEW
    CONTROLLER

    View Slide

  11. MASSIVE VIEW CONTROLLER
    IPHONE - 1.0
    // Lines of code, including whitespace
    * Hotspot:
    * HSViewController .h: 25, .m: 716
    * HSBoxController .h: 44, .m: 370
    * Portfolio: .h: 14, .m: 523

    View Slide

  12. View Slide

  13. View Slide

  14. TECHNICAL
    DEBT

    View Slide

  15. BREAK
    THINGS
    UP

    View Slide

  16. INTRODUCING
    ASYNCVIEWCONTROLLER

    View Slide

  17. View Slide

  18. @interface AsyncViewController : UIViewController
    @property (nonatomic, strong, readonly)
    UIView *asyncView;
    @property (nonatomic, strong, readonly)
    UIView *statusView;
    - (void)performAsyncDataRequest;
    @end

    View Slide

  19. Y U NO SWIFT?

    View Slide

  20. @property (nonatomic, strong) UIView *asyncView;

    View Slide

  21. UIView *

    View Slide

  22. @Inferis None yet. You can
    define a generic parameter BaseClass where T: Protocol>,
    but that isn't representable as a
    type yet.
    — @jckarter

    View Slide

  23. View Slide

  24. rdar://20990743

    View Slide

  25. rdar://20990743

    View Slide

  26. @interface AsyncViewController : UIViewController
    @property (nonatomic, strong, readonly)
    UIView *asyncView;
    @property (nonatomic, strong, readonly)
    UIView *statusView;
    - (void)performAsyncDataRequest;
    @end

    View Slide

  27. SUBCLASS OF
    UIViewController

    View Slide

  28. @property (nonatomic, strong, readonly)
    UIView *asyncView;

    View Slide

  29. @property (nonatomic, strong, readonly)
    UIView *statusView;

    View Slide

  30. - performAsyncDataRequest

    View Slide

  31. ASYNC DATA FETCH
    &
    STATUSVIEW + ASYNCVIEW
    &
    VIEW CONTROLLER CONTAINMENT

    View Slide

  32. SRP

    View Slide

  33. A MORE PURE
    MVC

    View Slide

  34. EASE OF USE

    View Slide

  35. FLEXIBLE

    View Slide

  36. TESTABLE

    View Slide

  37. View Slide

  38. @protocol IIAsyncView
    @required
    @property (nonatomic, strong, readonly) id asyncData;
    - (void)asyncDataApplyValueAnimated:(BOOL)animated;
    @optional
    // ...
    @end

    View Slide

  39. @protocol IIAsyncData
    @required
    @property (nonatomic, assign, readonly) BOOL loading;
    @property (nonatomic, strong) NSError *error;
    @property (nonatomic, strong) id value;
    - (void)reset;
    @end

    View Slide

  40. typedef NS_ENUM(NSUInteger, AsyncStatusViewState) {
    IIAsyncStatusLoading,
    IIAsyncStatusError,
    IIAsyncStatusNoData,
    IIAsyncStatusData
    };

    View Slide

  41. View Slide

  42. - (AsyncStatusViewState)determineState
    {
    // determine the new state
    if (![self isAsyncViewLoaded] || [self.asyncView.asyncData isLoading]) {
    return AsyncStatusLoading;
    }
    else if (self.asyncView.asyncData.error) {
    return AsyncStatusError;
    }
    else if (self.asyncView.asyncData.value) {
    return AsyncStatusData;
    }
    else {
    return AsyncStatusNoData;
    }
    }

    View Slide

  43. @protocol IIAsyncData
    @required
    @property (nonatomic, assign, readonly) BOOL loading;
    @property (nonatomic, strong) NSError *error;
    @property (nonatomic, strong) id value;
    @property (nonatomic, weak) id asyncDataDelegate;
    - (void)reset;
    - (void)invalidateState;
    @end
    @protocol IIAsyncDataDelegate
    @required
    - (void)asyncData:(id)data
    didInvalidateStateForced:(BOOL)forced;
    - (void)reloadAsyncData;
    @end

    View Slide

  44. @protocol AsyncView
    @required
    // ...
    @optional
    - (id)asyncNoDataMessage;
    - (BOOL)asyncCanReload;
    @end

    View Slide

  45. @protocol AsyncStatusView
    @required
    @property (nonatomic, strong) UIView *asyncView;
    - (void)transitionToLoadingStateAnimated:(BOOL)animated;
    - (void)transitionToErrorState:(NSError*)error animated:(BOOL)animated;
    - (void)transitionToNoDataStateAnimated:(BOOL)animated;
    - (void)transitionToDataStateAnimated:(BOOL)animated;
    @end

    View Slide

  46. @interface AsyncData : NSObject
    @interface AsyncView : UIView
    @interface AsyncStatusView : NSObject
    @interface AsyncViewController : UIViewController

    View Slide

  47. @interface IIAsyncViewController : UIViewController
    - (void)reloadAsyncData;
    - (UIView*)loadStatusView;
    - (void)willTransitionToNewStateAnimated:(BOOL)animated;
    - (void)didTransitionToLoadingStateAnimated:(BOOL)animated;
    - (void)didTransitionToErrorStateAnimated:(BOOL)animated;
    - (void)didTransitionToDataStateAnimated:(BOOL)animated;
    - (void)didTransitionToNoDataStateAnimated:(BOOL)animated;
    @end

    View Slide

  48. EXAMPLE

    View Slide

  49. @interface SuchViewController ()
    @property (nonatomic, strong) LaggyBackend *backend;
    @end
    @implementation SuchViewController
    - (void)performAsyncDataRequest
    {
    [self.backend thatReallySlowCall:^(id result) {
    self.asyncView.asyncData.value = result;
    } error:^(NSError* error) {
    self.asyncView.asyncData.error = error;
    }];
    }
    @end

    View Slide

  50. @interface SuchView ()
    @property (nonatomic, strong) IBOutlet UILabel *label;
    @end
    @implementation SuchView
    - (void)asyncDataApplyValueAnimated:(BOOL)animated
    {
    self.label.text = [NSString stringWithFormat:
    @"Such %@", self.asyncData.value];
    }
    - (BOOL)asyncCanReload
    {
    return self.asyncData.error != nil;
    }

    View Slide

  51. UNDER THE
    HOOD

    View Slide

  52. // In AsyncViewController
    - (void)viewDidLoad
    {
    [self wrapWithStatusView];
    // call regular viewDidLoad
    [super viewDidLoad];
    // force an update of the state to loading
    [self updateState:YES];
    [self performAsyncDataRequest];
    }

    View Slide

  53. - (void)wrapWithStatusView
    {
    // don't wrap if there's a status view already
    if (self.statusView) return;
    // get async view. This is just self.view when first wrapping.
    UIView *asyncView = (UIView*)self.view;
    // now create the status view
    UIView *statusView = [self loadStatusView];
    statusView.frame = asyncView.frame;
    statusView.asyncView = asyncView;
    asyncView.asyncData.asyncDataDelegate = self;
    // make main view the wrapping view
    [super setView:statusView];
    }

    View Slide

  54. View Slide

  55. @interface UIViewController (UILayoutSupport)
    // These objects may be used as layout items
    // in the NSLayoutConstraint API
    @property(nonatomic,readonly,strong)
    id topLayoutGuide;
    @property(nonatomic,readonly,strong)
    id bottomLayoutGuide;
    @end

    View Slide

  56. View Slide

  57. 1. find constraints related to layout guides before
    wrapping
    2. wrap views
    3. first layout pass: recreate constraints

    View Slide

  58. - (NSArray*)findLayoutGuideConstraints:(UIView*)view
    {
    NSMutableArray *constraints = [NSMutableArray new];
    UIViewController *viewController = [self findViewDelegate:view];
    if (!viewController) return;
    for (NSLayoutConstraint *constraint in view.constraints) {
    if (constraint.class != [NSLayoutConstraint class]) continue;
    if (constraint.firstItem == viewController.topLayoutGuide ||
    constraint.secondItem == viewController.topLayoutGuide) {
    [constraints addObject:@[constraint, @YES, viewController.topLayoutGuide]];
    }
    else if (constraint.firstItem == delegate.bottomLayoutGuide ||
    constraint.secondItem == delegate.bottomLayoutGuide) {
    [constraints addObject:@[constraint, @NO, viewController.bottomLayoutGuide]];
    }
    }
    return [constraints copy];
    }

    View Slide

  59. - (UIViewController*)findViewDelegate:(UIView*)view

    View Slide

  60. - (UIViewController*)findViewDelegate:(UIView*)view
    {
    const char *ivarName = [[NSString stringWithFormat:@"_%@Delegate", @"view"] UTF8String];
    Ivar ivar = class_getInstanceVariable(view.class, ivarName);
    UIViewController *delegate = nil;
    if (ivar) {
    delegate = object_getIvar(view, ivar);
    if ([delegate isKindOfClass:[UIViewController class]]) {
    return delegate;
    }
    }
    return nil;
    }

    View Slide

  61. ! 

    View Slide

  62. - (void)reapplyLayoutGuideConstraints:(NSArray*)constraintInfos
    {
    UIViewController *viewController = [self findViewDelegate:self];
    if (!viewController) return;
    for (NSArray *constraintInfo in constraintInfos) {
    NSLayoutConstraint *oldConstraint = constraintInfo[0];
    BOOL topGuide = [constraintInfo[1] boolValue];
    // replace first and/or second item with new guides
    id firstItem = oldConstraint.firstItem == constraintInfo[2]
    ? (topGuide ? viewController.topLayoutGuide : viewController.bottomLayoutGuide) :
    : oldConstraint.firstItem;
    id secondItem = oldConstraint.secondItem == constraintInfo[2]
    ? (topGuide ? viewController.topLayoutGuide : viewController.bottomLayoutGuide)
    : oldConstraint.secondItem;
    // Create new constraint
    NSLayoutConstraint *constraint =
    [NSLayoutConstraint constraintWithItem:firstItem
    attribute:oldConstraint.firstAttribute
    relatedBy:oldConstraint.relation
    toItem:secondItem
    attribute:oldConstraint.secondAttribute
    multiplier:oldConstraint.multiplier
    constant:oldConstraint.constant];
    [self addConstraint:constraint];
    }
    }

    View Slide

  63. View Slide

  64. DETAILS

    View Slide

  65. MASSIVE
    VIEW CONTROLLER
    A REPRISE

    View Slide

  66. MASSIVE VIEW CONTROLLER
    IPHONE - 1.0
    // Lines of code, including whitespace
    * Portfolio: .h: 14, .m: 523
    * Hotspot:
    * HSViewController .h: 25, .m: 716
    * HSBoxController .h: 44, .m: 370

    View Slide

  67. MASSIVE VIEW CONTROLLER
    IPHONE - 1.2
    // Lines of code, including whitespace
    * Portfolio: .h: 16, .m: 373
    * Hotspot:
    * HSViewController: .h: 28, .m: 539
    * HSBoxController: 41.h, .m: 108

    View Slide

  68. MASSIVE VIEW CONTROLLER
    IPHONE - 1.0 VS 1.2
    * Portfolio: .h: 114%, .m: 71%
    * Hotspot:
    * HSViewController: .h: 112%.h, .m: 75%
    * HSBoxController: .h: 93%.h, .m: 29%

    View Slide

  69. ¯\_()_/¯

    View Slide

  70. TAKEAWAY

    View Slide

  71. 1. embrace View Controller Containment
    2. reduce dependencies
    3. avoid boilerplate
    4. DRY
    5. testable

    View Slide

  72. OPEN SOURCE

    View Slide

  73. Reference implementation:
    http://github.com/inferis/IIASyncViewController

    View Slide

  74. THANKS!
    Get in touch!
    @inferis
    http://blog.inferis.org

    View Slide