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

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.

Chris Trott

March 10, 2015
Tweet

More Decks by Chris Trott

Other Decks in Programming

Transcript

  1. CHRIS TROTT
    @TWOCENTSTUDIOS
    MVVM ARCHITECTURE
    AT TIMEHOP

    View Slide

  2. View Slide

  3. View Slide

  4. 100K 1M 10M

    View Slide

  5. LET’S TALK ABOUT
    ARCHITECTURE

    View Slide

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

    View Slide

  7. LET’S TALK ABOUT
    MVVM

    View Slide

  8. MVVM
    Model View View Model

    View Slide

  9. VVMM
    View - View Model - Model

    View Slide

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

    View Slide

  11. VIEWS SHOULD BE
    DUMB

    View Slide

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

    View Slide

  13. PUT YOUR SMARTS IN THE
    VIEW MODEL

    View Slide

  14. HOW DO WE
    MVVM
    @ TIMEHOP?

    View Slide

  15. REACTIVECOCOA + MVVM = ❤️

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  26. // 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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  37. EXAMPLE 2
    Image System
    @ Timehop

    View Slide

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

    View Slide

  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;

    View Slide

  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;

    View Slide

  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

    View Slide

  42. // 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];
    }];
    }];
    }

    View Slide

  43. EXAMPLE 3
    Issues & Content
    @ Timehop

    View Slide

  44. IssueViewModel
    AlbumViewModel
    UserContentViewModel
    ContentBlockViewModel

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  48. What happens when you scroll the table view?
    UserContentCell
    TextLinksSlimView

    UserContentViewModel
    TextLinksViewModel

    UserContentViewModel
    TextLinksViewModel

    UserContentViewModel
    TextLinksViewModel

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  52. What happens when you tap a location view?

    View Slide

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

    View Slide

  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

    View Slide

  55. What happens when you tap a link view?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  60. QUESTIONS?

    View Slide

  61. INTERESTED IN
    REACTIVECOCOA & MVVM?
    TIMEHOP IS HIRING

    View Slide