Slide 1

Slide 1 text

CHRIS TROTT @TWOCENTSTUDIOS MVVM ARCHITECTURE AT TIMEHOP

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

100K 1M 10M

Slide 5

Slide 5 text

LET’S TALK ABOUT ARCHITECTURE

Slide 6

Slide 6 text

Architecture Examples • Apple? • Open source? • Other platforms? • Large scale? Small scale?

Slide 7

Slide 7 text

LET’S TALK ABOUT MVVM

Slide 8

Slide 8 text

MVVM Model View View Model

Slide 9

Slide 9 text

VVMM View - View Model - Model

Slide 10

Slide 10 text

Why MVVM? • Isolation • Reusability • Refactorability • Testability • Predictability

Slide 11

Slide 11 text

VIEWS SHOULD BE DUMB

Slide 12

Slide 12 text

VIEWS SHOULD BE DUMB SMART (ABOUT VIEW STUFF)

Slide 13

Slide 13 text

PUT YOUR SMARTS IN THE VIEW MODEL

Slide 14

Slide 14 text

HOW DO WE MVVM @ TIMEHOP?

Slide 15

Slide 15 text

REACTIVECOCOA + MVVM = ❤️

Slide 16

Slide 16 text

EXAMPLE 1 Userlist https://github.com/timehop/Userlist

Slide 17

Slide 17 text

Ownership Data Flow VIEW VIEW MODEL CONTROLLER DATA SOURCE VIEW VIEW MODEL CONTROLLER DATA SOURCE RAW MODEL MODEL VIEW MODEL

Slide 18

Slide 18 text

Userlist Ownership UserController UsersViewModel UserViewModel User UserViewModel UserViewModel UserViewModel UsersViewController UserCell UserCell UserCell UserCell smarter dumber

Slide 19

Slide 19 text

Controllers • Not UIViewControllers! • PONSOs (plain old NSObjects) • Convert Raw Models to Models • Can’t manipulate View Models

Slide 20

Slide 20 text

Userlist Ownership UserController UsersViewModel UserViewModel User UserViewModel UserViewModel UserViewModel UsersViewController UserCell UserCell UserCell UserCell smarter dumber

Slide 21

Slide 21 text

// User.h @interface User : NSObject @property (nonatomic, readonly) NSString *name; @property (nonatomic, readonly) NSURL *avatarURL; - (instancetype)initWithName:(NSString *)name avatarURL: (NSURL *)avatarURL; @end

Slide 22

Slide 22 text

// UserController.h @interface UserController : NSObject /// Sends an array of fabricated User objects then completes. - (RACSignal *)fetchRandomUsers:(NSUInteger)numberOfUsers; @end

Slide 23

Slide 23 text

View Models • PONSOs or helper superclass • Convert Models to View Models • Expose data as observable properties • Accept events from owners as methods or blocks • Can’t manipulate Views

Slide 24

Slide 24 text

Userlist Ownership UserController UsersViewModel UserViewModel User UserViewModel UserViewModel UserViewModel UsersViewController UserCell UserCell UserCell UserCell smarter dumber

Slide 25

Slide 25 text

// UserViewModel.h @interface UserViewModel : NSObject @property (nonatomic, readonly) NSString *name; @property (nonatomic, readonly) UIImage *avatarImage; /// Initiate the avatarImage fetch. @property (nonatomic, getter=isActive) BOOL active; - (instancetype)initWithUser:(User *)user; @end

Slide 26

Slide 26 text

// UsersViewModel.h @interface UsersViewModel : NSObject /// Array of UserViewModel objects. /// NSArray @property (nonatomic, readonly) NSArray *userViewModels; @property (nonatomic, readonly, getter=isLoading) BOOL loading; @property (nonatomic, getter=isActive) BOOL active; /// Initiate a fetch. - (void)fetchRandomUsers; @end

Slide 27

Slide 27 text

Views • UIViews and UIViewControllers • Final conversion to UI • Bind to View Models and native properties • Pass events to View Models • Can’t see Models

