Share
Embed
Expressive Objective-C
Exploring Objective-C through fine-grained, flexible abstractions
Ian Wong
February 26, 2014
Transcript
75 slides
-
Imperative jungle Callbacks Manual sync & KVO Common Solutions I.
Data manipulation ! II. API communication ! III.State management -
I. Data manipulation ! II. API communication ! III.State management
Functional programming Promises Atoms Expressive Solutions -
NSArray *xs = … NSArray *ys = [[xs map:^(X *x)
{ return [self xToY:x]; }] filter:^int(Y *y) { return y.shouldStay; }]; } -
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 -
- (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 -
- (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. -
! ! ! ! ! ! ! ! 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 -
! ! ! ! ! ! ! ! ! !
! ! ! ! ! ! ! ! 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 -
(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 -
- (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 -
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 -
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; } -
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 -
[self addSubviews:@[byline, title, body] layoutInfo:({ LayoutInfo *info = [LayoutInfo new];
info.offsetBy = kOffset; info.addDirection = kSubViewAddDirectionVertical; info.interItemPadding = kPadding; info; })]; UIView+V.m -
- (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 -
- (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 -
- (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 -
typedef void(^CallbackBlock) (id value, NSError *e) ! -(void)deliver:(id)value error:(NSError *)error
! -(void)withValue:(CallbackBlock *)callback Promise -
- (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 -
Then id<Promise> feedPromise = [[API authenticate] then:^id<Promise>(User *user, NSError *e)
{ return [API feedFor:user]; }]; -
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 }]; -
- (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; } -
- (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 } -
- (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 -
@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 -
- (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; } ! -
- (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; } ! -
@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 -
Functional programming Promises Atoms Expressive Solutions I. Data manipulation !
II. API communication ! III.State management