Unloading viewControllers from UIPageViewController - iphone

I got a UIPageViewController where I add other viewControllers. Those ViewControllers are in the array viewControllersArray = [[NSMutableArray alloc] init]; I add viewControllers on this array like this:
[viewControllersArray addObject: infoViewController];
After the viewControllers are added on my array:
NSArray *initialViewControllers = [[NSArray alloc] initWithObjects:[viewControllersArray objectAtIndex:0], nil];
[self setViewControllers:initialViewControllers
direction:UIPageViewControllerNavigationDirectionForward
animated:YES
completion:^(BOOL finished){
NSLog(#"call back success");}];
This above code is all done in the viewDidLoad from my UIPageViewController.
For loading this array I got those functions form <UIPageViewControllerDataSource>
which I use like this:
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
if ([viewControllersArray containsObject:viewController]) {
NSInteger index = [viewControllersArray indexOfObject:viewController];
if (index < [viewControllersArray count] && index > 0) {
return [viewControllersArray objectAtIndex:(index - 1)];
}
}
return nil;
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
if ([viewControllersArray containsObject:viewController]) {
NSInteger index = [viewControllersArray indexOfObject:viewController];
if (index < [viewControllersArray count] - 1) {
return [viewControllersArray objectAtIndex:(index + 1)];
}
}
return nil;
}
Now what the problem is, is that when you swipe, the next viewcontrollers gets load, etc. But they don't get unloaded. So I'm searching for a way to unload the viewcontrollers that you already passed to save memory, and when you swipe back they will get reloaded.

your problem is that you hold on to your view controllers in the viewControllersArray. The array holds a strong reference to each object that you add to it. The easiest way to save memory is to just create the requested view controller on the fly in both the pageViewController:viewControllerBeforeViewController: and the pageViewController:viewControllerAfterViewController: data source methods. That way, only the page view controller will have a strong reference to the view controller and will release (dealloc) it once it is moved off-screen.
If it's too expensive to create them on-the-fly, you might consider just keeping 3 view controllers in the array: the one currently displayed and the one to the immediate left and the immediate right.
Example:
- (UIViewController *)pageViewController:(UIPageViewController *)pvc
viewControllerBeforeViewController:(MyGreatViewController *)vc
{
NSUInteger index = vc.position - 1; //custom property on your VC
if (index > 0) {
return [[MyGreatViewController alloc] initWithPosition:index];
}
return nil;
}
- (UIViewController *)pageViewController:(UIPageViewController *)pvc
viewControllerBeforeViewController:(MyGreatViewController *)vc
{
NSUInteger index = vc.position + 1; //custom property on your VC
if (index <= MAX_POSITION) {
return [[MyGreatViewController alloc] initWithPosition:index];
}
return nil;
}
Enjoy!

You should not unload View Controller. If memory is low, controllers unload their views. In code, use -(void)viewDidUnload and -(void)didReceiveMemoryWarning (iOS 6) to free your resources. When UIPageViewController will show some page, controller recreate unloaded view and call -(void)viewDidLoad (if you customize your controllers manually).

Related

RTL languages uipageviewcontroller animation

I know this question is typique and it was asked many times in the forum, but I still cannot solve my problem, so please if any body can help be that would be GREAT :)
I am creating a book application in the arabic languages and I need to perform the transitions of the uipageviewcontroller from right to left. And thats all I have to say.
One more thing (if I hadn't explain very well my self) I have the exact need as this thread: How to change UIPageViewController direction of paging curl animation but I couldn't manage to make the solution they spoke about, so if someone can explain me or give me a link where I can have how to do it that would be more than enough :)
It can be done in this way
Swap the code of pageViewController's datasource from viewControllerBeforeViewController to viewControllerAfterViewController
Change UIPageViewControllerSpineLocationMin to UIPageViewControllerSpineLocationMax
To check that, start Page-Based Application template as Universal and change the following in ModelController.m
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
NSUInteger index = [self indexOfViewController:(DataViewController *)viewController];
if (index == NSNotFound) {
return nil;
}
index++;
if (index == [self.pageData count]) {
return nil;
}
return [self viewControllerAtIndex:index storyboard:viewController.storyboard];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
NSUInteger index = [self indexOfViewController:(DataViewController *)viewController];
if ((index == 0) || (index == NSNotFound)) {
return nil;
}
index--;
return [self viewControllerAtIndex:index storyboard:viewController.storyboard];
}
and change UIPageViewControllerSpineLocationMin to UIPageViewControllerSpineLocationMax and swipe the condition of (indexOfCurrentViewController % 2 == 0) in "RootViewController.m"
- (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation
{
if (UIInterfaceOrientationIsPortrait(orientation) || ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone)) {
UIViewController *currentViewController = [self.pageViewController.viewControllers objectAtIndex:0];
NSArray *viewControllers = [NSArray arrayWithObject:currentViewController];
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];
self.pageViewController.doubleSided = NO;
return UIPageViewControllerSpineLocationMax;
}
DataViewController *currentViewController = [self.pageViewController.viewControllers objectAtIndex:0];
NSArray *viewControllers = nil;
NSUInteger indexOfCurrentViewController = [self.modelController indexOfViewController:currentViewController];
if (indexOfCurrentViewController == 0 || indexOfCurrentViewController % 2 == 0) {
UIViewController *previousViewController = [self.modelController pageViewController:self.pageViewController viewControllerBeforeViewController:currentViewController];
viewControllers = [NSArray arrayWithObjects:previousViewController, currentViewController, nil];
} else {
UIViewController *nextViewController = [self.modelController pageViewController:self.pageViewController viewControllerAfterViewController:currentViewController];
viewControllers = [NSArray arrayWithObjects:currentViewController, nextViewController, nil];
}
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];
return UIPageViewControllerSpineLocationMid;
}
Source: PageViewControllers Apple Doc
You should dive into core graphics and core animation.
Make 2 layers (a previous/next and current)
When doing a 'pangesture' (see the Event Handling Guide ) you need to see if it's a swipe left or swipe right (previous/next page)
Then perform a 3D Rotation on the layer(s)
here's a nice example about flipping pages
Madev, Do you not want to use the "PageBased Application" template?
Simply start a new xCode Project:
Under iOS > Application , look for Page-Based Application. Hit Okay, Choose you Options. All you have to do is supply your content (via the "DataSource").
Now that is the complicated part...But here's some tips
http://www.techotopia.com/index.php/Implementing_a_Page_based_iOS_5_iPhone_Application_using_UIPageViewController
http://www.techotopia.com/index.php/An_Example_iOS_5_iPhone_UIPageViewController_Application
You can set the semanticContentAttribute on your UIPageViewController's viewDidLoad like:
if <You app language is Arabic/RTL based> {
self.view.semanticContentAttribute = .forceRightToLeft
}else {
self.view.semanticContentAttribute = .forceLeftToRight
}
Keep coding......... :)

