ylliX - Online Advertising Network
Introduction to MVVM pattern in Objective-C

Introduction to MVVM pattern in Objective-C


Even though the iOS ecosystem is growing further every day from Objective-C, some companies still heavily rely on it. A week away for another wave of innovation from WWDC 2020, I thought it would be interesting to dive back into Objective-C starting with a MVVM pattern implementation.

As a quick reminder, MVVM pattern is an architectural design pattern decoupling logic in three main pieces: Model – View – ViewModel. If you’re looking for a similar content in Swift, I’ve covered this subject in the past in Swift but also with RxSwift.

Let’s dive in the code

Model

For this sample app, I’m building a Playlist app, listing song title, artist name and album cover.

// Song.h
@interface Song : NSObject

@property (nonatomic, strong) NSString * title;
@property (nonatomic, strong) NSString * artistName;
@property (nonatomic, strong) NSString * albumCover;

- (instancetype)initWithTitle:(NSString*)title artistName:(NSString*)artistName albumCover:(NSString*)albumCover;

- (nullable NSURL*)albumCoverUrl;

@end


// Song.m
@implementation Song

- (instancetype)initWithTitle:(NSString*)title artistName:(NSString*)artistName albumCover:(NSString*)albumCover
{
    self = [super init];
    if (self) {
        self.title = title;
        self.artistName = artistName;
        self.albumCover = albumCover;
    }
    return self;
}

- (nullable NSURL*)albumCoverUrl {
    return [NSURL URLWithString:self.albumCover];
}

From there, I want to keep a clear separation between each layer, so I use a protocol oriented programming approach to keep the code maintainable and testable.

One component would be to fetch data where another one would be to parse them.

Since we don’t have Result type in Objective-C, I want to decouple the success and error journey separately. To do so, I’m using two closures for callbacks. Even though it can make the syntax a bit less readable, I prefer it to a delegation pattern.


@protocol SongParserProtocol <NSObject>

- (void)parseSongs:(NSData *)data withSuccess:(void (^)(NSArray<Song *> *songs))successCompletion error:(void (^)(NSError *error))errorCompletion;

@end

@protocol SongFetcherProtocol <NSObject>

- (void)fetchSongsWithSuccess:(void (^)(NSArray<Song *> *songs))successCompletion error:(void (^)(NSError *error))errorCompletion;

@end

Here, the first protocol SongParserProtocol is responsible to deserialize raw data into a model. The second protocol SongFetcherProtocol fetches data from a source and chain it to the defined parser to get the final result.

The implementation would rely on a mocked JSON file since I don’t have any API yet.

// SongFetcher.h

@interface SongFetcher : NSObject<SongFetcherProtocol>

- (instancetype)initWithParser:(id<SongParserProtocol>)parser;

@end

// SongFetcher.m
@interface SongFetcher()

@property (nonatomic, strong) id<SongParserProtocol> parser;

@end

@implementation SongFetcher

- (instancetype)initWithParser:(id<SongParserProtocol>)parser 
{
    self = [super init];
    if (self) {
        self.parser = parser;
    }
    return self;
}

/// Mocked data based on JSON file
- (void)fetchSongsWithSuccess:(void (^)(NSArray<Song *> *))successCompletion error:(void (^)(NSError *))errorCompletion {
    
    __weak SongFetcher * weakSelf = self;
    void (^dataResponse)(NSData *) = ^(NSData *data){ 
        [weakSelf.parser parseSongs:data withSuccess:successCompletion error:errorCompletion];
    };
    
    // TODO: improve error handling at each steps
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
        FileReader * reader = [[FileReader alloc] init];
        [reader readJson:@"songs" withSuccess:dataResponse error:errorCompletion];
    });
}

The services are ready to consume, the work dispatch in background queue, we’re ready to build our ViewModel.

ViewModel

Since the goal is to display a representation of Song, I want to create a specific model to represent its display in a cell. It avoid exposing our business model to the UI component itself. It makes it more explicit the display of each property.

// SongDisplay.h

@class Song;
@interface SongDisplay : NSObject

@property (nonatomic, readonly, nullable) NSString *title;
@property (nonatomic, readonly, nullable) NSString *subtitle;
@property (nonatomic, readonly, nullable) UIImage *coverImage;

- (instancetype)initWithSong:(nonnull Song*)song;

@end

Moving on to the ViewModel, I want to expose a way to get this new model. It also has to include different methods to feed the UITableViewDataSource later on.