Slide 28

Slide 28 text

Userlist Ownership UserController UsersViewModel UserViewModel User UserViewModel UserViewModel UserViewModel UsersViewController UserCell UserCell UserCell UserCell smarter dumber

Slide 29

Slide 29 text

// UserCell.h @interface UserCell : UITableViewCell @property (nonatomic) UserViewModel *viewModel; @end

Slide 30

Slide 30 text

// UserCell.m - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier: (NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self != nil) { RAC(self.imageView, image) = RACObserve(self, viewModel.avatarImage); RAC(self.textLabel, text) = RACObserve(self, viewModel.name); } return self; }

Slide 31

Slide 31 text

// UsersViewController.h @interface UsersViewController : UITableViewController @property (nonatomic, readonly) UsersViewModel *viewModel; - (instancetype)initWithViewModel:(UsersViewModel *)viewModel; @end

Slide 32

Slide 32 text

// UsersViewController.m - (void)viewDidLoad { [super viewDidLoad]; [self.tableView registerClass:[UserCell class] forCellReuseIdentifier:NSStringFromClass([UserCell class])]; @weakify(self); [[RACObserve(self.viewModel, userViewModels) ignore:nil] subscribeNext:^(id _) { @strongify(self); [self.tableView reloadData]; }]; }

Slide 33

Slide 33 text

// UsersViewController.m # pragma mark UITableViewDataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.viewModel.userViewModels count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UserCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([UserCell class]) forIndexPath:indexPath]; UserViewModel *viewModel = self.viewModel.userViewModels[indexPath.row]; cell.viewModel = viewModel; return cell; }

Slide 34

Slide 34 text

// UsersViewController.m # pragma mark UITableViewDelegate - (void)tableView:(UITableView *)tableView willDisplayCell: (UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { UserCell *userCell = (UserCell *)cell; userCell.viewModel.active = YES; } - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { UserCell *userCell = (UserCell *)cell; userCell.viewModel.active = NO; }

Slide 35

Slide 35 text

Ownership Data Flow VIEW VIEW MODEL CONTROLLER DATA SOURCE VIEW VIEW MODEL CONTROLLER DATA SOURCE RAW MODEL MODEL VIEW MODEL

Slide 36

Slide 36 text

// Raw Model @{ @“name” = @“Taylor Switch”, @“avatarURL” = @“http://example.com/taylor_switch.jpg” } // Model @interface User : NSObject @property (nonatomic, readonly) NSString *name; @property (nonatomic, readonly) NSURL *avatarImageURL; @end // View Model @interface UserViewModel : NSObject @property (nonatomic, readonly) NSString *name; @property (nonatomic, readonly) UIImage *avatarImage; @end // View @interface UserCell () @property (nonatomic, readonly) UILabel *textLabel; @property (nonatomic, readonly) UIImageView *imageView; @end

Slide 37

Slide 37 text

EXAMPLE 2 Image System @ Timehop

Slide 38

Slide 38 text

Image System Data Flow IMAGE PROGRESS VIEW IMAGE VIEW MODEL IMAGE CONTROLLER DATA SOURCE

Slide 39

Slide 39 text

// ImageController.h @interface ImageController : NSObject - (instancetype)initWithWebImageManager:(SDWebImageManager *)webImageManager imageManager: (PHImageManager *)imageManager cachingImageManager:(PHCachingImageManager *)cachingImageManager; /// Sends RACTuple(UIImage *downloadedImage, NSNumber *progress) /// at unspecified intervals, where progress is a double == 0..1, /// and downloadedImage in nil for 0..0.99 and /// the complete image for progress == 1. /// /// Sends RACTuple(nil, @1) if imageURL is nil. /// /// Sends complete when the final representation of the image has /// been returned. - (RACSignal *)imageAndProgressWithURL:(NSURL *)imageURL;

Slide 40

Slide 40 text