TableView obstructed by Titlebar

I'm building my first basic tabbed, application with one of the views as a navigation controller that will display a view controller.
I'm running into an issue at the point the user selects a category from the first tableview as shown in the screenshot: http://www.cl.ly/7YOF
When another instance of the tableviewcontroller is loaded and pushed onto the stack of the navigationcontroller, the table is obstructed by the title bar:
http://www.cl.ly/7ZRz
The table view select logic is below:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
KHCategory *selectedItem = [categoryArray objectAtIndex:indexPath.row];
if (selectedItem.categories.count > 0) {
KHCategoryTableViewController *nextCategoryController = [[KHCategoryTableViewController alloc] init];
nextCategoryController.categoryArray = [[NSArray alloc] initWithArray:selectedItem.categories];
nextCategoryController.title = selectedItem.labelValue;
[self.navigationController pushViewController:nextCategoryController animated:YES];
[nextCategoryController release];
} else {
NSLog(#"show detail view");
}
}
EDIT:
I should be clear that an instance of KHCategoryTableViewController is the root of my NavigationController and the NavController is wired up to the first tab of a TabController.
Two interesting things: it measures 20 pixels down (size of status bar) and your line "nextCategoryController.title = ..." doesn't seem to do anything. So...
1) I assume you haven't used setStatusBarHidden?
2) Looks like navController stuff isn't working. Can you give the code from the appDelegate that creates the tabBar and NavController?
3) Add this code, and try calling [self dumpWindow: #"VDL"] from your Subcategory ViewDidLoad method. I find it invaluable whenever checking whether my view structure is correct.
- (void) dumpWindowFrom:(NSString *) fromText {
[self dumpViews: nil from:fromText];
}
void dumpViewsRecursive(UIView* view, NSString *text, NSString *indent) {
Class cl = [view class];
NSString *classDescription = [cl description];
if ([text compare:#""] == NSOrderedSame)
NSLog(#"%d: %# %# %#", (int)view, classDescription, NSStringFromCGRect(view.frame), view.hidden ? #"Inv" : #"Vis");
else
NSLog(#"%d: %# %# %# %#", (int)view, text, classDescription, NSStringFromCGRect(view.frame), view.hidden ? #"Inv" : #"Vis");
for (NSUInteger i = 0; i < [view.subviews count]; i++)
{
UIView *subView = [view.subviews objectAtIndex:i];
NSString *newIndent = [[NSString alloc] initWithFormat:#" %#", indent];
NSString *msg = [[NSString alloc] initWithFormat:#"%#%d:", newIndent, i];
dumpViewsRecursive (subView, msg, newIndent);
[msg release];
[newIndent release];
}
}
- (void) dumpViews: (UIView *) view {
dumpViewsRecursive (( (!view) ? [[UIApplication sharedApplication] keyWindow] : view), #"" ,#"");
}
- (void) dumpViews: (UIView *) view from:(NSString *) fromText{
dumpViewsRecursive ((!view) ? [[UIApplication sharedApplication] keyWindow] : view, fromText, #"");
}
4) You could always just cheat and add:
CGRect frame = [nextCategoryController.view frame];
frame.origin.y = frame.origin.y+20.0;
[nextCategoryController.view setFrame:frame];
Check the autoResizingMask of your KHCategoryTableViewController's view.
UINavigationController overview at iPhone Dev Center says:
Note: Because the amount of space
available for the custom view can vary
(depending on the size of the other
navigation views), your custom view’s
autoresizingMask property should be
set to have a flexible width and
height. Before displaying your view,
the navigation controller
automatically positions and sizes it
to fit the available space.
This issue became resolved when I built against iOS 4.3 and not iOS 5.

Refresh UINavigationController?

I have a UINavigationController with two ViewControllers on the stack. At a certain point in the program execution, the second view controller is visible on the screen and at that moment, I would like to replace that ViewController with another. However, it's not working. Here is my code:
UINavigationController * thisNavController = self.waitingController;
// remove the Dummy and set the new page instead
NSMutableArray * newControllers = [NSMutableArray arrayWithArray: thisNavController.viewControllers];
[newControllers replaceObjectAtIndex: ([thisNavController.viewControllers count] - 1) withObject: page];
NSLog (#"visible before: %#", [thisNavController.visibleViewController description]);
[thisNavController setViewControllers: [NSArray arrayWithArray: newControllers] animated: YES];
NSLog (#"visible after: %#", [thisNavController.visibleViewController description]);
[thisNavController.visibleViewController.view setNeedsDisplay];
The above code produces this output:
2011-05-05 13:30:22.201 myApp[3286:207] visible before: <DummyViewController: 0x4c8b4c0>
2011-05-05 13:30:22.209 myApp[3286:207] visible after: <RealViewController: 0x60173f0>
But what is shown on the screen does not change. It seems that everything works fine after I switch tabs, so it seems that it is a redrawing problem, but setNeedsDisplay does nothing and I couldn't find a method that tells the NavigationController that its viewControllers have changed.
Is there some refresh mechanism that I have to trigger to refresh the screen?
One solution would be to say add 2 (initial) view controllers when your app is started, and only allow navigation from the 2nd and 3rd ones, falling back to the 1st (root) view controller in your senario described. You never allow navigation back to this 1st view controller or from this 1st view controller to the 2nd; you see this sort of behaviour in some of Apple's apps, like iTunes and Remote - if there's no network connect the app shows a no-network connection view immediately.
So, when you want to show the 1st view controller above, you do something like:
NSArray *array = [navigationController popToRootViewControllerAnimated:NO];
Without more info about the navigation behaviour of your app I hope this helps.
Or show a modal view controller?
The problem turned out to be the fact that I was trying to replace the view controller stack before the initial transition animation for the Dummy controller has finished. This can be prevented in the following manner.
First, preserve the (eventual) delegate, set the current object as the delegate, set a flag that animation is in progress and push the new controller:
self.oldNavigationControllerDelegate = self.waitingController.navigationController.delegate;
self.waitingController.navigationController.delegate = self;
self.isAnimating = YES;
[viewController.navigationController pushViewController: [[DummyViewController alloc] init] animated: YES];
Then, implement the UIViewControllerDelegate protocol methods as follows:
#pragma mark -
#pragma mark UINavigationControllerDelegate methods
- (void) navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (navigationController == self.waitingController.navigationController)
self.isAnimating = YES;
}
- (void) navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (navigationController == self.waitingController.navigationController) {
self.isAnimating = NO;
if (self.readyPage != nil)
[self pageIsReady: self.readyPage]; // method to load the ready controller
}
}
After that, whenever your content/controller/download/whatever is ready, make sure that the navigation controller is no longer animating. If it is, set a flag that the page is ready. If it isn't, load the page:
if (self.isAnimating)
self.readyPage = controller;
else
[self pageIsReady: controller];
And, of course, implement the actual loading of the new stack (as usual):
- (void) pageIsReady: (UIViewController *) page {
// this method should replace the dummy that is spinning there
UINavigationController * thisNavController = self.waitingController.navigationController;
// remove the Dummy and set the new page instead
NSMutableArray * newControllers = [NSMutableArray arrayWithArray: thisNavController.viewControllers];
[newControllers replaceObjectAtIndex: ([thisNavController.viewControllers count] - 1) withObject: page];
thisNavController.viewControllers = [NSArray arrayWithArray: newControllers];
thisNavController.delegate = self.oldNavigationControllerDelegate; // restore the original delegate
// clean up
self.isAnimating = NO;
self.readyPage = nil;
self.waitingController = nil;
self.oldNavigationControllerDelegate = nil;
}
This makes everybody happy :P

page controller

I have a problem with removing unused pages from an array:
NSMutableArray *controllers = [[NSMutableArray alloc] init];
for (unsigned i = 0; i < [descriptionsList count]; i++) {
[controllers addObject:[NSNull null]];
}
self.viewControllers = controllers;
[controllers release];
[self loadScrollViewWithPage:0];
[self loadScrollViewWithPage:1];
i'm adding objects with:
- (void)loadScrollViewWithPage:(int)page {
if (page < 0) return;
if (page >= [descriptionsList count]) return;
// replace the placeholder if necessary
DetailsView *controller = [viewControllers objectAtIndex:page];
if ((NSNull *)controller == [NSNull null]) {
controller = [[DetailsView alloc] initWithElement:[descriptionsList objectAtIndex:page]
andFrame:CGRectMake(320*page, 0, 320, 420)];
[viewControllers replaceObjectAtIndex:page withObject:controller];
[controller release];
}
// add the controller's view to the scroll view
if (nil == controller.superview) {
[scrollView addSubview:controller];
}
}
and i'm using this to remove and create pages:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
pageControlUsed = NO;
//load the visible page and the page on either side of it (to avoid flashes when the user starts scrolling)
for (unsigned i = 0; i < [descriptionsList count]; i++) {
if (i < pageController.currentPage - 1 || i > pageController.currentPage + 1) {
if ([viewControllers objectAtIndex:i] != nil) {
[[viewControllers objectAtIndex:i] release];
[viewControllers replaceObjectAtIndex:i withObject:[NSNull null]];
}
}
else {
[self loadScrollViewWithPage:i];
}
}
}
My app is crashing big time when i want to view page 3. Any advice on how should this be done? Thanks.
A couple of problems:
NSArrays can't store 'nil' objects, so your check for != nil will always succeed, so you don't need it
You definitely should not be releasing the object in the array; You don't have a corresponding -retain message, and regardless, the array will automatically retain objects put into it, and release them when they're removed
your nomenclature is a little confusing. You have an array called viewControllers, and an objected called controller, but these both appear to be views (since you're initWithFrame'ing them.
This line:
if ([viewControllers objectAtIndex:i] != nil)
will always evaluate as TRUE because the array is populated with NSNULL objects which do not evaluate to nil. The block executes even when there is a view stored at index. This block will populate your entire array with NSNull objects, wiping out all your views. Any subsequent call to the view will crash.
I think you've got a bad design here. You shouldn't be putting views into an Array. Instead, you need to have your data in an array and then populate reusable views based on what data should be displayed at any given time. Look at how 'UITable' displays itself with reusable 'UITableViewCells'.
This approach with lazy loading i've got it from a good book in fact, but the sample was with very very simple views, and without releasing them. The pages initialization was made in the scrollViewDidScroll method, which was a total mess on the device with my content: a photo and 2 texts. And the memory used is crashing my app, that's why i want to keep loaded only 3 pages. Here's the updated code, but i can't release the object nor remove from view, so i get duplicates.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
for (unsigned i = 0; i < [descriptionsList count]; i++) {
if (i < pageController.currentPage - 1 || i > pageController.currentPage + 1) {
DetailsView *controller = [viewControllers objectAtIndex:i];
if ((NSNull *)controller != [NSNull null]) {
if (nil != controller.superview) {
NSLog(#"remove from superview %d", i);
//[controller.superview removeFromSuperview];
}
[viewControllers removeObjectAtIndex:i];
[viewControllers insertObject:[NSNull null] atIndex:i];
//[viewControllers replaceObjectAtIndex:i withObject:[NSNull null]];
}
}
else {
NSLog(#"allocating %d", i);
[self loadScrollViewWithPage:i];
}
}
}
So, will i be able to create my views in real time without flashes if i'm using only 2 reusable views? I've saw a sample with 2 views but said that the content must exist, and i'm not sure how good is to keep in memory about 15 pngs.

Tab bar Controller raising NSInternalInconsistencyException

In my SDK 3.0 core data based app, I have a tab bar controller managing 4 tabs. From time to time, apparently randomly, when I launch the app, it crashes with the following message:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Directly modifying a tab bar managed by a tab bar controller is not allowed.'
However, I am not modifying in my code any of the tabs except for the following. In practice, all of the navigation controllers or controllers in the tabs have been setup using IB, and in my code I have declared two of them as outlets, since I need to access them in my app delegate applicationDidFinishLaunching() method to setup their core data managedObjectContext as follows:
[self managedObjectContext];
[self managedObjectModel];
[self persistentStoreCoordinator];
[rootViewController retain];
rootViewController.managedObjectContext = self.managedObjectContext;
Is this not correct? If so, why?
The only reference to the problem I have seen on the web is here:
http://discussions.apple.com/thread.jspa?messageID=9716886
However, the problem still persists even after deleting and recreating the tab bar controller from scratch in IB.
Any clue?
Thanks in advance.
I've had this problem too. Do you have an outlet to the UITabBar itself (not the UITabBarController) in the nib? When I removed that, I stopped having problems.
Sorry this isn't a 100% reliable explanation, but this workaround cleared the problem up for me.
I've gotten this exception a few times, especially when changing things with localizations. Cleaning the targets and then rebuilding seems to work around the issue.
I quickly wrote the following class and showing/hiding tab views from UITabBarController worked like magic:
TabBarDesigner.h
#import <Foundation/Foundation.h>
#interface TabBarDesigner : NSObject
{
}
+(void) setTabBarController:(UITabBarController *)tabBarController
items:(NSArray *)tabBarItems
viewControllers:(NSArray *)viewControllers;
+(void) removeItemsInRange:(NSRange) range;
#end
TabBarDesigner.m
#import "TabBarDesigner.h"
static NSArray *_tabBarItems = NULL;
static NSArray *_viewControllers = NULL;
static UITabBarController *_tabBarController = NULL;
#implementation TabBarDesigner
+(void) setTabBarController:(UITabBarController *)tabBarController
items:(NSArray *)tabBarItems
viewControllers:(NSArray *)viewControllers
{
if (tabBarItems && viewControllers && tabBarController)
{
if ([tabBarItems count] == [viewControllers count])
{
[_tabBarItems release];
[_viewControllers release];
_tabBarItems = [tabBarItems copy];
_viewControllers = [viewControllers copy];
_tabBarController = tabBarController;
}
}
}
+(void) removeItemsInRange:(NSRange) range
{
if (_tabBarController)
{
if ( range.location < ([_tabBarItems count] - 1) )
{
if ( (range.length + range.location) < [_tabBarItems count] )
{
NSMutableArray *tabBarItems = [_tabBarItems mutableCopy];
[tabBarItems removeObjectsInRange:range];
NSMutableArray *viewControllers = [_viewControllers mutableCopy];
[viewControllers removeObjectsInRange:range];
[_tabBarController setViewControllers:viewControllers];
NSUInteger i;
for (i = 0; i< [viewControllers count]; i++)
{
UIViewController *vC = [viewControllers objectAtIndex:i];
vC.tabBarItem.image = [[tabBarItems objectAtIndex:i] image];
vC.tabBarItem.title = [[tabBarItems objectAtIndex:i] title];
vC.tabBarItem.tag = [[tabBarItems objectAtIndex:i] tag];
}
[tabBarItems release];
[viewControllers release];
}
}
}
}
#end
A sample of how to use this class:
In your MyAppDelegate.m
#import "TabBarDesigner.h"
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[TabBarDesigner setTabBarController:_tabBarController
items:[_tabBarController.tabBar items]
viewControllers:[_tabBarController viewControllers]];
// remove the first 3 tabs
[TabBarDesigner removeItemsInRange:NSMakeRange(0,3)];
// show all tabs
[TabBarDesigner removeItemsInRange:NSMakeRange(0,0)];
// continue with your code
}
Cheers!