Tag: UITableView

  • Updating View With Two Data Sources in iOS

    In this article I am going to describe a robust, simple and clean app architecture that allows to update a single iOS tableView from two different data sources asynchronously.

    Let’s say, we have two sources of data, that need to be displayed in a tableView. When user pulls to refresh a tableView, two separate asynchronous requests are performed. While updating continues we need to show activity indicator and we can dismiss it only when both requests are finished.

    This can be cleanly accomplished using ReactiveCocoa + MVVM. One of the benefits of ReactiveCocoa is in the easiness of synchronization of asynchronous data streams.

    One would say, that this task can be also performed by using of dispatch groups. I agree, but it is more complex and not easy to do when the nature of data sources is different. For example, when one of the datasources is HTTP API and the second is a WebSocket.

    Architecture looks like this:

    In a ViewModel we have 3 arrays – cloudData1 (or any other name you want, that better describes your domain), cloudData2 and a union single datasource of a TableView – tableData. When cloudData1 or cloudData2 updates, the update of tableData is triggered and then it triggers an update of the TableView itself by bindings.

    In the init method of a ViewModel we create a binding of cloudData1 and cloudData2 to tableData:

    - (instancetype)init
    {
        self = [super init];
        
        if (self) {
            RAC(self, tableData) = [RACSignal combineLatest:@[RACObserve(self, cloudData1), RACObserve(self, cloudData2)]
                                                     reduce:^id _Nullable (NSArray *cloudData1, NSArray *cloudData2) {
                                                         NSMutableArray *temp = [NSMutableArray new];
                                                         [temp addObjectsFromArray:cloudData1];
                                                         [temp addObjectsFromArray:cloudData2];
                                                         return [NSArray arrayWithArray:temp];
                                                     }];
            
            RAC(self, inProgress) = [RACSignal combineLatest:@[RACObserve(self, cloud1RequestInProgress), RACObserve(self, cloud2RequestInProgress)]
                                                      reduce:^id _Nullable (NSNumber *cloud1RequestInProgress, NSNumber *cloud2RequestInProgress) {
                                                          return @(cloud1RequestInProgress.boolValue || cloud2RequestInProgress.boolValue);
                                                      }];
        }
        
        return self;
    }
    

    Also we combine two flags of progress cloud1RequestInProgress and cloud2RequestInProgress to a single one – inProgress.

    LoadData method of a ViewModel starts the loading of data from each of two datasources:

    - (void)loadData
    {
        [self loadCloudData1];
        
        [self loadCloudData2];
    }
    

    And in a TableView we create a binding between ViewModels tableData and a View:

    [RACObserve(self, viewModel.tableData)
         subscribeNext:^(id  _Nullable x) {
             [self.tableView reloadData];
         }];
    

    Pull to refresh triggers loadData of a ViewModel:

    if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max) {
            self.tableView.refreshControl = [UIRefreshControl new];
            [self.tableView.refreshControl addTarget:self.viewModel
                                              action:@selector(loadData)
                                    forControlEvents:UIControlEventValueChanged];
        }
    

    In a view we also create a binding between inProgress property of a ViewModel and the need to dismiss refreshing indicator:

    [[RACObserve(self, viewModel.inProgress)
          filter:^BOOL(NSNumber*  _Nullable inProgress) {
              return ! inProgress.boolValue;
          }]
         subscribeNext:^(id  _Nullable x) {
             [self.tableView.refreshControl endRefreshing];
         }];
    

    Simple and brief -> easy to support, extend and reuse.

  • Solution for iOS Developer Technical Interview Problem

    I have found an interesting iOS Developer Technical Interview Problem and solved it.

    “I also have the ultimate iOS Developer technical test you can assign a potential hire. It should take 1-3 hours. It is easy to communicate, allows a lot of freedom of implementation so you can really get a better picture into how a developer thinks, and will make sure this developer knows the absolute fundamentals. Ready for it?

    Calculate the and display each Fibonacci number from 1 -> max N possible on an iPhone with unsigned integers, and display each F(n) in a table view. The UITableView scrolling MUST remain smooth.

    That’s it. You’ll be amazed at how profoundly simple this task sounds and yet how much iOS knowledge can be demonstrated. Not just what they know, but how they structure their work. You can assess their APIs, their separation of concerns when designing classes, the considerations they’ve made for performance, and their knowledge of concurrency. (Not to mention their knowledge of recursive functions.) It is ok to give them the formula, and allow them to use Google. F(n) = F(n-1) + F(n-2).”

    This is the result:
    IMG_0051

    GitHub repository with a solution is here.

    I used recursion and concurrency. I added a little delay of 0.1 s to make a delay visible but scrolling is still smooth. This is my solution:

    //
    //  FiboTableViewController.m
    //  fibo
    //
    //  Created by Dennis Kutlubaev on 05.11.14.
    //  Copyright (c) 2014 Alwawee. All rights reserved.
    //
    
    #import "FiboTableViewController.h"
    
    
    @interface FiboTableViewController ()
    {
        NSInteger _maxN;
        dispatch_queue_t _backgroundQueue;
    }
    
    @property (nonatomic, strong) NSMutableArray *tableData;
    
    
    @end
    
    
    @implementation FiboTableViewController
    
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSLog(@"Max Integer: %ld", NSIntegerMax);
        
        _maxN = 10000;
        
        _backgroundQueue = dispatch_queue_create("background queue", nil);
        
        [self loadData];
    }
    
    
    - (void)loadData
    {
        _tableData = [NSMutableArray arrayWithArray:@[@(1),@(2)]];
    }
    
    
    #pragma mark - Table view data source
    
    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    {
        return 1;
    }
    
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
        return _maxN;
    }
    
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
        
        NSInteger n = indexPath.row;
        __block NSInteger fibo;
        if ( n < [_tableData count] ) {
            fibo = [_tableData[n] integerValue];
            if (fibo > 0) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    cell.textLabel.text = [NSString stringWithFormat:@"%ld", (long)fibo];
                });
            }
        }
        else {
            dispatch_async(_backgroundQueue, ^{
                
                fibo = [self fibo:n];
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (fibo > 0) {
                        cell.textLabel.text = [NSString stringWithFormat:@"%ld", (long)fibo];
                    }
                    else {
                        [self.tableView reloadData];
                        //cell.textLabel.text = [NSString stringWithFormat:@"Max integer reached: %ld", NSIntegerMax];
                    }
                });
                
            });
        }
        
        return cell;
    }
    
    
    //F(n) = F(n-1) + F(n-2)
    - (NSInteger)fibo:(NSInteger)n
    {
        NSInteger f;
        
        if ( n < [_tableData count] ) {
            f = [_tableData[n] integerValue];
        }
        else {
            [NSThread sleepForTimeInterval:0.1];
            
            f = [self fibo:(n-2)] + [self fibo:(n-1)];
            
            if ((f < NSIntegerMax)&&(f > 0)) {
                [_tableData addObject:@(f)];
            }
            else {
                f = 0;
                _maxN = n-1;
            }
        }
        
        return f;
    }
    
    
    @end
    
    

    It scrolls without lags on iPhone 6 Plus. I do not calculate twice a fibonacci number if already have calculated it, I store a new fibonacci number in a NSMutableArray.