// ImageViewModel.h @interface ImageViewModel : ViewModel /// The latest image downloaded from imageURL. May be nil. @property (nonatomic, readonly) UIImage *image; /// The progress of the current download. @property (nonatomic, readonly) CGFloat progress; /// ErrorViewModel encapsulates any download error. @property (nonatomic, readonly) BOOL hasError; @property (nonatomic, readonly) ErrorViewModel *errorViewModel; /// Whether any download operation is in progress. @property (nonatomic, readonly, getter=isLoading) BOOL loading; /// The View layer should call this block when a user taps the image view. /// If an image exists and there is no error, it will call `openImageURLBlock`. /// If an error exists, it will try to fetch the image again. @property (nonatomic, readonly) void (^selectBlock)(void); /// Exists on ViewModel superclass. /// Setting active to YES triggers the image load unless the image /// has already been fetched. @property (nonatomic, getter=isActive) BOOL active; @property (nonatomic, readonly) NSURL *imageURL; - (instancetype)initWithImageURL:(NSURL *)imageURL openImageURLBlock:(void (^) (NSURL *))openImageURLBlock imageController:(ImageController *)imageController;

Slide 41

Slide 41 text

// ImageProgressView.h /// * Updates the image to the latest value of viewModel.image. /// * Shows progress before first image is loaded. /// * Shows error view on error. /// * Handles taps. @interface ImageProgressView : UIView @property (nonatomic) ImageViewModel *viewModel; @end /// ImageView.h /// * Updates the image to the latest value of viewModel.image. @interface ImageView : UIImageView @property (nonatomic) ImageViewModel *viewModel; @end

Slide 42

Slide 42 text

// SDWebImageManager+RACSignalSupport.m - (RACSignal *)rac_imageAndProgressWithURL:(NSURL *)url options: (SDWebImageOptions)options { return [RACSignal createSignal:^RACDisposable *(id subscriber) { id operation = [self downloadImageWithURL:url options:options progress:^(NSInteger receivedSize, NSInteger expectedSize) { if (expectedSize > 0 && receivedSize < expectedSize) { [subscriber sendNext:RACTuplePack(nil, @(receivedSize/ (CGFloat)expectedSize))]; } } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { if (error != nil) { [subscriber sendError:error]; } [subscriber sendNext:RACTuplePack(image, @1)]; if (finished) { [subscriber sendCompleted]; } }]; return [RACDisposable disposableWithBlock:^{ [operation cancel]; }]; }]; }

Slide 43

Slide 43 text

EXAMPLE 3 Issues & Content @ Timehop

Slide 44

Slide 44 text

IssueViewModel AlbumViewModel UserContentViewModel ContentBlockViewModel

Slide 45

Slide 45 text

• IssueViewModel • AlbumViewModel[] • UserContentViewModel[] • ContentBlockViewModel

Slide 46

Slide 46 text

• IssueViewModel • AlbumViewModel[] • UserContentViewModel[] • ContentMetadataViewModel • AlbumMetadataViewModel • PhotoAlbumViewModel • ImageViewModel[] • VideoViewModel • ImageViewModel • TextLinksViewModel • EventViewModel • LocationViewModel • LinkViewModel • ImageViewModel • InteractionsViewModel • SourceActionViewModel • ShareViewModel

Slide 47

Slide 47 text

How does a Timehop Issue get loaded? VIEW VIEW MODEL CONTROLLER DATA SOURCE NSDICTIONARY ISSUE VIEWMODELS[] ACTIVE @YES DATEKEY “20150309” URL “/ISSUES/20150309”

Slide 48

Slide 48 text

What happens when you scroll the table view? UserContentCell TextLinksSlimView … UserContentViewModel TextLinksViewModel … UserContentViewModel TextLinksViewModel … UserContentViewModel TextLinksViewModel …

Slide 49

Slide 49 text

