Releasing unused pages in UIPageViewController

EricAdvance picture EricAdvance · Feb 7, 2013 · Viewed 9.6k times · Source

I am using a separate .h, .m, and .xib files for each UIViewController based page of a UIPageViewController based picture book. Each page is loaded with animations, music, etc. and takes about 4MB of memory. In Instruments, the free memory drops down about 4MB as each page is loaded. This memory is never released as the pages are turned. It eventually gives memory warnings. UIPageViewController seems to keep each page it instantiates in memory and won't unload it. So when pages are turned fast, the app crashes.

I would like to be able to unload all pages except the 3 needed by UIPageViewController - the previous, current, and next pages. How can I unload undesired pages since they were instantiated by UIPageViewController.

Below is the Array of the pages that UIPageViewController pulls from. All of the pages (Page1, Page2, etc.) basically just load image files, provide basic animation, and have music.

    //ARRAY OF PAGES    
pageArray = [[NSArray alloc] initWithObjects:
        (id)[[Page1 alloc] initWithNibName:nil bundle:nil],
            [[Page2 alloc] initWithNibName:nil bundle:nil],    
            [[Page3 alloc] initWithNibName:nil bundle:nil],    
            [[Page4 alloc] initWithNibName:nil bundle:nil],    
            [[Page5 alloc] initWithNibName:nil bundle:nil],    
            [[Page6 alloc] initWithNibName:nil bundle:nil], 
            [[Page7 alloc] initWithNibName:nil bundle:nil],
            [[Page8 alloc] initWithNibName:nil bundle:nil],
             // continues all the way up to page 47
             [[Page47 alloc] initWithNibName:nil bundle:nil],
             nil];

I've left out the standard initialization for UIPageViewController. It uses "nextPageNumber" to pull the right page from the pageArray above to create a new page object.

-(void)turnPageForward{

[pageController setViewControllers:[NSArray arrayWithObject:[pageArray objectAtIndex:nextPageNumber]]
                         direction:UIPageViewControllerNavigationDirectionForward
                          animated:YES completion:^(BOOL finished){
                          }];

}

I have tried creating an object "pageIndex" (see below) that is set to nil after providing it to the pageController. It didn't work. The page still took up memory well after the pages had advanced.

//PROGRAM PAGE FORWARD

-(void)turnPageForward{

UIViewController * pageIndex =[pageArray objectAtIndex:nextPageNumber];  //nextPageNumber is the next page to load

[pageController setViewControllers:[NSArray arrayWithObject:pageIndex]
                         direction:UIPageViewControllerNavigationDirectionForward 
                          animated:YES completion:^(BOOL finished){
                          }];                                  
pageIndex = nil;                            

}

I've looked through stackoverflow for posts using the same way of supplying pages to UIPageViewController, but haven't found anything close. The closest was "ARC not releasing memory when going “back” in navigation controller" but doesn't set the view controllers the same way.

I've tried to set the undesired pages to nil so ARC can remove them with no luck. Any suggestions or alternate paths I should try? I like the page curl effect and have not been able to find a good one elsewhere that does horizontal page curls.

Thanks! Eric

Answer

rdelmar picture rdelmar · Feb 7, 2013

"UIPageViewController seems to keep each page it instantiates in memory"

No, you're doing that by instantiating all those "pages" (controllers), and putting them into an array (the memory jumps as you turn the page because the controller's view is not actually loaded until you display it, even though the controller has been instantiated. But once you've done that, your controller retains its view and the array retains the controller). You just need to keep some kind of count of which page you're on, and in the implementation of viewControllerBeforeViewController: and viewControllerAfterViewController:, instantiate a page there. When you go away from that page, its controller will be dealloc'd. If the loading is slow, you might need to keep an array that has the current page and the ones before and after -- UIPageViewController does do that if you have it set to scroll instead of page curl for the transition.

I made a simple test app like this:

@implementation ViewController {
    int count;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    count = 1;
    self.pager = [[UIPageViewController alloc]initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl navigationOrientation:UIPageViewControllerNavigationDirectionForward options:nil];
    self.pager.dataSource = self;
    self.pager.delegate = self;
    Page1 *first = [[Page1 alloc] initWithNibName:@"Page1" bundle:nil];
    [self.pager setViewControllers:@[first] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
    [self addChildViewController:self.pager];
    [self.view addSubview:self.pager.view];
    [self.pager didMoveToParentViewController:self];
}


-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
    if (count > 1) {
        NSString *nibName = [@"Page" stringByAppendingFormat:@"%d",count-1];
        UIViewController *prev = [[NSClassFromString(nibName) alloc] initWithNibName:nibName bundle:nil];
        count -= 1;
        return prev;
    }
    return nil;
}

-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
    if (count < 3) {
        NSString *nibName = [@"Page" stringByAppendingFormat:@"%d",count+1];
        UIViewController *next = [[NSClassFromString(nibName) alloc] initWithNibName:nibName bundle:nil];
        count += 1;
        return next;
    }
    return nil;
}

My example only has 3 pages, but it illustrates one way to do this. I put logs in the dealloc methods of the 3 controllers, and they were called when I navigated away from them.