// ViewModel.h
@interface ViewModel : NSObject

- (void)getSongsWithSuccess:(void (^)(NSArray<SongDisplay*> *songs))successCompletion error:(void (^)(NSError *error))errorCompletion;

- (NSUInteger)numberOfItems;
- (NSUInteger)numberOfSections;
- (nullable SongDisplay *)itemAtIndexPath:(NSIndexPath *)indexPath;

@end

Finally, we can implement those methods and fill the gap. What’s important is to reuse each protocol I previously prepared into my constructor. I could inject them from a custom constructor for Unit Testing but I’ll leave it simple for time being.

// ViewModel.m
@interface ViewModel()

@property (nonatomic, strong) id<SongFetcherProtocol> fetcher;
@property (nonatomic, strong) NSArray<SongDisplay *> *items;

@end

@implementation ViewModel

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.items = @[];
        self.fetcher = [[SongFetcher alloc] initWithParser:[[SongParser alloc] init]];
    }
    return self;
}

- (void)getSongsWithSuccess:(void (^)(NSArray<SongDisplay *> * _Nonnull))successCompletion error:(void (^)(NSError * _Nonnull))errorCompletion {
    
    __weak ViewModel *weakSelf = self;
    [self.fetcher fetchSongsWithSuccess:^(NSArray<Song *> *songs) {
        
        NSMutableArray * items = [[NSMutableArray alloc] init];
        for (Song *song in songs) {
            [items addObject:[[SongDisplay alloc] initWithSong:song]]; 
        }
        [weakSelf setItems:items];
        successCompletion(items);
    } error:errorCompletion];
}

- (NSUInteger)numberOfItems {
    return self.items.count;
}

- (NSUInteger)numberOfSections {
    return 1;
}

- (SongDisplay *)itemAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.row >= self.items.count) {
        return nil;
    }
    return self.items[indexPath.row];
}
@end

Note that I use id<SongFetcherProtocol> to avoid exposing a specific type of object in case a new implementation is needed in the future.

Finally, we can use our fetcher and transform whatever result from Song into SongDisplay and always finish with the completion. The fetcher does the heavy lifting of getting the data from the right place, formatting back into the right model.

We’re ready to finalize it with the View itself.

View

To represent the view, I use a UIViewController and since it’s a fairly small app, I’m going to implement the necessary UITableViewDataSource there as well.

// ViewController.h
@interface ViewController : UIViewController<UITableViewDataSource>

@property (nonatomic, strong) IBOutlet UITableView * tableView; 

@end

Finally, we can wrap up the implementation by connected the ViewController to its ViewModel.

@interface ViewController ()

@property (nonatomic, strong) ViewModel * viewModel;

@end

@implementation ViewController

- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    if (self) {
        self.viewModel = [[ViewModel alloc] init];
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.tableView setDataSource:self];
    [self getData];
}

- (void)getData {
    __weak ViewController *weakSelf = self;
    [self.viewModel getSongsWithSuccess:^(NSArray<SongDisplay *> * _Nonnull songs) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [weakSelf.tableView reloadData];
        });
    } error:^(NSError * _Nonnull error) {
        // TODO handle error
    }];
}

//MARK: - UITableViewDataSource 
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return self.viewModel.numberOfSections;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.viewModel.numberOfItems;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    SongTableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"SongTableViewCell"];
    
    if (!cell) {
        assert(false);
    }
    
    [cell setDisplay:[self.viewModel itemAtIndexPath:indexPath]];
    return cell;
}

@end

In this implementation, I trigger the ViewModel to get the data and make sure I reload the data accordingly on the main thread. We could push further and use a diffable algorithm to update only what’s necessary rather than reloading it all.

The cells are built based on SongDisplay model so that there are not exposed to any business logic, the UI stays separated to the rest all along.

The rest of the UI is directly implemented through Storyboard to make the design faster.


At the end, we have a complete MVVM pattern implementation with a clear separation of layer: the code is easy to maintain and test.

Like every solution, there is no silver bullet and always some tradeoff. As mentioned above, using closures rather than delegation is my choice but you might want to choose a more readable approach if you feel this one is limited.

I haven’t covered some area on purpose, like loading image cover, implementing network api or error handling since it’s a bit further than the goal of this article.

You can find a bit more details in the project itself on Github, named ObjectiveCSample.


Where to go from here



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *