MVVM Architecture at Timehop

MVVM Architecture at Timehop

In this talk, I discuss how we've used MVVM in the Timehop iOS app to create a robust and modular app architecture.

I start by walking through the basics of MVVM in a very simple example app called Userlist.

Next, I detail how the image loading subsystem works in the Timehop app.

Finally, I answer some questions about how Timehop works behind the scenes.

92dd0805ebf2aa63b19dccd4be2e5de8?s=128

Chris Trott

March 10, 2015
Tweet

Transcript

  1. CHRIS TROTT @TWOCENTSTUDIOS MVVM ARCHITECTURE AT TIMEHOP

  2. None
  3. None
  4. 100K 1M 10M

  5. LET’S TALK ABOUT ARCHITECTURE

  6. Architecture Examples • Apple? • Open source? • Other platforms?

    • Large scale? Small scale?
  7. LET’S TALK ABOUT MVVM

  8. MVVM Model View View Model

  9. VVMM View - View Model - Model

  10. Why MVVM? • Isolation • Reusability • Refactorability • Testability

    • Predictability
  11. VIEWS SHOULD BE DUMB

  12. VIEWS SHOULD BE DUMB SMART (ABOUT VIEW STUFF)

  13. PUT YOUR SMARTS IN THE VIEW MODEL

  14. HOW DO WE MVVM @ TIMEHOP?

  15. REACTIVECOCOA + MVVM = ❤️

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

  17. Ownership Data Flow VIEW VIEW MODEL CONTROLLER DATA SOURCE VIEW

    VIEW MODEL CONTROLLER DATA SOURCE RAW MODEL MODEL VIEW MODEL
  18. Userlist Ownership UserController UsersViewModel UserViewModel User UserViewModel UserViewModel UserViewModel UsersViewController

    UserCell UserCell UserCell UserCell smarter dumber
  19. Controllers • Not UIViewControllers! • PONSOs (plain old NSObjects) •

    Convert Raw Models to Models • Can’t manipulate View Models
  20. Userlist Ownership UserController UsersViewModel UserViewModel User UserViewModel UserViewModel UserViewModel UsersViewController

    UserCell UserCell UserCell UserCell smarter dumber
  21. // User.h @interface User : NSObject @property (nonatomic, readonly) NSString

    *name; @property (nonatomic, readonly) NSURL *avatarURL; - (instancetype)initWithName:(NSString *)name avatarURL: (NSURL *)avatarURL; @end
  22. // UserController.h @interface UserController : NSObject /// Sends an array

    of fabricated User objects then completes. - (RACSignal *)fetchRandomUsers:(NSUInteger)numberOfUsers; @end
  23. 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
  24. Userlist Ownership UserController UsersViewModel UserViewModel User UserViewModel UserViewModel UserViewModel UsersViewController

    UserCell UserCell UserCell UserCell smarter dumber
  25. // 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
  26. // UsersViewModel.h @interface UsersViewModel : NSObject /// Array of UserViewModel

    objects. /// NSArray<UserViewModel> @property (nonatomic, readonly) NSArray *userViewModels; @property (nonatomic, readonly, getter=isLoading) BOOL loading; @property (nonatomic, getter=isActive) BOOL active; /// Initiate a fetch. - (void)fetchRandomUsers; @end
  27. Views • UIViews and UIViewControllers • Final conversion to UI

    • Bind to View Models and native properties • Pass events to View Models • Can’t see Models
  28. Userlist Ownership UserController UsersViewModel UserViewModel User UserViewModel UserViewModel UserViewModel UsersViewController

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

    @end
  30. // 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; }
  31. // UsersViewController.h @interface UsersViewController : UITableViewController @property (nonatomic, readonly) UsersViewModel

    *viewModel; - (instancetype)initWithViewModel:(UsersViewModel *)viewModel; @end
  32. // 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]; }]; }
  33. // 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; }
  34. // 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; }
  35. Ownership Data Flow VIEW VIEW MODEL CONTROLLER DATA SOURCE VIEW

    VIEW MODEL CONTROLLER DATA SOURCE RAW MODEL MODEL VIEW MODEL
  36. // 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
  37. EXAMPLE 2 Image System @ Timehop

  38. Image System Data Flow IMAGE PROGRESS VIEW IMAGE VIEW MODEL

    IMAGE CONTROLLER DATA SOURCE
  39. // 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;
  40. // 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;
  41. // 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
  42. // SDWebImageManager+RACSignalSupport.m - (RACSignal *)rac_imageAndProgressWithURL:(NSURL *)url options: (SDWebImageOptions)options { return

    [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { id<SDWebImageOperation> 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]; }]; }]; }
  43. EXAMPLE 3 Issues & Content @ Timehop

  44. IssueViewModel AlbumViewModel UserContentViewModel ContentBlockViewModel

  45. • IssueViewModel • AlbumViewModel[] • UserContentViewModel[] • ContentBlockViewModel

  46. • IssueViewModel • AlbumViewModel[] • UserContentViewModel[] • ContentMetadataViewModel • AlbumMetadataViewModel

    • PhotoAlbumViewModel • ImageViewModel[] • VideoViewModel • ImageViewModel • TextLinksViewModel • EventViewModel • LocationViewModel • LinkViewModel • ImageViewModel • InteractionsViewModel • SourceActionViewModel • ShareViewModel
  47. How does a Timehop Issue get loaded? VIEW VIEW MODEL

    CONTROLLER DATA SOURCE NSDICTIONARY ISSUE VIEWMODELS[] ACTIVE @YES DATEKEY “20150309” URL “/ISSUES/20150309”
  48. What happens when you scroll the table view? UserContentCell TextLinksSlimView

    … UserContentViewModel TextLinksViewModel … UserContentViewModel TextLinksViewModel … UserContentViewModel TextLinksViewModel …
  49. 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 …
  50. 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 …
  51. 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 …
  52. What happens when you tap a location view?

  53. IssueViewModel What happens when you tap a location view? IssueViewController

    UserContentCell LocationView AlbumViewModel UserContentViewModel LocationViewModel *tap* Location View Controller
  54. 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
  55. What happens when you tap a link view?

  56. IssueViewModel What happens when you tap a link view? IssueViewController

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

    UserContentCell LinkSlimView AlbumViewModel UserContentViewModel LinkViewModel *tap* Link WebViewModel WebViewModel WebView Model WebView Controller
  58. // 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]; } }]; }
  59. 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
  60. QUESTIONS?

  61. INTERESTED IN REACTIVECOCOA & MVVM? TIMEHOP IS HIRING