Some verification involving block methods and OCMockito - ios

I'm using OCMockito and I want to test a method in my ViewController that uses a NetworkFetcher object and a block:
- (void)reloadTableViewContents
{
[self.networkFetcher fetchInfo:^(NSArray *result, BOOL success) {
if (success) {
self.model = result;
[self.tableView reloadData];
}
}];
}
In particular, I'd want to mock fetchInfo: so that it returns a dummy result array without hitting the network, and verify that the reloadData method was invoked on the UITableView and the model is what it should be.
As this code is asynchronous, I assume that I should somehow capture the block and invoke it manually from my tests.
How can I accomplish this?

This is quite easy:
- (void) testDataWasReloadAfterInfoFetched
{
NetworkFetcher mockedFetcher = mock([NetowrkFetcher class]);
sut.networkFetcher = mockedFetcher;
UITableView mockedTable = mock([UITableView class]);
sut.tableView = mockedTable;
[sut reloadTableViewContents];
MKTArgumentCaptor captor = [MKTArgumentCaptor new];
[verify(mockedFetcher) fetchInfo:[captor capture]];
void (^callback)(NSArray*, BOOL success) = [captor value];
NSArray* result = [NSArray new];
callback(result, YES);
assertThat(sut.model, equalTo(result));
[verify(mockedTable) reloadData];
}
I put everything in one test method but moving creation of mockedFetcher and mockedTable to setUp will save you lines of similar code in other tests.

