Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Expressive Objective-C

7c2038734171a0731cbe1094ade20a51?s=47 Ian Wong
February 26, 2014

Expressive Objective-C

Exploring Objective-C through fine-grained, flexible abstractions

7c2038734171a0731cbe1094ade20a51?s=128

Ian Wong

February 26, 2014
Tweet

Transcript

  1. Expressive Objective-C Using fine-grained, flexible abstractions Ian Wong @ihat

  2. Expressive = clear + concise + convincing

  3. It’s not easy.

  4. But the right building blocks can help a lot.

  5. I. Data manipulation ! II. API communication ! III.State management

    Common Tasks
  6. Imperative jungle Callbacks Manual sync & KVO Common Solutions I.

    Data manipulation ! II. API communication ! III.State management
  7. I. Data manipulation ! II. API communication ! III.State management

    Functional programming Promises Atoms Expressive Solutions
  8. I. Data manipulation Functional programming

  9. NSArray *xs = … NSArray *ys = [[xs map:^(X *x)

    { return [self xToY:x]; }] filter:^int(Y *y) { return y.shouldStay; }]; }
  10. NSArray+Sequence map:(MapFn)f filter:(PredicateFn)f every:(PredicateFn)f any:(PredicateFn)f reduce:(ReduceFn)f take:(NSUInteger )n butLast inspired

    sum:(DoubleFn)f findFirst:(PredicateFn)f findFirstIndex:(PredicateFn)f argMax:(DoubleFn)f argMin:(DoubleFn)f contains:(NSArray *)arr rest
  11. None
  12. None
  13. - (void)prependActivities:(NSArray *)activities { NSMutableArray *activityCells = [NSMutableArray new]; for

    (Activity *activity in activities) { ActivityCell *activityCell = [self transformActivityCell:activity]; if (activityCell.shouldDisplay) { [activityCells addObject:activityCell]; } } NSMutableArray *indexPaths = [NSMutableArray new]; for (NSUInteger i = 0; i < activities.count; ++i) { [indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]]; } [self.activityCells insertObjects:activityCells atIndexes:[NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, indexPaths.count)]]; dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView beginUpdates]; [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; [self.tableView endUpdates]; }); } 1 2 3 4
  14. Hides intention

  15. - (void)prependActivities:(NSArray *)activities { NSMutableArray *activityCells = [NSMutableArray new]; for

    (Activity *activity in activities) { ActivityCell *activityCell = [self transformActivityCell:activity]; if (activityCell.shouldDisplay) { [activityCells addObject:activityCell]; } } Intent: Transform activities to activity cells removing those that should not be displayed.
  16. ! ! ! ! ! ! ! ! NSMutableArray *indexPaths

    = [NSMutableArray new]; for (NSUInteger i = 0; i < activities.count; ++i) { [indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]]; } Intent: Get NSIndexPaths in section 0 from row 0 up to activities.count
  17. ! ! ! ! ! ! ! ! ! !

    ! ! ! ! ! ! ! ! dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView beginUpdates]; [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; [self.tableView endUpdates]; }); } Intent: Insert rows on the main thread, and in a table view edit transaction
  18. None
  19. (defn prepend-activities [activities] (let [activity-cells (->> activities (map transform-activity-cell) (filter

    :should-display?)) indices (map (partial index-path-for-row-in-section 0) (range (count activity-cells)))] (swap! all-activity-cells conj activity-cells) (on-main-thread (with-table-view-transaction [tv table-view] (insert-rows tv activity-cells indices UITableViewRowAnimationNone))))) Intent, with expressive notation
  20. - (void)prependActivities:(NSArray *)activities { NSArray *activityCells = [[activities map:^(Activity *activity)

    { return [self transformActivityCell:activity]; }] filter:^int(ActivityCell *activity) { return activity.shouldDisplay; }]; NSArray *indexPaths = [F upTo:activityCells.count withFn:^id(NSInteger i) { return [NSIndexPath indexPathForRow:i inSection:0]; }]; [self.activityCells insertObjects:activityCells atIndexes:[NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, indexPaths.count)]]; [F onMainThread:^{ [self.tableView update:^{ [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; }]; }]; } 1 2 3 4
  21. NSArray+Sequence map:(MapFn)f filter:(PredicateFn)f every:(PredicateFn)f any:(PredicateFn)f reduce:(ReduceFn)f take:(NSUInteger )n butLast inspired

    sum:(DoubleFn)f findFirst:(PredicateFn)f findFirstIndex:(PredicateFn)f argMax:(DoubleFn)f argMin:(DoubleFn)f contains:(NSArray *)arr rest
  22. F

  23. Data-focused programming

  24. None
  25. Data-focused programming

  26. Data-focused programming in Views

  27. V

  28. UIView+V.m

  29. None
  30. None
  31. CGFloat startX = offset.x; CGFloat startY = offset.y; for (UIView

    *view in @[byline, title, body]) { [self addSubview:view]; CGRect frame = view.frame; frame.origin = CGPointMake(frame.origin.x, startY); view.frame = frame; startY += padding; }
  32. Intent: Layout 3 subviews vertically, with an offset and a

    padding
  33. None
  34. 3 subviews vertical layout padding offset Doc View 3 positioned

    subviews
  35. LayoutInfo Doc View layoutSubviews: layoutInfo:

  36. LayoutInfo @interface LayoutInfo : NSObject ! @property (nonatomic) LayoutInfoHAlign hAlign;

    @property (nonatomic) LayoutInfoVAlign vAlign; @property (nonatomic) LayoutAddDirection addDirection; @property (nonatomic) CGFloat interItemPadding; @property (nonatomic) CGPoint offsetBy; ! @end
  37. +(void)layoutSubviews:(NSArray *)subviews
 layoutInfo:(LayoutInfo *)layoutInfo ! -(void)addSubviews:(NSArray *)subviews layoutInfo:(LayoutInfo *)layoutInfo UIView+V.m

  38. None
  39. [self addSubviews:@[byline, title, body] layoutInfo:({ LayoutInfo *info = [LayoutInfo new];

    info.offsetBy = kOffset; info.addDirection = kSubViewAddDirectionVertical; info.interItemPadding = kPadding; info; })]; UIView+V.m
  40. V

  41. II. API Communication Promises

  42. Network Delay Coordination Error handling

  43. None
  44. None
  45. - (instancetype)initWithInterest:(Interest *)interest { StatButton *statButton = [StatButton new]; [API

    feedWithInterest:interest onComplete:^(Feed *feed, NSError *e) { if (e) { … } [statButton setStatCount:feed.followersCount]; [statButton addTarget:self action:@selector(statButtonTouchUp) forControlEvents:UIControlEventTouchUpInside]; self.followersUrl = feed.followersUrl; }]; } ! - (void)statButtonTouchUp { [API feedWithUrl:self.nextUrl onComplete:^(Feed *feed, NSError *e) { if (e) { … } InterestTableVC *interestTableVC = [[InterestTableVC alloc] initWithFeed:feed]; [SharedAppDelegate.navController pushViewController:interestTableVC animated:YES]; }]; } Feed Header
  46. - (instancetype)initWithInterest:(Interest *)interest { StatButton *statButton = [StatButton new]; [API

    feedWithInterest:interest onComplete:^(Feed *feed, NSError *e) { if (e) { … } [statButton setStatCount:feed.followersCount]; [statButton addTarget:self action:@selector(statButtonTouchUp) forControlEvents:UIControlEventTouchUpInside]; self.followersUrl = feed.followersUrl; }]; } ! - (void)statButtonTouchUp { InterestTableVC *interestTableVC = [[InterestTableVC alloc] initWithUrlString:self.followersUrl]; [SharedAppDelegate.navController pushViewController:interestTableVC animated:YES]; } Feed Header
  47. - (instancetype)initWithInterest:(Interest *)interest { StatButton *statButton = [StatButton new]; [API

    feedWithInterest:interest onComplete:^(Feed *feed, NSError *e) { if (e) { … } [statButton setStatCount:feed.followersCount]; [statButton addTarget:self action:@selector(statButtonTouchUp) forControlEvents:UIControlEventTouchUpInside]; self.followersUrl = feed.followersUrl; }]; } ! - (void)statButtonTouchUp { InterestTableVC *interestTableVC = [[InterestTableVC alloc] initWithUrlString:self.followersUrl]; [SharedAppDelegate.navController pushViewController:interestTableVC animated:YES]; } Url vs. domain object? Feed Header
  48. Chasing control flow Inconsistent API (Maybe) Slow UI

  49. Promise = Result of an async operation

  50. Promise = Value that may or may not be fulfilled

  51. typedef void(^CallbackBlock) (id value, NSError *e) ! -(void)deliver:(id)value error:(NSError *)error

    ! -(void)withValue:(CallbackBlock *)callback
 Promise
  52. None
  53. - (instancetype)initWithFeedPromise:(id<Promise>)feedPromise { StatButton *statButton = [StatButton new]; [feedPromise withValue:^(Feed

    *feed, NSError *e) { if (e) { … } [statButton setStatCount:feed.followersCount]; [statButton onTouchUp:^{ id<Promise> promise = [API feedForUrl:feed.followersUrl]; InterestTableVC *interestTableVC = [[InterestTableVC alloc] initWithPromise:promise]; [SharedAppDelegate.navController pushViewController:interestTableVC animated:YES]; }]; }]; } Feed Header
  54. Declarative Consistent API Fast UI Response #win

  55. Chaining (no more nesting!) Coordination #somuchwin

  56. Then id<Promise> feedPromise = [[API authenticate] then:^id<Promise>(User *user, NSError *e)

    { return [API feedFor:user]; }];
  57. id<Promise> feedItemsPromise = [promise:map:^NSArray *(Feed *feed) { return feed.items; }];

    Map
  58. All [[Promise all:@[API.fn1, API.fn2]] withValue:^(NSArray *arr, NSError *e) { //

    arr[0] contains result from API.fn1 // arr[1] contains result from API.fn2 }];
  59. F?

  60. III.State management Atoms

  61. None
  62. None
  63. - (void)setupSaveButton:(Doc *)doc { self.saveButton = [UIButton new]; self.saveButton.selected =

    doc.isSaved; [self.saveButton addTarget:self action:@selector(saveButtonPressed) forControlEvents:UIControlEventTouchUpInside]; self.doc = doc; } ! - (void)saveButtonPressed { [self renderSaveView:!self.saveButton.selected]; self.doc.isSaved = !self.doc.isSaved; }
  64. - (void)setupSaveButton:(Doc *)doc { self.saveButton = [UIButton new]; [self.saveButton addTarget:self

    action:@selector(saveButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; self.saveButton.selected = doc.isSaved; self.doc = doc; } ! - (void)saveButtonPressed:(id)sender { [self renderSaveView:!self.saveButton.selected]; self.doc.isSaved = !self.doc.isSaved [API saveDoc:self.doc onComplete:^(BOOL success) { if (!success) { [self renderSaveView:!self.saveButton.selected]; self.doc.isSaved = !self.doc.isSaved } }]; // Q: how do you propagate the change to all local instances of the doc }
  65. - (void)setupSaveButton:(Doc *)doc { self.saveButton = [UIButton new]; [self.saveButton addTarget:self

    action:@selector(saveButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; self.saveButton.selected = doc.isSaved; self.doc = doc; } ! - (void)saveButtonPressed:(id)sender { [self renderSaveView:!self.doc.isSaved]; self.doc.isSaved = !self.doc.isSaved [API saveDoc:self.doc onComplete:^(BOOL success) { if (!success) { [self renderSaveView:!self.doc.isSaved]; self.doc.isSaved = !self.doc.isSaved; } }]; // Q: how do you propagate the change to all local instances of the doc } View logic duplication Communicating through ivar
  66. Coordinating state via UI Awkward interaction with API

  67. Atom = (thread-safe) mutable + watchable

  68. @protocol Watchable typedef void(^WatcherFn) (id oldVal, id newVal, 
 NSError

    *e) -(void)registerWatcher:(id)watcher
 onChange:(WatcherFn)changeFn -(void)removeWatcher:(id)watcher @protocol Atom <Watchable> -(id)value -(void)update:(MapFn)updateFn
  69. None
  70. - (void)setupSaveButton:(Doc *)doc { id<Atom> saveAtom = [DocActions.sharedInstance saveAtomForDoc:doc]; self.saveButton.selected

    = saveAtom.value; @weakify(self) [self.saveButton onTouchUp:^{ @strongify(self) [saveAtom update:F.toggleBoolean]; }]; [saveAtom registerWatcher:self onChange:^(NSNumber *oldVal, NSNumber *newVal, NSError *err) { @strongify(self) [self renderSaveView:[newVal boolValue]]; }]; self.saveAtom = saveAtom; } !
  71. - (void)setupSaveButton:(Doc *)doc { id<ServerBackedAtom> saveAtom = [DocActions.sharedInstance saveAtomForDoc:doc]; @weakify(self)

    [self.saveButton onTouchUp:^{ @strongify(self) [saveAtom update:F.toggleBoolean]; }]; [saveAtom registerWatcher:self onChange:^(NSNumber *oldVal, NSNumber *newVal, NSError *err) { @strongify(self) [self renderSaveView:[newVal boolValue]]; }]; self.saveAtom = saveAtom; } !
  72. @protocol Watchable -(void)registerWatcher:(id)watcher
 onChange:(WatcherFn)changeFn -(void)removeWatcher:(id)watcher @protocol Atom <Watchable> -(id)value -(void)update:(MapFn)updateFn

    @protocol ServerBackedAtom <Atom> -(void)setServerCall
  73. Functional programming Promises Atoms Expressive Solutions I. Data manipulation !

    II. API communication ! III.State management
  74. None
  75. Thanks! ihat@getprismatic.com