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

Expressive Objective-C

Ian Wong
February 26, 2014

Expressive Objective-C

Exploring Objective-C through fine-grained, flexible abstractions

Ian Wong

February 26, 2014
Tweet

More Decks by Ian Wong

Other Decks in Programming

Transcript

  1. Imperative jungle Callbacks Manual sync & KVO Common Solutions I.

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

    Functional programming Promises Atoms Expressive Solutions
  3. NSArray *xs = … NSArray *ys = [[xs map:^(X *x)

    { return [self xToY:x]; }] filter:^int(Y *y) { return y.shouldStay; }]; }
  4. 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
  5. - (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
  6. - (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.
  7. ! ! ! ! ! ! ! ! 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
  8. ! ! ! ! ! ! ! ! ! !

    ! ! ! ! ! ! ! ! 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
  9. (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
  10. - (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
  11. 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
  12. F

  13. V

  14. 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; }
  15. 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
  16. [self addSubviews:@[byline, title, body] layoutInfo:({ LayoutInfo *info = [LayoutInfo new];

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

  18. - (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
  19. - (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
  20. - (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
  21. - (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
  22. 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 }];
  23. F?

  24. - (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; }
  25. - (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 }
  26. - (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
  27. @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
  28. - (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; } !
  29. - (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; } !