(Edit: See Eugen's answer, and my comment. His use of OCMockito's MKTArgumentCaptor not only eliminates the need for the FakeNetworkFetcher, but results in a better test flow that reflects the actual flow. See my Edit note at the end.)
Your real code is asynchronous only because of the real networkFetcher. Replace it with a fake. In this case, I'd use a hand-rolled fake instead of OCMockito:
#interface FakeNetworkFetcher : NSObject
#property (nonatomic, strong) NSArray *fakeResult;
#property (nonatomic) BOOL fakeSuccess;
#end
#implementation FakeNetworkFetcher
- (void)fetchInfo:(void (^)(NSArray *result, BOOL success))block {
if (block)
block(self.fakeResult, self.fakeSuccess);
}
#end
With this, you can create helper functions for your tests. I'm assuming your system under test is in the test fixture as an ivar named sut:
- (void)setUpFakeNetworkFetcherToSucceedWithResult:(NSArray *)fakeResult {
sut.networkFetcher = [[FakeNetworkFetcher alloc] init];
sut.networkFetcher.fakeSuccess = YES;
sut.networkFetcher.fakeResult = fakeResult;
}
- (void)setUpFakeNetworkFetcherToFail
sut.networkFetcher = [[FakeNetworkFetcher alloc] init];
sut.networkFetcher.fakeSuccess = NO;
}
Now your success path test needs to ensure that your table view is reloaded with the updated model. Here's a first, naive attempt:
- (void)testReloadTableViewContents_withSuccess_ShouldReloadTableWithResult {
// given
[self setUpFakeNetworkFetcherToSucceedWithResult:#[#"RESULT"]];
sut.tableView = mock([UITablewView class]);
// when
[sut reloadTableViewContents];
// then
assertThat(sut.model, is(#[#"RESULT"]));
[verify(sut.tableView) reloadData];
}
Unfortunately, this doesn't guarantee that the model is updated before the reloadData message. But you'll want a different test anyway to ensure that the fetched result is represented in the table cells. This can be done by keeping the real UITableView and allowing the run loop to advance with this helper method:
- (void)runForShortTime {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
}
Finally, here's a test that's starting to look good to me:
- (void)testReloadTableViewContents_withSuccess_ShouldShowResultInCell {
// given
[self setUpFakeNetworkFetcherToSucceedWithResult:#[#"RESULT"]];
// when
[sut reloadTableViewContents];
// then
[self runForShortTime];
NSIndexPath *firstRow = [NSIndexPath indexPathForRow:0 inSection:0];
UITableViewCell *firstCell = [sut.tableView cellForRowAtIndexPath:firstRow];
assertThat(firstCell.textLabel.text, is(#"RESULT"));
}
But your real test will depend on how your cells actually represent the fetched results. And that shows that this test is fragile: if you decide to change the representation, then you have to go fix up a bunch of tests. So let's extract a helper assertion method:
- (void)assertThatCellForRow:(NSInteger)row showsText:(NSString *)text {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
UITableViewCell *cell = [sut.tableView cellForRowAtIndexPath:indexPath];
assertThat(cell.textLabel.text, is(equalTo(text)));
}
With that, here's a test that uses our various helper methods to be expressive and pretty robust:
- (void)testReloadTableViewContents_withSuccess_ShouldShowResultsInCells {
[self setUpFakeNetworkFetcherToSucceedWithResult:#[#"FOO", #"BAR"]];
[sut reloadTableViewContents];
[self runForShortTime];
[self assertThatCellForRow:0 showsText:#"FOO"];
[self assertThatCellForRow:1 showsText:#"BAR"];
}
Note that I didn't have this end in my head when I started. I even made some false steps along the way which I haven't shown. But this shows how I try to iterate my way to test designs.
Edit: I see now that with my FakeNetworkFetcher, the block get executed in the middle of reloadTableViewContents — which doesn't reflect what will really happen when it's asynchronous. By shifting to capturing the block then invoking it according to Eugen's answer, the block will be executed after reloadTableViewContents completes. This is far better.
- (void)testReloadTableViewContents_withSuccess_ShouldShowResultsInCells {
[sut reloadTableViewContents];
[self simulateNetworkFetcherSucceedingWithResult:#[#"FOO", #"BAR"]];
[self runForShortTime];
[self assertThatCellForRow:0 showsText:#"FOO"];
[self assertThatCellForRow:1 showsText:#"BAR"];
}

Related

Is it more memory efficient to define and initialize an NSArray when a UIView first loads or only when a condition is met?

Is it better to initialize an NSArray in viewDidLoad when the program first starts up, or define the NSArray only if a condition is met?
Basically, I initialized an NSArray in an IF condition of one of my methods. This method may be called multiple times, and want to know if it's better on memory if the NSArray is created and destroyed in the method, or if it's better to define it once in viewDidLoad and reference it in the method?
If I'm not clear, please let me know.
Thanks
Create any data collection only when you need it, this way you make sure that the program/screen is initially launched quickly. This is called as lazy loading, this approach should be followed whenever possible.
If you are using NSMutableArray and managing this in run time, you can do something like this for lazy loading, and clearing it from memory when not needed. Add helper methods to add and remove objects from an array, array is created automatically when needed and removed from memory when it is empty.
- (void)addObject:(NSObject *)value
{
if (value == nil) return;
if (_collection == nil) {
_collection = [[NSMutableArray alloc] init];
}
[_collection addObject:value];
}
- (void)removeObject:(NSObject *)value
{
if (value == nil) return;
[_collection removeObject:value];
if ([_collection count] == 0) {
[_collection release], _collection = nil;
}
}

How can I unit test a method that depends on a volatile variable in Objective-C?

Here's a simplified version of my class:
#interface RTMovieBuilder : NSObject
#property (atomic, getter = isCancelled) volatile BOOL cancelled;
#property (nonatomic, weak) id<BuilderDelegate>delegate;
- (void)moviesFromJSON:(id)JSON;
- (Movie *)movieFromDictionary:(NSDictionary *)dict;
- (void)cancel;
#end
#implementation RTMovieBuilder
- (void)moviesFromJSON:(id)JSON
{
// Check for errors -> If good, then do...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[self syncrouslyCreateMoviesFromJSON:JSON];
});
}
- (void)syncrouslyCreateMoviesFromJSON:(id)JSON
{
NSMutableArray *movies = [NSMutableArray array];
for (NSDictionary *dict in JSON)
{
if ([self isCancelled])
return;
else
[movies addObject:[self movieFromDictionary:dict]];
}
[self notifyDelegateCreatedObjects:movies];
}
- (Movie *)movieFromDictionary:(NSDictionary *)dict
{
Movie *movie = [[Movie alloc] init];
// Set movie properties based on dictionary...
return movie;
}
- (void)cancel
{
[self setCancelled:YES];
}
// ... Other methods omitted for brevity's sake
#end
The property cancelled is atomic and volatile because it may be accessed by other threads (i.e. the main thread may call cancel method to stop the operation). (I believe these are needed, if not, please note why it's not in your answer.)
I am trying to write unit tests to make sure this will work before writing the view controller class.
How can I write a unit test that will simulate a call to cancel while RTMovieBuilder is in the middle of creating movies?
Edit
Here's a unit test I have already written which tests to make sure that notifyDelegateCreatedObjects: isn't called if cancel is called first.
- (void)testIfCancelledDoesntNotifyDelegateOfSuccess
{
// given
RTMovieBuilder *builder = [[RTMovieBuilder alloc] init];
builder.delegate = mockProtocol(#protocol(BuilderDelegate));
// when
[builder cancel];
[builder notifyDelegateCreatedObjects:#[]];
// then
[verifyCount(builder.delegate, never()) builder:builder createdObjects:anything()];
}
I'm using OCHamcrest and OCMockito. This test passes.
I would avoid trying to simulate thread timing in unit tests and focus more on figuring out what all the possible end states could be regardless of where the timing falls, and write tests for code under those conditions. This avoids endless complexity in your tests, as bbum points out as well.
In your case it seems the condition you need to be testing for is if the call to notifyDelegateCreatedObjects happens after the action is canceled, because the cancel came too late. So instead just unit test the handling of that scenario downstream in your notifyDelegateCreatedObjects method, or whatever class is being notified of that aborted event because of the thread timing.
I know this is not a specific answer to your question but I think its a better approach to achieve the same unit testing goal.
There is no reason to use volatile if your property is atomic and you always go through the setter/getter.
As well, this is a bit of re-inventing the wheel, as noted in the comments.
In general trying to unit test cancellation with any hope of full coverage is very hard because you can't really effectively test all possible timing interactions.

NSMutableArray thread safety

In my app I'm accessing and changing a mutable array from multiple threads. At the beginning it was crashing when I was trying to access an object with objectAtIndex, because index was out of bounds (object at that index has already been removed from array in another thread). I searched on the internet how to solve this problem, and I decided to try this solution .I made a class with NSMutableArray property, see the following code:
#interface SynchronizedArray()
#property (retain, atomic) NSMutableArray *array;
#end
#implementation SynchronizedArray
- (id)init
{
self = [super init];
if (self)
{
_array = [[NSMutableArray alloc] init];
}
return self;
}
-(id)objectAtIndex:(NSUInteger)index
{
#synchronized(_array)
{
return [_array objectAtIndex:index];
}
}
-(void)removeObject:(id)object
{
#synchronized(_array)
{
[_array removeObject:object];
}
}
-(void)removeObjectAtIndex:(NSUInteger)index
{
#synchronized(_array)
{
[_array removeObjectAtIndex:index];
}
}
-(void)addObject:(id)object
{
#synchronized(_array)
{
[_array addObject:object];
}
}
- (NSUInteger)count
{
#synchronized(_array)
{
return [_array count];
}
}
-(void)removeAllObjects
{
#synchronized(_array)
{
[_array removeAllObjects];
}
}
-(id)copy
{
#synchronized(_array)
{
return [_array copy];
}
}
and I use this class instead of old mutable array, but the app is still crashing at this line: return [_array objectAtIndex:index]; I tried also this approach with NSLock, but without a luck. What I'm doing wrong and how to fix this?
I believe this solution is poor. Consider this:
thread #1 calls count and is told there are 4 objects in the array.
array is unsynchronized.
thread #2 calls removeObjectAtIndex:2 on the array.
array is unsynchronized.
thread #1 calls objectAtIndex:3 and the error occurs.
Instead you need a locking mechanism at a higher level where the lock is around the array at both steps 1 and 5 and thread #2 cannot remove an object in between these steps.
You need to protect (with #synchronized) basically all usage of the array. Currently you only prevent multiple threads from concurrently getting objects out of the array. But you have no protection for your described scenario of concurrent modification and mutation.
Ask yourself why you're modifying the array on multiple threads - should you do it that way or just use a single thread? It may be easier to use a different array implementation or to use a wrapper class that always switches to the main thread to make the requested modification.

Can anybody explain MVC in terms of UItableview when getting data for the table from the internet?

Can anybody explain to me how MVC works when it comes to UITableView especially when getting data from the internet.
I would exactly like to know what is the model, view and controller when it comes to a UItableview
I have written the following ViewController code which sources data from the internet and displays it on a table using AFNetworking framework.
Could you please tell me how to change this and separate it into model, view and controller.
I have also written a refresh class, which i am guessing is a part of the model. Could you tell me how exactly do i make changes and make it a part of the model.
EDIT : The below answers help me understand the concept theoritically, Could someone please help me in changing the code accordingly( By writing a new class on how to call the array to this class and populate the table because i am using a json parser). I would like to implent it. And not just understand it theoritically.
#import "ViewController.h"
#import "AFNetworking.h"
#implementation ViewController
#synthesize tableView = _tableView, activityIndicatorView = _activityIndicatorView, movies = _movies;
- (void)viewDidLoad {
[super viewDidLoad];
// Setting Up Table View
self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0.0, 0.0, self.view.bounds.size.width, self.view.bounds.size.height) style:UITableViewStylePlain];
self.tableView.dataSource = self;
self.tableView.delegate = self;
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.tableView.hidden = YES;
[self.view addSubview:self.tableView];
// Setting Up Activity Indicator View
self.activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.activityIndicatorView.hidesWhenStopped = YES;
self.activityIndicatorView.center = self.view.center;
[self.view addSubview:self.activityIndicatorView];
[self.activityIndicatorView startAnimating];
// Initializing Data Source
self.movies = [[NSArray alloc] init];
NSURL *url = [[NSURL alloc] initWithString:#"http://itunes.apple.com/search?term=rocky&country=us&entity=movie"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[refreshControl addTarget:self action:#selector(refresh:) forControlEvents:UIControlEventValueChanged];
[self.tableView addSubview:refreshControl];
[refreshControl endRefreshing];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
self.movies = [JSON objectForKey:#"results"];
[self.activityIndicatorView stopAnimating];
[self.tableView setHidden:NO];
[self.tableView reloadData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
}];
[operation start];
}
- (void)refresh:(UIRefreshControl *)sender
{
NSURL *url = [[NSURL alloc] initWithString:#"http://itunes.apple.com/search?term=rambo&country=us&entity=movie"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
self.movies = [JSON objectForKey:#"results"];
[self.activityIndicatorView stopAnimating];
[self.tableView setHidden:NO];
[self.tableView reloadData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
}];
[operation start];
[sender endRefreshing];
}
- (void)viewDidUnload {
[super viewDidUnload];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return YES;
}
// Table View Data Source Methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (self.movies && self.movies.count) {
return self.movies.count;
} else {
return 0;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellID = #"Cell Identifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellID];
}
NSDictionary *movie = [self.movies objectAtIndex:indexPath.row];
cell.textLabel.text = [movie objectForKey:#"trackName"];
cell.detailTextLabel.text = [movie objectForKey:#"artistName"];
NSURL *url = [[NSURL alloc] initWithString:[movie objectForKey:#"artworkUrl100"]];
[cell.imageView setImageWithURL:url placeholderImage:[UIImage imageNamed:#"placeholder"]];
return cell;
}
#end
It's a pretty big question you are asking. But let me answer by making it as simple as possible.
Model - your data source; ultimately it's your web service data
Controller should be the thing that owns the table view and mediates setting properties on your view and reacting to events in the view and making changes , as needed, to the model
View(s) -- a combination of your table view and table view cells
There are a lot of approaches to coordinating between your web data and your table view but one I might suggest would be to refactor your web service calls into a separate store class - say iTunesStore - have that class be responsible for making the calls to the service and setting an internal array with the results, it should also be able to return a row count as well as a specific item for a given row index.
You then have this class respond to calls for the required table view delegate methods. Other things to consider, make this other class a singleton, have it conform to UITableviewDatasource protocol itself and assign it as the table views' data source.
Like I said, a big question with a lot of options for you, but I've given you some things to consider in terms of where to go next.
UPDATE
I'm adding some code examples to help clarify. At the outset, I want to make clear that I am not going to provide the total solution because doing so would require me to assume too much in terms of the necessary actual solution -- and because there are a few different ways to work with AFNetworking, web services, etc....and I don't want to get side tracked going down that rabbit hole. (Such as caching data on the client, background tasks & GCD, etc...) Just showing you how to wire up the basics -- but you will definitely want to learn how to use AFNetworking on a background task, look into Core Data or NSCoding for caching, and a few other topics to do this sort of thing correctly.
Suffice it to say that in a proper solution:
- You don't want to be calling your web service synchronously
- You also don't want to be re-requesting the same data every time - ie don't re-download the same record from the service unless its changed
- I am not showing how to do those things here because its way beyond the scope; look a the book recommendation below as well as this link to get an idea about these topics Ray Wenderlich - sync Core Data with a web service
For your data services code, I would create a 'store' class. (do yourself a favor and get the Big Nerd Ranch iOS book if you don't already have it.
iOS Programming 4th Edition
Take the following the code with a grain of salt - for reasons I can't go into I am not able to do this from my Mac (on a Win machine) and I also am not able to copy or even email myself the code ... so I am doing all in the StackOverflow editor...
My iTunesStore contract (header file) would look something like:
// iTunesStore.h
#import <Foundation/Foundation.h>
#interface iTunesStore : NSObject
- (NSUInteger)recordCount;
- (NSDictionary*)recordAtIndex:(NSUInteger)index; // could be a more specialized record class
+ (instancetype)sharedStore; // singleton
#end
...and the implementation would look something like:
// iTunesStore.m
#import "iTunesStore.h"
// class extension
#interface iTunesStore()
#property (nonatomic, strong) NSArray* records;
#end
#implementation iTunesStore
-(id)init
{
self = [super init];
if(self) {
// DO NOT DO IT THIS WAY IN PRODUCTION
// ONLY FOR DIDACTIC PURPOSES - Read my other comments above
[self loadRecords];
}
return self;
}
- (NSUInteger)recordCount
{
return [self.records count];
}
- (NSDictionary*)recordAtIndex:(NSUInteger)index
{
NSDictionary* record = self.records[index];
}
-(void)loadRecords
{
// simulate loading records from service synchronously (ouch!)
// in production this should use GCD or NSOperationQue to
// load records asynchrononusly
NSInteger recordCount = 10;
NSMutableArray* tempRecords = [NSMutableArray arrayWithCapacity:recordCount];
// load some dummy records
for(NSInteger index = 0; index < recordCount; index++) {
NSDictionary* record = #{#"id": #(index), #"title":[NSString stringWithFormat:#"record %d", index]};
[tempRecords addObject:record];
}
self.records = [tempRecords copy];
}
// create singleton instance
+ (instancetype)sharedStore
{
static dispatch_once_t onceToken;
static id _instance;
dispatch_once(&onceToken, ^{
_instance = [[[self class] alloc] init];
});
return _instance;
}
#end
I now have a 'store' object singleton I can use to get records, return a given record and also tell me a record count. Now I can move a lot of the logic doing this from the viewcontroller.
Now I don't need to do this in your VC viewDidLoad method. Ideally, you would have an async method in your store object to get records and a block to call you back once records are loaded. Inside the block you reload records. The signature for something like that 'might' look like:
[[iTunesStore sharedStore] loadRecordsWithCompletion:^(NSError* error){
... if no error assume load records succeeded
... ensure we are on the correct thread
[self.tableView reloadData]; // will cause table to reload cells
}];
Your view controller data source methods now look like:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection(NSInteger)section {
[[iTunesStore sharedStore] recordCount];
}
Inside cellForRowAtIndexPath - I also call my store object to get the correct record
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// ... get cell
// ... get record
NSDictionary* record = [[iTunesStore sharedStore] recordAtIndex:indexPath.row];
// ... configure cell]
return cell;
}
That's the gist of it. Other things to do, as noted above would be:
Have ITunesStore implement UITableViewDataSource and then just directly handle the tableview datasource methods - if you do this you don't want to make iTunesStore a singleton. And you would set an instance of iTunesStore as the tableview's delegate, rather than the viewcontroller. There are pros and cons to such an approach.
I haven't shown any real async behavior or caching which this app is crying out for
This does show how to pull off some of your model responsibilities and separate some of the tableview data source concerns.
Hopefully this will help to give you some ideas about different directions you might explore.
Happy coding!
In terms of UITableViewController, typically all the roles Model, View and Controller (MVC) is played by your UITableViewController itself. That is the case with your code as well.
As Model - It supplies data to your table view.
As Controller - It controls the look and feel of the table like number of rows, sections, height and width of them etc., supplies data from model to table view
As View - Its view property holds the UITableView
Now, to adopt a different approach you could have Model separated out from your controller class. For that have a subclass from NSObject and have it set its state which could be used by Controller.
Hope this makes sense to you.

A ViewModel pattern for iOS apps with ReactiveCocoa

I'm working on integrating RAC into my project with the goal of creating a ViewModel layer that will allow easy caching/prefetching from the network (plus all of the other benefits of MVVM). I'm not especially familiar with MVVM or FRP yet, and I'm trying to develop a nice, reusable pattern for iOS development. I have a couple of questions about this.
First, this is sort of how I've added a ViewModel to one of my views, just to try it out. (I want this here to reference later).
In ViewController viewDidLoad:
#weakify(self)
//Setup signals
RAC(self.navigationItem.title) = self.viewModel.nameSignal;
RAC(self.specialtyLabel.text) = self.viewModel.specialtySignal;
RAC(self.bioButton.hidden) = self.viewModel.hiddenBioSignal;
RAC(self.bioTextView.text) = self.viewModel.bioSignal;
RAC(self.profileImageView.hidden) = self.viewModel.hiddenProfileImageSignal;
[self.profileImageView rac_liftSelector:#selector(setImageWithContentsOfURL:placeholderImage:) withObjectsFromArray:#[self.viewModel.profileImageSignal, [RACTupleNil tupleNil]]];
[self.viewModel.hasOfficesSignal subscribeNext:^(NSArray *offices) {
self.callActionSheet = [[UIActionSheet alloc] initWithTitle:#"Choose Office" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
self.directionsActionSheet = [[UIActionSheet alloc] initWithTitle:#"Choose Office" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
self.callActionSheet.delegate = self;
self.directionsActionSheet.delegate = self;
}];
[self.viewModel.officesSignal subscribeNext:^(NSArray *offices){
#strongify(self)
for (LMOffice *office in offices) {
[self.callActionSheet addButtonWithTitle: office.name ? office.name : office.address1];
[self.directionsActionSheet addButtonWithTitle: office.name ? office.name : office.address1];
//add offices to maps
CLLocationCoordinate2D coordinate = {office.latitude.doubleValue, office.longitude.doubleValue};
MKPointAnnotation *point = [[MKPointAnnotation alloc] init];
point.coordinate = coordinate;
[self.mapView addAnnotation:point];
}
//zoom to include all offices
MKMapRect zoomRect = MKMapRectNull;
for (id <MKAnnotation> annotation in self.mapView.annotations)
{
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.2, 0.2);
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
[self.mapView setVisibleMapRect:zoomRect animated:YES];
}];
[self.viewModel.openingsSignal subscribeNext:^(NSArray *openings) {
#strongify(self)
if (openings && openings.count > 0) {
[self.openingsTable reloadData];
}
}];
ViewModel.h
#property (nonatomic, strong) LMProvider *doctor;
#property (nonatomic, strong) RACSubject *fetchDoctorSubject;
- (RACSignal *)nameSignal;
- (RACSignal *)specialtySignal;
- (RACSignal *)bioSignal;
- (RACSignal *)profileImageSignal;
- (RACSignal *)openingsSignal;
- (RACSignal *)officesSignal;
- (RACSignal *)hiddenBioSignal;
- (RACSignal *)hiddenProfileImageSignal;
- (RACSignal *)hasOfficesSignal;
ViewModel.m
- (id)init {
self = [super init];
if (self) {
_fetchDoctorSubject = [RACSubject subject];
//fetch doctor details when signalled
#weakify(self)
[self.fetchDoctorSubject subscribeNext:^(id shouldFetch) {
#strongify(self)
if ([shouldFetch boolValue]) {
[self.doctor fetchWithCompletion:^(NSError *error){
if (error) {
//TODO: display error message
NSLog(#"Error fetching single doctor info: %#", error);
}
}];
}
}];
}
return self;
}
- (RACSignal *)nameSignal {
return [RACAbleWithStart(self.doctor.displayName) distinctUntilChanged];
}
- (RACSignal *)specialtySignal {
return [RACAbleWithStart(self.doctor.primarySpecialty.name) distinctUntilChanged];
}
- (RACSignal *)bioSignal {
return [RACAbleWithStart(self.doctor.bio) distinctUntilChanged];
}
- (RACSignal *)profileImageSignal {
return [[[RACAbleWithStart(self.doctor.profilePhotoURL) distinctUntilChanged]
map:^id(NSURL *url){
if (url && ![url.absoluteString hasPrefix:#"https:"]) {
url = [NSURL URLWithString:[NSString stringWithFormat:#"https:%#", url.absoluteString]];
}
return url;
}]
filter:^BOOL(NSURL *url){
return (url != nil && ![url.absoluteString isEqualToString:#""]);
}];
}
- (RACSignal *)openingsSignal {
return [RACAbleWithStart(self.doctor.openings) distinctUntilChanged];
}
- (RACSignal *)officesSignal {
return [RACAbleWithStart(self.doctor.offices) distinctUntilChanged];
}
- (RACSignal *)hiddenBioSignal {
return [[self bioSignal] map:^id(NSString *bioString) {
return #(bioString == nil || [bioString isEqualToString:#""]);
}];
}
- (RACSignal *)hiddenProfileImageSignal {
return [[self profileImageSignal] map:^id(NSURL *url) {
return #(url == nil || [url.absoluteString isEqualToString:#""]);
}];
}
- (RACSignal *)hasOfficesSignal {
return [[self officesSignal] map:^id(NSArray *array) {
return #(array.count > 0);
}];
}
Am I right in the way I'm using signals? Specifically, does it make sense to have bioSignal to update the data as well as a hiddenBioSignal to directly bind to the hidden property of a textView?
My primary question comes with moving concerns that would have been handled by delegates into the ViewModel (hopefully). Delegates are so common in iOS world that I'd like to figure out the best, or even just a moderately workable, solution to this.
For a UITableView, for example, we need to provide both a delegate and a dataSource. Should I have a property on my controller NSUInteger numberOfRowsInTable and bind it to a signal on the ViewModel? And I'm really unclear on how to use RAC to provide my TableView with cells in tableView: cellForRowAtIndexPath:. Do I just need to do these the "traditional" way or is it possible to have some sort of signal provider for the cells? Or maybe it's best to leave it how it is, because a ViewModel shouldn't really be concerned with building the views, just modifying the source of the views?
Further, is there a better approach than my use of a subject (fetchDoctorSubject)?
Any other comments would be appreciated as well. The goal of this work is to make a prefetching/caching ViewModel layer that can be signalled whenever needed to load data in the background, and thus reduce wait times on the device. If anything reusable comes out of this (other than a pattern) it will of course be open source.
Edit: And another question: It looks like according to the documentation, I should be using properties for all of the signals in my ViewModel instead of methods? I think I should configure them in init? Or should I leave it as-is so that getters return new signals?
Should I have an active property as in the ViewModel example in ReactiveCocoa's github account?
The view model should model the view. Which is to say, it shouldn't dictate any view appearance itself, but the logic behind whatever the view appearance is. It shouldn't know anything about the view directly. That's the general guiding principle.
On to some specifics.
It looks like according to the documentation, I should be using properties for all of the signals in my ViewModel instead of methods? I think I should configure them in init? Or should I leave it as-is so that getters return new signals?
Yes, we typically just use properties that mirror their model properties. We'd configure them in -init kinda like:
- (id)init {
self = [super init];
if (self == nil) return nil;
RAC(self.title) = RACAbleWithStart(self.model.title);
return self;
}
Remember that view models are just models for a specific use. Plain old objects with plain old properties.
Am I right in the way I'm using signals? Specifically, does it make sense to have bioSignal to update the data as well as a hiddenBioSignal to directly bind to the hidden property of a textView?
If the bio signal's hiddenness is driven by some specific model logic, it'd make sense to expose it as a property on the view model. But try not to think of it in view terms like hiddenness. Maybe it's more about validness, loading, etc. Something not tied to specifically how it's presented.
For a UITableView, for example, we need to provide both a delegate and a dataSource. Should I have a property on my controller NSUInteger numberOfRowsInTable and bind it to a signal on the ViewModel? And I'm really unclear on how to use RAC to provide my TableView with cells in tableView: cellForRowAtIndexPath:. Do I just need to do these the "traditional" way or is it possible to have some sort of signal provider for the cells? Or maybe it's best to leave it how it is, because a ViewModel shouldn't really be concerned with building the views, just modifying the source of the views?
That last line is exactly right. Your view model should give the view controller the data to display (an array, set, whatever), but your view controller is still the table view's delegate and data source. The view controller creates cells, but the cells are populated by data from the view model. You could even have a cell view model if your cells are relatively complex.
Further, is there a better approach than my use of a subject (fetchDoctorSubject)?
Consider using a RACCommand here instead. It'll give you a nicer way of handling concurrent requests, errors, and thread-safety. Commands are a pretty typical way of communicating from the view to the view model.
Should I have an active property as in the ViewModel example in ReactiveCocoa's github account?
It just depends on whether you need it. On iOS it's probably less commonly needed than OS X, where you could have multiple views and view models allocated but not "active" at once.
Hopefully this has been helpful. It looks like you're heading in the right direction generally!
For a UITableView, for example, we need to provide both a delegate and
a dataSource. Should I have a property on my controller NSUInteger
numberOfRowsInTable and bind it to a signal on the ViewModel?
The standard approach, as described by joshaber above is to manually implement the datasource and delegate within your view controller, with the view model simply exposing an array of items each of which represents a view model which backs a table view cell.
However, this results in a lot of boiler-plate in your otherwise elegant view controller.
I have created a simple binding helper that allows you to bind an NSArray of view models to a table view with just a few lines of code:
// create a cell template
UINib *nib = [UINib nibWithNibName:#"CETweetTableViewCell" bundle:nil];
// bind the ViewModels 'searchResults' property to a table view
[CETableViewBindingHelper bindingHelperForTableView:self.searchResultsTable
sourceSignal:RACObserve(self.viewModel, searchResults)
templateCell:nib];
It also handles selection, executing a command when a row is selected. The complete code is over on my blog. Hope this helps!

Resources