HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Switching between view controllers that are loaded in the background

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
thebackgroundarecontrollersswitchingviewbetweenthatloaded

Problem

I have a dining menu app that scrapes the data from a website and redisplays it in a mobile format, displayed below:

If the user swipes left and right, the app will show the previous/next meal (ex. if current meal is lunch, swiping left will show dinner).

I'm trying to come up with a more efficient way of switching between menus. With my current implementation, each menu is a separate view controller.

When someone swipes right, it calls:

-(void)swipeleft:(UISwipeGestureRecognizer*)gestureRecognizer {
    //swipes to next meal
    MealType curMeal = currentMenu.type;
    int newMeal = [MenuLoader MealAfterMeal:curMeal];

    MenuTableController *newMenu = [self.storyboard instantiateViewControllerWithIdentifier:@"Menu"];
    newMenu.currentMeal = newMeal;

    [self.navigationController pushViewController:newMenu animated:YES];
}


When each view controller is created (including the first), it loads the menu in a background thread:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        MenuLoader *ml = [[MenuLoader alloc] init];
        currentMenu = [ml mealForType:_currentMeal Specificity:[self summaryPreference]];

        dispatch_async(dispatch_get_main_queue(), ^(void) {
            ...
        });
    });


Inside the mealForType:Specificity: call, I parse 2 separate web pages through NSURL, and combine the information from both sites:

NSURL *url = [NSURL URLWithString:urlString];
NSData *pageData = [NSData dataWithContentsOfURL:url];


Some of the optimizations I'm hoping to make are:

  • Preloading adjacent menus in another thread to reduce loading time when someone swipes



  • If someone swipes forward and then back, it should show the already loaded menu. Right now, it makes a fresh call to the server and reloads everything.



  • Right now, if someone swipes multiple times in a row before any of the menus finish loading, the app tries processing all of the server calls at once and this slows down

Solution

I recently created a similar layout for a project I'm working on using Apple's PageControl example as a base.

I used a UICollectionView for the top nav section and a UIScrollView for paging through view controllers. The main view controller is setup as below.

In the viewDidLoad method for the I set up the content size for the scrollView and set paging to YES.

[self.containerScrollView setContentSize:CGSizeMake(CGRectGetWidth(self.containerScrollView.frame)*self.numberOfPages, 
CGRectGetHeight(self.containerScrollView.frame))];
[self.containerScrollView setPagingEnabled:YES];
[self.containerScrollView setShowsHorizontalScrollIndicator:NO];
[self.containerScrollView setShowsVerticalScrollIndicator:NO];
[self.containerScrollView setScrollsToTop:NO];
[self.containerScrollView setDelegate:self];


(Note: if you're using xibs/size classes, I ran into a problem where the scrollView's size is stated as 600x600px no matter which device is used until the ParentViewController's viewDidLayoutSubviews is called. I had to reset the scroll view's content size again.)

After this is set up, I load the first two view controllers:

- (void)loadScrollViewWithPage:(NSUInteger)page {

    UIViewController *viewController = ;

    if (viewController.view.superview == nil) {
        [self addChildViewController:viewController];
        CGRect frame = [self.containerScrollView frame];
        frame.origin.x = CGRectGetWidth(frame)*page;
        frame.origin.y = 0;
        [viewController.view setFrame:frame];

        [self.containerScrollView addSubview:viewController.view];
        [viewController didMoveToParentViewController:self];
    }
}


Since the first two view controllers are loaded, there is no lag when the user swipes between them. When UIScrollView is scrolled, the page is scrolls to is already loaded. Once it does, I load the next one in order:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    CGFloat pageWidth = CGRectGetWidth(self.scrollView.frame);
    NSUInteger page = floor((self.scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;

    [self loadScrollViewWithPage:page - 1];
    [self loadScrollViewWithPage:page];
    [self loadScrollViewWithPage:page + 1];
}


This way you always have the current, previous and the next view controller always ready. You could load more than 3 if the network calls you have to make take longer and the what the user is expected to scroll at.

You could unload older view controllers by calling viewController.view = nil and always only have a set number of view controllers loaded.

I created a custom protocol for the UICollectionView and set my parent controller as delegate to respond to cell clicks and navigate to the appropriate page.

Code Snippets

[self.containerScrollView setContentSize:CGSizeMake(CGRectGetWidth(self.containerScrollView.frame)*self.numberOfPages, 
CGRectGetHeight(self.containerScrollView.frame))];
[self.containerScrollView setPagingEnabled:YES];
[self.containerScrollView setShowsHorizontalScrollIndicator:NO];
[self.containerScrollView setShowsVerticalScrollIndicator:NO];
[self.containerScrollView setScrollsToTop:NO];
[self.containerScrollView setDelegate:self];
- (void)loadScrollViewWithPage:(NSUInteger)page {

    UIViewController *viewController = <get your view controller>;

    if (viewController.view.superview == nil) {
        [self addChildViewController:viewController];
        CGRect frame = [self.containerScrollView frame];
        frame.origin.x = CGRectGetWidth(frame)*page;
        frame.origin.y = 0;
        [viewController.view setFrame:frame];

        [self.containerScrollView addSubview:viewController.view];
        [viewController didMoveToParentViewController:self];
    }
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    CGFloat pageWidth = CGRectGetWidth(self.scrollView.frame);
    NSUInteger page = floor((self.scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;

    [self loadScrollViewWithPage:page - 1];
    [self loadScrollViewWithPage:page];
    [self loadScrollViewWithPage:page + 1];
}

Context

StackExchange Code Review Q#74831, answer score: 2

Revisions (0)

No revisions yet.