I have a UIPageViewController and I just can not figure out how to know to what direction the user turned the page so i can set the page count appropriately.
Thanks
Shani
As Hejazi said
After a gesture-driven transition completes this delegate method
is called:
pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:
The part that should be clarified is that completed will be YES if the page was fully turned and will be NO if the page did not actually turn. The NO case happens, for example, when the user just pulls up the corner of the page and then puts it back down without flipping the page.
This is the concept you will want to implement:
- (void)pageViewController:(UIPageViewController *)pvc didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
// If the page did not turn
if (!completed)
{
// You do nothing because whatever page you thought
// the book was on before the gesture started is still the correct page
return;
}
// This is where you would know the page number changed and handle it appropriately
// [self sendPageChangeNotification:YES];
}
After a gesture-driven transition completes this delegate method is called:
pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:
So by comparing the previousViewControllers parameter and pageViewController.viewControllers you can know the direction.
The 'Page-Based Application' template provide these 2 methods :
- (NSUInteger)indexOfViewController:(DataViewController *)viewController;
a method for finding index given a view controller
- (DataViewController *)viewControllerAtIndex:(NSUInteger)index
a method for instantiating a view controller given an index.
For making the correct animation, you need to know the index of your current view controller. The page-based template methods are perfect fit for that. Then, you simply compare your 'jump to' index and your 'current' index.
Here's some code to get the idea :
- (void)jumpToPage:(NSInteger)page {
// find current index
DataViewController *currentViewController = (DataViewController *)[self.pageViewController.viewControllers lastObject];
NSUInteger index = [self indexOfViewController:currentViewController];
// choosing the correct direction
// if the 'current' is smaller than the 'jump to' page, then choose forward
// vice versa
UIPageViewControllerNavigationDirection direction;
if (index < page) {
direction = UIPageViewControllerNavigationDirectionForward;
} else {
direction = UIPageViewControllerNavigationDirectionReverse;
}
// choose view controllers according to the orientation
NSArray *viewControllers;
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) {
DataViewController *rightViewController = [self viewControllerAtIndex:page];
viewControllers = [NSArray arrayWithObject:rightViewController];
} else {
DataViewController *rightViewController = [self viewControllerAtIndex:page];
DataViewController *leftViewController = [self viewControllerAtIndex:page-1];
viewControllers = [NSArray arrayWithObjects:leftViewController, rightViewController, nil];
}
// fire the method which actually trigger the animation
[self.pageViewController setViewControllers:viewControllers
direction:direction
animated:YES
completion:nil];
}
You could add a "pageIndex" property to your view controllers that serve as the pages. IOW, when you create the view controllers for viewControllerBeforeViewController & viewControllerAfterViewController (or when you call setViewControllers), store a pageIndex in those view controllers that you can then reference whenever you need to know the index.
great answers for page controller handling. I found that the view controller that was added as a page will call viewWillAppear as the user slides the page into view and will also call viewDidAppear upon completion.
When the user turns a page the UIPageViewController's setViewControllers: method will be called. This method receives an argument of type UIPageViewControllerNavigationDirection that will give you the information you need.
Related
I'm implementing a page based app.
Basically the pageViewController will be used to show pages of data.
Same format, different data.
Created a ViewController to hold the pageViewController and another viewController which is going to present the data.
I only need one different viewController for the data, I saw this can be a problem.
Normal pagers, like a Android ViewPager, will ask you to return the view you want to show given an index, PageViewController doesn't work like that, it just ask you for the next and the previous viewcontrollers.
I was hoping that storing the index in a property of the content ViewController will do the job... but it wasn't.
It is partially working but..
here is my relevant code:
- (void)viewDidLoad {
[super viewDidLoad];
page = [[UIPageViewController alloc]initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];
[page setDelegate:self];
[page setDataSource:self];
index = 0;
CampaignContent * content = [[CampaignContent alloc]initWithNibName:#"CampaignContent" bundle:nil];
[content setTitlestr:#"Index 0"];
[content setDesc:#"desc1"];
[content.view setTag:1];
[page setViewControllers:#[content] direction:UIPageViewControllerNavigationDirectionForward animated:TRUE completion:nil];
[page willMoveToParentViewController:self];
[self addChildViewController:page];
[self.view addSubview:page.view];
[page didMoveToParentViewController:self];
}
-(UIViewController*)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
last=false;
NSLog(#"move right index %i",index);
CampaignContent * content = [[CampaignContent alloc]initWithNibName:#"CampaignContent" bundle:nil];
[content setTitlestr:[NSString stringWithFormat:#"Index %i",index+1]];
[content.view setTag:viewController.view.tag+1];
return content;
}
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
if(completed)
{
if([[previousViewControllers objectAtIndex:0] view].tag<index){
NSLog(#"Left");
index = index-1;
}else{
NSLog(#"Right");
index = index+1;
}
NSLog(#"didFinishAnimating view tag %i",[[previousViewControllers objectAtIndex:0] view].tag,index);
}
}
-(UIViewController*)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
last= true;
NSLog(#"move left index %i",index);
CampaignContent * content = [[CampaignContent alloc]initWithNibName:#"CampaignContent" bundle:nil];
[content setTitlestr:[NSString stringWithFormat:#"Index %i",index-1]];
[content.view setTag:viewController.view.tag-1];
return content;
}
Just wanting to see the index properly working but instead...
Starting on 0, I swipe to the next page and I see the following in the log
move right index 0
move left index 0
Right
didFinishAnimating view tag 1
move right index 1
Wow such a lot of calls... but index displayed still OK
Again swipe to the next page
Right
didFinishAnimating view tag 2
move right index 2
To this point the index displayed its correct, the problem starts when changing direction.
Swipe to the previous now I see this:
Right
didFinishAnimating view tag 3
move left index 3
Looks like it has failed on didFinishAnimating when determining swipe direction but still index displayed is OK
Again Swipe left
Left
didFinishAnimating view tag 2
Here seems that viewControllerBeforeViewController are not getting called, we have gone from 0->1->2->1->0 right? but index displayed has done 0->1->2->1->2
And if I swipe left again I can see it goes to ->1->0->-1 just like it should.
The same applies going on the other way.
So, I'm thinking that viewControllerBeforeViewController and viewControllerAfterViewController are not reliable and didFinishAnimating can't determine swipe direction alone.
Is there any way I can do this properly or I'm simply doing something bad?
Sorry for my large post, hope someone can help me.
What you can do is use ViewControllerAtIndex method. Inside that method set the contentViewControllers property accordingly based on index.
Then in the viewDidAppear method of contentView Controller you will get the values that you set in the ViewControllerAtIndex method which you can use according to your requirement.
Okay I am kind of new to IOS development, but I am writing an application where I am using a timer class to time out the user if they idle too long on any particular scene in my storyboard and it bumps the user back to the original scene/view. I have a single story board that is made up of several scenes/views(not sure what the correct word here is), and each scene has its own view controller.
I accomplish the timeout via the appdelegate class. See code below.
So I have the code working and it works great, but I am trying to make it so that it will ignore the timer if we are on the main scene.
I have googled this, read copious amounts of documentation, and have tried many things but so far I haven't been able to figure out how to get the currently viewed scene in the applicationDidTimeout method.
If I can get the name of the currently viewed scene/view, then I can choose to ignore the timer or not.
Does anyone know how to do this?
Thank you for your time.
#import "StoryboardAppDelegate.h"
#import "TIMERUIApplication.h"
#implementation StoryboardAppDelegate
#synthesize window = _window;
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// applicaiton has timed out
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(applicationDidTimeout:) name:kApplicationDidTimeoutNotification object:nil];
return YES;
}
-(void)applicationDidTimeout:(NSNotification *) notif
{
NSLog (#"time exceeded!!");
UIViewController *controller = [[UIStoryboard storyboardWithName:#"Main" bundle:NULL] instantiateViewControllerWithIdentifier:#"StoryboardViewController"];
UINavigationController * navigation = [[UINavigationController alloc]initWithRootViewController:controller];
[self.window setRootViewController:navigation];
navigation.delegate = self;
navigation.navigationBarHidden = YES;
if (controller) {
#try {
[navigation pushViewController:controller animated:NO];
} #catch (NSException * ex) {
//“Pushing the same view controller instance more than once is not supported”
//NSInvalidArgumentException
NSLog(#"Exception: [%#]:%#",[ex class], ex );
NSLog(#"ex.name:'%#'", ex.name);
NSLog(#"ex.reason:'%#'", ex.reason);
//Full error includes class pointer address so only care if it starts with this error
NSRange range = [ex.reason rangeOfString:#"Pushing the same view controller instance more than once is not supported"];
if ([ex.name isEqualToString:#"NSInvalidArgumentException"] &&
range.location != NSNotFound) {
//view controller already exists in the stack - just pop back to it
[navigation popToViewController:controller animated:NO];
} else {
NSLog(#"ERROR:UNHANDLED EXCEPTION TYPE:%#", ex);
}
} #finally {
//NSLog(#"finally");
}
} else {
NSLog(#"ERROR:pushViewController: viewController is nil");
}
[(TIMERUIApplication *)[UIApplication sharedApplication] resetIdleTimer];
}
#end
I'm assuming you've written the logic for the timer somewhere. Can you just invalidate the timer when you've popped back to the rootViewController?
Also instead of pushing a viewController onto the navigationViewController and handling the errors, you should check to see if the controller you're pushing is already in the stack like so:
if (![navigation.viewControllers containsObject:viewController] {
// push onto the stack
}
You could also check to see how many levels are currently in the navigationController by checking the count of the viewControllers array like so:
if ([navigation.viewControllers count] == 0) {
// I know we're on the main screen because no additional viewControllers have been added to the stack.
}
If you are not using modal controllers anywhere then the simplest solution would be
UINavigationController* nav = (UINavigationController*)self.window.rootViewController; // You could just save the nav as part of your app delegate
if (nav.viewControllers.count > 1){
[nav popToRootViewControllerAnimated:YES];
}
This is different then your current code because your main page will not be deleted and recreated every time the timer goes off
Okay I figured out how to do this. I was making this way too complicated.
To solve this I simply made a property and method in the app delegate class where I could set a scene name.
Then in each view controller header file I import the header file for the app delegate class and define a reference to it. Then in the load event for each view I simply set the scene name in the app delegate class using this line of code.
[myAppDelegate setSceneName:self.title];
Easy peasy!
I have two UICollectionView objects, that both have different source and delegate. I would like to achieve a "Photos app'esque" look with the transition, using UseLayoutToLayoutNavigationTransitions.
It doesn't work though. When I call the UseLayoutToLayoutNavigationTransitions it changes the layout, but not the content.
First picture is first collection view. A series of categories and the people contained in them.
Second picture is what I'd like the animation to end up in. A series of people within a certain category.
Last picture is what happens right now. Categories just get rearranged.
Have a look at http://www.objc.io/issue-12/collectionview-animations.html in the
Transitions Between UICollectionViewController Instances
section.
it basically shows you that you'll have to change the datasource and delegate manually by implementation of the navigation controller delegate methods:
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if ([viewController isKindOfClass:[FJDetailViewController class]]) {
FJDetailViewController *dvc = (FJDetailViewController*)viewController;
dvc.collectionView.dataSource = dvc;
dvc.collectionView.delegate = dvc;
[dvc.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:_selectedItem inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:NO];
}
else if (viewController == self){
self.collectionView.dataSource = self;
self.collectionView.delegate = self;
}
}
Your problem is that during the transition iOS will change the datasource. See my answer to this question How to use useLayoutToLayoutNavigationTransitions in UICollectionView?
You can use the same pattern described there:
use UseLayoutToLayoutNavigationTransitions to get the layout changes
observe when the transition is done
set the datasource to the one you need at that point
I am using a uipopover to present a mini number pad to the user when they enter a textfield on my main view controller.
when they enter numbers using the number pad, i save the entry into a nsstring property that I've named keypadvalue.
there is an unwind segue wired to a done button on the popover which fires the following code.
- (IBAction)doneWithKeyboard:(UIStoryboardSegue *)segue
{
NSLog(#"unwind");
if ([segue.sourceViewController isKindOfClass:[KeyPopupViewController class]])
{
KeyPopupViewController *popOver2 = segue.sourceViewController;
activeField.text =popOver2.keypadValue;
}
}
the activetextfield on my main view controller then gets updated to the kepadvalue, and this all works fine.
my problem now is that i want the activetextfield to update the same way if the user presses outside the uipopover, and it dismisses without firing the unwind segue.
i thought i might use the following to perform the update when the popover dismisses
-(BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
{
activeField.text = controller.keypadValue;
return YES;
}
unfortunately despite multiple attempts i can't get the property to return a value it is always null even though the method fires as expected.
how should i recover the property value from the popover using this or another method?
i am obviously doing something wrong
can anyone advise
thanks
It should help:
-(BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
{
[self.view endEditing:YES];
activeField.text = controller.keypadValue;
return YES;
}
i've got a question regarding a UIScrollView with paging enabled that contains many UIView, each managed by an own UIViewController.
Right now there are about 20 to 30 UIViewControllers that COULD be contained in the UIScrollView. It's a catalog app on the iPad, and I started preloading all the views at the beginning, but with the amount of UIViewControllers getting bigger and bigger, that is not an option any more.
I'm looking for the perfect solution in terms of memory usage. It's no problem to reload the UIViewControllers when the ContentOffset of the ScrollView reaches a specific controller. And I think to nil the UIViewControllers when the ContentOffset tells me that the UIViewControllers is not needed any more isn't that hard as well.
What is the correct way to handle this? Is it enough to alloc the UIViewControllers when needed, putting them into a NSMutableDictionary or NSMutableArray and nil them when they are not needed any more? A little bit of help from someone already having done something similar would be great!
Thanks for your help!
I'm sure there are some good infinite scrolling classes out there, but if you were going to "roll your own", here is a minimalist bit of code that demonstrates the process of infinite scrolling, keeping the current, previous, and next pages in memory, but letting go of anything else. This assumes that:
you're doing horizontal scrolling and have turned on paging;
that you're using view controllers for the child views;
your child view controller class has a page property to keep track of what page it's for; and
you've made your view controller the delegate for your scroll view
Thus, it might look like:
- (void)viewDidLoad
{
[super viewDidLoad];
// my underlying model is just an array of strings, which I'll show on my child
// view; your model will be more elaborate, but I just want to illustrate the concept
self.objects = #[#"1", #"2", #"3", #"4", #"5", #"6", #"7", #"8", #"9"];
// set the `contentSize` for the scrollview
CGRect content = self.view.bounds;
content.size.width *= [self.objects count]; // make it wide enough to hold everything
self.scrollView.contentSize = content.size;
// set our current page and load the first pages (the first and the next pages)
self.currentPage = 0;
[self addChildPage:0 toScrollView:self.scrollView];
[self addChildPage:1 toScrollView:self.scrollView];
}
- (void)addChildPage:(NSInteger)page toScrollView:(UIScrollView *)scrollView
{
// create the child controller
ChildViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:#"child"];
// set whatever properties you need to in order for it to present its information correctly
controller.text = self.objects[page];
controller.page = page;
// now do the stuff to add it to the right place in the scrollview
CGRect frame = self.view.bounds;
frame.origin.x = frame.size.width * page;
controller.view.frame = frame;
[self addChildViewController:controller]; // containment call for adding child view controller
[scrollView addSubview:controller.view];
[controller didMoveToParentViewController:self]; // containment call when done adding child
}
- (ChildViewController *)childControllerForPage:(NSInteger)page
{
for (ChildViewController *controller in self.childViewControllers)
{
if (controller.page == page)
return controller;
}
return nil;
}
- (void)addChildIfNecessary:(NSInteger)page toScrollView:(UIScrollView *)scrollView
{
if (page < 0 || page >= [self.objects count])
return;
ChildViewController *controller = [self childControllerForPage:page];
if (controller == nil)
[self addChildPage:page toScrollView:scrollView];
}
- (void)removeChildController:(UIViewController *)controller
{
[controller willMoveToParentViewController:nil]; // containment call before removing child
[controller.view removeFromSuperview];
[controller removeFromParentViewController]; // containment call to remove child
}
- (void)updateChildrenViewsForPage:(NSInteger)page forScrollView:(UIScrollView *)scrollView
{
if (page == self.currentPage)
return;
// add child pages as necessary
[self addChildIfNecessary:page toScrollView:scrollView];
[self addChildIfNecessary:(page-1) toScrollView:scrollView];
[self addChildIfNecessary:(page+1) toScrollView:scrollView];
// find any pages that need removing
NSMutableArray *pagesToRemove = [NSMutableArray array];
for (ChildViewController *controller in self.childViewControllers)
{
if (controller.page < (page - 1) ||
controller.page > (page + 1))
{
[pagesToRemove addObject:controller];
}
}
// remove them if they need removing
for (UIViewController *controller in pagesToRemove)
{
[self removeChildController:controller];
}
// update our "current page" index
self.currentPage = page;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSInteger page = scrollView.contentOffset.x / scrollView.frame.size.width + 0.5;
[self updateChildrenViewsForPage:page forScrollView:scrollView];
}
This demonstrates the appropriate custom container calls and the handling of the scroll events. I hope this helps.