What happens when you scroll the table view? - (void)tableView:(UITableView *)tableView didEndDisplayingCell: (UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { UserContentCell *userContentCell = (UserContentCell *)cell; userContentCell.viewModel.active = NO; } UserContentCell TextLinksSlimView … UserContentViewModel TextLinksViewModel … UserContentViewModel TextLinksViewModel …

Slide 50

Slide 50 text

What happens when you scroll the table view? - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UserContentCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([UserContentCell class]) forIndexPath:indexPath]; AlbumViewModel *albumViewModel = self.viewModel.viewModels[indexPath.section]; UserContentViewModel *userContentViewModel = albumViewModel.userContentViewModels[indexPath.row]; cell.viewModel = userContentViewModel; return cell; } UserContentCell TextLinksSlimView … UserContentViewModel TextLinksViewModel … UserContentViewModel TextLinksViewModel …

Slide 51

Slide 51 text

What happens when you scroll the table view? - (void)tableView:(UITableView *)tableView willDisplayCell: (UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { UserContentCell *userContentCell = (UserContentCell *)cell; userContentCell.viewModel.active = YES; } UserContentCell TextLinksSlimView … UserContentViewModel TextLinksViewModel … UserContentViewModel TextLinksViewModel …

Slide 52

Slide 52 text

What happens when you tap a location view?

Slide 53

Slide 53 text

IssueViewModel What happens when you tap a location view? IssueViewController UserContentCell LocationView AlbumViewModel UserContentViewModel LocationViewModel *tap* Location View Controller

Slide 54

Slide 54 text

IssueViewModel What happens when you tap a location view? IssueViewController UserContentCell LocationView AlbumViewModel UserContentViewModel LocationViewModel *tap* Location LocationViewModel LocationViewModel Location View Model Location View Controller

Slide 55

Slide 55 text

What happens when you tap a link view?

Slide 56

Slide 56 text

IssueViewModel What happens when you tap a link view? IssueViewController UserContentCell LinkSlimView AlbumViewModel UserContentViewModel LinkViewModel *tap* WebView Controller

Slide 57

Slide 57 text

IssueViewModel What happens when you tap a link view? IssueViewController UserContentCell LinkSlimView AlbumViewModel UserContentViewModel LinkViewModel *tap* Link WebViewModel WebViewModel WebView Model WebView Controller

Slide 58

Slide 58 text

// IssueViewController.m - (void)viewDidLoad { [self.viewModel.presentableViewModelSignal subscribeNext:^(ViewModel *viewModel) { if ([viewModel isKindOfClass:[LocationViewModel class]]) { LocationViewController *locationViewController = [[LocationViewController alloc] initWithViewModel:viewModel]; [self.navigationController pushViewController:locationViewController animated:YES]; } else if ([viewModel isKindOfClass:[InteractionsDetailViewModel class]]) { InteractionsDetailViewController *interactionsDetailViewController = [[InteractionsDetailViewController alloc] initWithViewModel:viewModel]; [self.navigationController pushViewController:interactionsDetailViewController animated:YES]; } else if ([viewModel isKindOfClass:[WebViewModel class]]) { WebViewController *webViewController = [[WebViewController alloc] initWithViewModel:viewModel]; [self.navigationController pushViewController:webViewController animated:YES]; } else if ([viewModel isKindOfClass:[EventDetailViewModel class]]) { EventDetailViewController *eventViewController = [[EventDetailViewController alloc] initWithViewModel:viewModel]; [self.navigationController pushViewController:eventViewController animated:YES]; } }]; }

Slide 59

Slide 59 text

Learn more • http://www.sprynthesis.com/2014/12/06/ reactivecocoa-mvvm-introduction/ • https://github.com/timehop/userlist • https://github.com/twocentstudios/longtail • https://github.com/edc1591/Bikes • https://github.com/jspahrsummers/ GroceryList

Slide 60

Slide 60 text

QUESTIONS?

Slide 61

Slide 61 text

INTERESTED IN REACTIVECOCOA & MVVM? TIMEHOP IS HIRING