Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Expressive = clear + concise + convincing

Slide 3

Slide 3 text

It’s not easy.

Slide 4

Slide 4 text

But the right building blocks can help a lot.

Slide 5

Slide 5 text

I. Data manipulation ! II. API communication ! III.State management Common Tasks

Slide 6

Slide 6 text

Imperative jungle Callbacks Manual sync & KVO Common Solutions I. Data manipulation ! II. API communication ! III.State management

Slide 7

Slide 7 text

I. Data manipulation ! II. API communication ! III.State management Functional programming Promises Atoms Expressive Solutions

Slide 8

Slide 8 text

I. Data manipulation Functional programming

Slide 9

Slide 9 text

NSArray *xs = … NSArray *ys = [[xs map:^(X *x) { return [self xToY:x]; }] filter:^int(Y *y) { return y.shouldStay; }]; }

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

- (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

Slide 14

Slide 14 text

Hides intention

Slide 15

Slide 15 text

- (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.

Slide 16

Slide 16 text

! ! ! ! ! ! ! ! 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

Slide 17

Slide 17 text

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! 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

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

(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

Slide 20

Slide 20 text

- (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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

F

Slide 23

Slide 23 text

Data-focused programming

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

Data-focused programming

Slide 26

Slide 26 text

Data-focused programming in Views

Slide 27

Slide 27 text

V

Slide 28

Slide 28 text

UIView+V.m

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

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; }

Slide 32

Slide 32 text

Intent: Layout 3 subviews vertically, with an offset and a padding

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

3 subviews vertical layout padding offset Doc View 3 positioned subviews

Slide 35

Slide 35 text

LayoutInfo Doc View layoutSubviews: layoutInfo:

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

+(void)layoutSubviews:(NSArray *)subviews
 layoutInfo:(LayoutInfo *)layoutInfo ! -(void)addSubviews:(NSArray *)subviews layoutInfo:(LayoutInfo *)layoutInfo UIView+V.m

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

[self addSubviews:@[byline, title, body] layoutInfo:({ LayoutInfo *info = [LayoutInfo new]; info.offsetBy = kOffset; info.addDirection = kSubViewAddDirectionVertical; info.interItemPadding = kPadding; info; })]; UIView+V.m

Slide 40

Slide 40 text

V

Slide 41

Slide 41 text

II. API Communication Promises

Slide 42

Slide 42 text

Network Delay Coordination Error handling

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

- (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

Slide 46

Slide 46 text

- (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

Slide 47

Slide 47 text

- (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

Slide 48

Slide 48 text

Chasing control flow Inconsistent API (Maybe) Slow UI

Slide 49

Slide 49 text

Promise = Result of an async operation

Slide 50

Slide 50 text

Promise = Value that may or may not be fulfilled

Slide 51

Slide 51 text

typedef void(^CallbackBlock) (id value, NSError *e) ! -(void)deliver:(id)value error:(NSError *)error ! -(void)withValue:(CallbackBlock *)callback
 Promise

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

- (instancetype)initWithFeedPromise:(id)feedPromise { StatButton *statButton = [StatButton new]; [feedPromise withValue:^(Feed *feed, NSError *e) { if (e) { … } [statButton setStatCount:feed.followersCount]; [statButton onTouchUp:^{ id promise = [API feedForUrl:feed.followersUrl]; InterestTableVC *interestTableVC = [[InterestTableVC alloc] initWithPromise:promise]; [SharedAppDelegate.navController pushViewController:interestTableVC animated:YES]; }]; }]; } Feed Header

Slide 54

Slide 54 text

Declarative Consistent API Fast UI Response #win

Slide 55

Slide 55 text

Chaining (no more nesting!) Coordination #somuchwin

Slide 56

Slide 56 text

Then id feedPromise = [[API authenticate] then:^id(User *user, NSError *e) { return [API feedFor:user]; }];

Slide 57

Slide 57 text

id feedItemsPromise = [promise:map:^NSArray *(Feed *feed) { return feed.items; }]; Map

Slide 58

Slide 58 text

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 }];

Slide 59

Slide 59 text

F?

Slide 60

Slide 60 text

III.State management Atoms

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

No content

Slide 63

Slide 63 text

- (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; }

Slide 64

Slide 64 text

- (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 }

Slide 65

Slide 65 text

- (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

Slide 66

Slide 66 text

Coordinating state via UI Awkward interaction with API

Slide 67

Slide 67 text

Atom = (thread-safe) mutable + watchable

Slide 68

Slide 68 text

@protocol Watchable typedef void(^WatcherFn) (id oldVal, id newVal, 
 NSError *e) -(void)registerWatcher:(id)watcher
 onChange:(WatcherFn)changeFn -(void)removeWatcher:(id)watcher @protocol Atom -(id)value -(void)update:(MapFn)updateFn

Slide 69

Slide 69 text

No content

Slide 70

Slide 70 text

- (void)setupSaveButton:(Doc *)doc { id 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; } !

Slide 71

Slide 71 text

- (void)setupSaveButton:(Doc *)doc { id 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; } !

Slide 72

Slide 72 text

@protocol Watchable -(void)registerWatcher:(id)watcher
 onChange:(WatcherFn)changeFn -(void)removeWatcher:(id)watcher @protocol Atom -(id)value -(void)update:(MapFn)updateFn @protocol ServerBackedAtom -(void)setServerCall

Slide 73

Slide 73 text

Functional programming Promises Atoms Expressive Solutions I. Data manipulation ! II. API communication ! III.State management

Slide 74

Slide 74 text

No content

Slide 75

Slide 75 text