Removing a view controller from UIPageViewController - iphone

It's odd that there's no straightforward way to do this. Consider the following scenario:
You have a page view controller with 1 page.
Add another page (total 2) and scroll to it.
What I want is, when the user scrolls back to the first page, the 2nd page is now removed and deallocated, and the user can no longer swipe back to that page.
I've tried removing the view controller as a child view controller after the transition is completed, but it still lets me scroll back to the empty page (it doesn't "resize" the page view)
Is what I want to do possible?

While the answers here are all informative, there is an alternate way of handling the problem, given here:
UIPageViewController navigates to wrong page with Scroll transition style
When I first searched for an answer to this problem, the way I was wording my search wound me up at this question, and not the one I've just linked to, so I felt obligated to post an answer linking to this other question, now that I've found it, and also elaborating a little bit.
The problem is described pretty well by matt here:
This is actually a bug in UIPageViewController. It occurs only with the scroll style (UIPageViewControllerTransitionStyleScroll) and only
after calling setViewControllers:direction:animated:completion: with
animated:YES. Thus there are two workarounds:
Don't use UIPageViewControllerTransitionStyleScroll.
Or, if you call setViewControllers:direction:animated:completion:, use
only animated:NO.
To see the bug clearly, call
setViewControllers:direction:animated:completion: and then, in the
interface (as user), navigate left (back) to the preceding page
manually. You will navigate back to the wrong page: not the preceding
page at all, but the page you were on when
setViewControllers:direction:animated:completion: was called.
The reason for the bug appears to be that, when using the scroll
style, UIPageViewController does some sort of internal caching. Thus,
after the call to setViewControllers:direction:animated:completion:,
it fails to clear its internal cache. It thinks it knows what the
preceding page is. Thus, when the user navigates leftward to the
preceding page, UIPageViewController fails to call the dataSource
method pageViewController:viewControllerBeforeViewController:, or
calls it with the wrong current view controller.
This is a good description, not quite the problem noted in this question but very close. Note the line about if you do setViewControllers with animated:NO you will force the UIPageViewController to re-query its data source next time the user pans with a gesture, as it no longer "knows where it is" or what view controllers are next to its current view controller.
However, this didn't work for me because there were times when I need to programmatically move the PageView around with an animation.
So, my first thought was to call setViewControllers with an animation, and then in the completion block call the method again with whatever view controller was now showing, but with no animation. So the user can pan, fine, but then we call the method again to get the page view to reset.
Unfortunately when I tried that I started getting strange "assertion errors" from the page view controller. They look something like this:
*** Assertion failure in -[UIPageViewController queuingScrollView: ...
Not knowing exactly why this was happening, I backtracked and eventually started using Jai's answer as a solution, creating an entirely new UIPageViewController, pushing it onto a UINavigationController, then popping out the old one. Gross, but it works--mostly. I have been finding I'm still getting occasional Assertion Failures from the UIPageViewController, like this one:
*** Assertion failure in -[UIPageViewController queuingScrollView:didEndManualScroll:toRevealView:direction:animated:didFinish:didComplete:],
/SourceCache/UIKit_Sim/UIKit-2380.17/UIPageViewController.m:1820 $1 =
154507824 No view controller managing visible view >
And the app crashes. Why? Well, searching, I found this other question that I mentioned up top, and particularly the accepted answer which advocates my original idea, of simply calling setViewControllers: animated:YES and then as soon as it completes calling setViewControllers: animated:NO with the same view controllers to reset the UIPageViewController, but it had the missing element: calling that code back on the main queue! Here's the code:
__weak YourSelfClass *blocksafeSelf = self;
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:^(BOOL finished){
if(finished)
{
dispatch_async(dispatch_get_main_queue(), ^{
[blocksafeSelf.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:NULL];// bug fix for uipageview controller
});
}
}];
Wow! The only reason this actually made sense to me is because I have watched the the WWDC 2012 Session 211, Building Concurrent User Interfaces on iOS (available here with a dev account). I recall now that attempting to modify data source objects that UIKit objects (like UIPageViewController) depend on, and doing it on a secondary queue, can cause some nasty crashes.
What I have never seen particularly documented, but must now assume to be the case and read up on, is that the completion block for an animation is performed on a secondary queue, not the main one. So the reason why UIPageViewController was squawking and giving assertion failures, both when I originally attempted to call setViewControllers animated:NO in the completion block of setViewControllers animated:YES and also now that I am simply using a UINavigationController to push on a new UIPageViewController (but doing it, again, in the completion block of setViewControllers animated:YES) is because it's all happening on that secondary queue.
That's why that piece of code up there works perfectly, because you come from the animation completion block and send it back over to the main queue so you don't cross the streams with UIKit. Brilliant.
Anyway, wanted to share this journey, in case anyone runs across this problem.
EDIT: Swift version here, if anyone's interested.

Concluding Matt Mc's great answer, the following method could be added to a subclass of UIPageViewController, that way allowing the usage of setViewControllers:direction:animated:completion: as it was intended to be used if the bug would not be present.
- (void) setViewControllers:(NSArray*)viewControllers direction:(UIPageViewControllerNavigationDirection)direction animated:(BOOL)animated completion:(void (^)(BOOL))completion {
if (!animated) {
[super setViewControllers:viewControllers direction:direction animated:NO completion:completion];
return;
}
[super setViewControllers:viewControllers direction:direction animated:YES completion:^(BOOL finished){
if (finished) {
dispatch_async(dispatch_get_main_queue(), ^{
[super setViewControllers:viewControllers direction:direction animated:NO completion:completion];
});
} else {
if (completion != NULL) {
completion(finished);
}
}
}];
}
Now, simply call setViewControllers:direction:animated:completion: on the class/subclasses implementing this method, and it should work as expected.

maq is right. If you are using the scrolling transition, removing a child view controller from the UIPageViewController does not prevent the deleted "page" from returning on-screen if the user navigates to it. If you're interested, here's how I removed the child view controller from the UIPageViewController.
// deleteVC is a child view controller of the UIPageViewController
[deleteVC willMoveToParentViewController:nil];
[deleteVC.view removeFromSuperview];
[deleteVC removeFromParentViewController];
View controller deleteVC is removed from the childViewControllers property of the UIPageViewController, but still appears on-screen if the user navigates to it.
Until someone smarter than me finds an elegant solution, here's a work around (it's a hack--so you have to ask yourself if you really need to remove pages from a UIPageViewController).
These instructions assume that only one page is displayed at a time.
After the user taps a button indicating that she would like to delete the page, navigate to the next or previous page using the setViewControllers:direction:animated:completion: method. Of course, you then need to delete the page's content from your data model.
Next (and here's the hack), create and configure a brand new UIPageViewController and load it in the foreground (i.e., in front of the other UIPageViewController). Make sure that the new UIPageViewController starts off displaying the exact same page that was previously displayed. Your new UIPageViewController will fetch fresh view controllers from the data source.
Finally, unload and destroy the UIPageViewController that's in the background.
Anyway, maq asked a really good question. Unfortunately, I don't have enough reputation points to up vote the question. Ah, dare to dream... someday I will have 15 reputation points.

I'll put this answer here just for my own future reference and if it helps anyone - what I ended up doing was:
Delete the page and advance to the next page
In the completion block of setViewControllers, I created/init'ed a new UIPageViewController with the modified data (item removed), and pushed it without animating, so nothing changes on-screen (my UIPageViewController is contained within a UINavigationController)
After pushing the new UIPageViewController, get a copy of the viewControllers array of the UINavigationController, remove the second-to-last view controller (which is the old UIPageViewController)
There is no step 4 - done!

I am just learning this myself, so take with a grain of salt, but from what I understand, you need to change the datasource of the pageviewcontroller, not remove the viewcontroller. How many pages are shown in a pageviewcontroller is determined by its datasource, not the viewcontrollers.

For improve this. You should detect whether pageView is scrolling or not before setViewControllers.
var isScrolling = false
func viewDidLoad() {
...
for v in view.subviews{
if v.isKindOfClass(UIScrollView) {
(v as! UIScrollView).delegate = self
}
}
}
func scrollViewWillBeginDragging(scrollView: UIScrollView){
isScrolling = true
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView){
isScrolling = false
}
func jumpToVC{
if isScrolling { //you should not jump out when scrolling
return
}
setViewControllers([vc], direction:direction, animated:true, completion:{[unowned self] (succ) -> Void in
if succ {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.setViewControllers([vc], direction:direction, animated:false, completion:nil)
})
}
})
}

This problem exists when you are trying to change viewControllers during swipe gesture animation between viewControllers. To solve this problem, i made a simple flag to discover when page view controller is during transition.
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers {
self.pageViewControllerTransitionInProgress = YES;
}
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed {
self.pageViewControllerTransitionInProgress = NO;
}
And when i am trying to chenge view controller i am checking if there is transition in progress.
- (void)setCurrentPage:(NSString *)newCurrentPageId animated:(BOOL)animated {
if (self.pageViewControllerTransitionInProgress) {
return;
}
[self.pageContentViewController setViewControllers:#[pageDetails] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:^(BOOL finished) {
}
}

Actually I think I solved the issue. This is what was happening in my app:
UIViewController subclass instance was removed from UIPageViewController by removing it from model and setting viewControllers property of UIPageViewController to different ViewController. This is enough to do the job. No need to do any controller containment code on that controller
The ViewController was gone, but I could still scroll to it by swiping right in my case.
My issue was this. I was adding a custom gesture recognizer to ViewController displayed by UIPageViewController. This recognizer was hold as a strong property on the controller that also owned UIPageViewController.
FIX: before loosing access to the ViewController being dismissed I made sure I properly cleaned all the memory it uses(dealloc) and removed the gesture recognizer
Your mileage may vary, but I see no point for this solution to be wrong, when something's not working I first suspect my code :)

I had a similar situation where I wanted the user to be able to "tap and delete" any page from the UIPageViewController. After playing a bit with it, I found a simpler solution than the ones described above:
Capture the page index of the "dying page".
Check to see if the "dying page" is the last page.
This is important because if it is the last page, we need to scroll left (if we have pages "A B C" and delete C, we will scroll to B).
If it is not the last page, we will scroll right (if we have pages "A B C" and delete B, we will scroll to C).
Make a temporary jump to a "safe" place (ideally the final one). Use animated: NO to have this happen instantaneously.
UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
[self setViewControllers:#[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];
Delete the selected page from the model/datasource.
Now, if it is the last page:
Adjust your model to select the page on the left.
Get the viewcontroller on the left, note that THIS IS THE SAME ONE than the one you retrieved in step 3.
It is important to do this AFTER you have deleted the page from the datasource because it will refresh it.
Jump to it, this time with animated: YES.
jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
[self setViewControllers:#[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil];
In the case where it was NOT the last page:
Adjust your model to select the page on the right.
Get the viewcontroller on the right, note that this is not the one you retrieved in step 3. In my case, you will see that it is the one at dyingPageIndex, because the dying page has already been removed from the model.
Again, it is important to do this AFTER you have deleted the page from the datasource because it will refresh it.
Jump to it, this time with animated: YES.
jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex;
[self setViewControllers:#[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
That's it! This works well in XCode 6.1.1.
Full code below. This code is in my UIPageViewController and is called through a delegate from the page to be deleted. In my case, deleting the first page was not allowed as it contains different things from the rest of the pages. Of course, you need to substitute:
YourUIViewController: with the class of your individual pages.
YourTotalNumberOfPagesFromModel: with the total number of pages in your model
[YourModel deletePage:] with the code to delete the dying page from your model
- (void)deleteAViewController:(id)sender {
YourUIViewController *dyingGroup = (YourUIViewController *)sender;
NSUInteger dyingPageIndex = dyingGroup.pageIndex;
// Check to see if we are in the last page as it's a special case.
BOOL isTheLastPage = (dyingPageIndex >= YourTotalNumberOfPagesFromModel.count);
// Make a temporary jump back - make sure to use animated:NO to have it jump instantly
UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
[self setViewControllers:#[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];
// Now delete the selected group from the model, setting the target
[YourModel deletePage:dyingPageIndex];
if (isTheLastPage) {
// Now jump to the definitive controller. In this case, it's the same one, we're just reloading it to refresh the data source.
// This time we're using animated:YES
jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
[self setViewControllers:#[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil];
} else {
// Now jump to the definitive controller. This reloads the data source. This time we're using animated:YES
jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex];
[self setViewControllers:#[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
}
}

NSMutableArray *mArray = [[NSMutableArray alloc] initWithArray:self.userArray];
[mArray removeObject:userToBlock];
self.userArray = mArray;
UIViewController *startingViewController = [self viewControllerAtIndex:atIndex-1];
NSArray *viewControllers = #[startingViewController];
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];
I didn't quite have time to read through the above comments but this worked for me. Basically I remove the data (in my case a user) and then move to the page before it. Works like a charm. Hope this helps those looking for a quick fix.

It did it the following in Swift 3.0
fileprivate var isAnimated:Bool = false
override func setViewControllers(_ viewControllers: [UIViewController]?, direction: UIPageViewControllerNavigationDirection, animated: Bool, completion: ((Bool) -> Void)? = nil) {
if self.isAnimated {
delay(0.5, closure: {
self.setViewControllers(viewControllers, direction: direction, animated: animated, completion: completion)
})
}else {
super.setViewControllers(viewControllers, direction: direction, animated: animated, completion: completion)
}
}
extension SliderViewController:UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
self.isAnimated = true
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
self.isAnimated = finished
}
}
And here the delay function
func delay(_ delay:Double, closure:#escaping ()->()) {
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
}

we are getting crash on tab change .So one solution i got is didChangeSelect of tabs disable the userintraction.

Related

How to identify that an UIViewController is presented

I have created an UIViewController sub class which can either be pushed in a navigation stack of a UINavigationController or presented(modally) from any UIViewController. I need to identify whether my view controller is presented, if it is presented I need to add a toolbar with a close button at the top of the view controller. (else if it is pushed in navigation stack then the default close button will get added, by using that the user can go back.)
In all the available versions say 4.3, 5.0, till 6.0, from inside an UIViewController sub class, Can I assume that the view controller is presented(modally) if the following condition is satisfied.
if(self.parentViewController == nil || self.navigationController == nil)
With iOS 5, UIViewController gained a readonly property named presentingViewController, that replaces the older semantics of parentViewController (which now describes containment). This property can be used when a view controller needs to get at the view controller that’s presenting it — note: this will often be something else than what you’d expect, if you’re new to the API!
In addition, the isBeingPresented property had been introduced to pretty much solve the class of situations you’re currently in. Check for this property in your view controller’s viewWillAppear:.
Update
I overread that you seem to target iOS 4.3 as well:
In that case, you need to guard the call to isBeingPresented with an if ([self respondsToSelector:…]) you can then in the else block check for whether the parentViewController is not nil.
Another approach to backwards compatibility might be to override +resolveInstanceMethod: to add an implementation for -isBeingPresented at runtime. This will leave your calling sites clean, and you’d get rid of runtime-magic as soon as you let go of ancient iOS support ;-)
Note, though, that there are edge cases to this, and you initial approach as well, when running on iOS <5:
The view controller can be presented contained in any other view controller—including navigation controllers. When that last case happens, you’re out of luck: parentViewController will be nil, while navigationController will not. You can try to add gobs of unwieldy code to mitigate this limitation in older iOSes…or you could just let it go.
I use the this code to check whether the UIViewController is presented.
if (uiviewcontroller.presentingViewController != nil) {
// do something
}
I had a similar case, however the view controller that I presented is wrapped in it's own navigation controller. So in that view controller when I need to determine whether or not to add the close button vs a back button, I just check the navigation controllers stack size. If the screen is presented, the stack size should be one (needs close button)... and if it is pushed using an existing navigation controller, then stack size will be larger than one (needs back button).
BOOL presented = [[self.navigationController viewControllers] count] == 1;
To handle this kind of behavior, I usually set/reset a BOOL toggling it in viewWillAppear/viewWillDisappear methods.
By the way, your test condition seems incorrect. I think you should use
if(self.parentViewController != nil || self.navigationController != nil)
Why can't you just always add the toolbar to your view controller? Is there any case the view is loaded but never presented?
In Swift on iOS 9 (or later):
if viewController.viewIfLoaded?.window != nil {
// viewController is visible
}
#saikamesh.
As you use UINavigationController to navigate your viewControllers, I think you can use topViewController (Doc here) and visibleViewController (Doc again) to reach your intention.
You mention that :
when it is pushed in navigation stack then the default close button
will get added, by using that the user can go back
If the instance of the the specific UIViewController is important, I think it better to create a shared singleton instance and provide a global presented flag:
id specificVC = [SpecificViewController sharedInstance];
if (specificVC.isPushed) {
[self.navController popToViewController:specificVC animated:YES];
}
and to check if it is presented:
if ([self.navController.visibleViewController isKindOfClass:[SpecificViewController class]]) {
// Hide or add close button
self.isPresented = YES;
}
Or, you can read the much accepted answer.
:) Hope helps.
Please check this way:
for (UIViewController*vc in [self.navigationController viewControllers]) {
if ([vc isKindOfClass: [OffersViewController class]]){ //this line also checks OffersViewController is presented or not
if(vc.isViewLoaded){
NSLog(#"Yes");
}
}
}
You could do it like this, it's fast and safe
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
// Find the top controller on the view hierarchy
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
// If the top controller it is not already presented
if (![topController isKindOfClass:[YourViewController class]]) {
// Present it
[topController presentViewController:yourViewController animated:YES completion:nil];
}
else {
// do some stuff here
}
You can at any point check whether you have a modal view controller presented or not by using the modalViewController property from your navigation controller.
Ex:
UIViewController *presentedController = self.navigationController.modalViewController;
if (presentedController) {
// At this point, you have a view controller presented from your navigation controller
if ([presentedController isKindOfClass:[controllerYouWantToCheck class]]) {
// add your toolbar/buttons/etc here
}
}
One elegant answer that I haven't seen here:
// Edit: Added 2 other modal cases
extension UIViewController {
var isModal: Bool {
return self.presentingViewController?.presentedViewController == self
|| (navigationController != nil && navigationController?.presentingViewController?.presentedViewController == navigationController)
|| tabBarController?.presentingViewController is UITabBarController
}
}
credit: based on this gist
As Martin Reed said, this is the best way
BOOL presented = [[self.navigationController viewControllers] count] == 1;
if (presented) {
[self dismissViewControllerAnimated:YES completion:^{
// do whatever you need here
}];
}
else {
[self.navigationController popViewControllerAnimated:YES];
}
If it was me I'd have a custom init method and use that when creating the vc.
vc = [[[MyUIViewControllerSubClass alloc] init] initWithToolbarAndCloseButton:YES];
Small modification in #AmitaiB answer to create a function,
func isModallyPresented(tmpVC:UIViewController) -> Bool {
return tmpVC.presentingViewController?.presentedViewController == tmpVC
|| (tmpVC.navigationController != nil && tmpVC.navigationController?.presentingViewController?.presentedViewController == tmpVC.navigationController)
|| tmpVC.tabBarController?.presentingViewController is UITabBarController
}
Just check by calling:
if(isModallyPresented(tmpVC:myTopVC)){
//return true if viewcontroller is presented
}

presentModalViewController is not working?

I am making an application using two view controlers. When I am working on my first view I have posibility to go to another view using button "Settings" and method conected to this button looks like this:
-(IBAction)Settings:(id)sender{
[self presentModalViewController:settingsHandle animated:YES];
settingsHandle is an object of a second view class which is alloceted when the first view is loaded.
My problem starts while I am in a second view and i call method which include NSTimer object. This method is working during hmmm lets say 30 sec but it also can be 5 min, the result of this method is calling onother view the 3rd one.
Everything goes fine while am waiting for the result of this function in a second view.The result is that i am in a 3rd view.
When, during the method is working I am going to the first view from the second (using [self dismissModalViewControllerAnimated:YES]; )I can see that the method has finished(using NSLOG) but [self presentModalViewController:thirdview animated:YES]; is not working, just nothing happens.
so to sum up:
Waiting for the result in a secodnview (Succes third view uploaded)
Waiting for the result in a firstview (fail nothing happens)
And my goal is to make it happens from the firstview!
You can't present a ModalViewController from a ViewController that is dismissed. So, you need to keep track of the visible ViewController and call presentModalViewController from there. In your scenario, the easiest solution would be to make your NSTimer invoke a method in your first ViewController which goes like this
- (void)presentViewController:(NSTimer *)timer
{
if(self.modalViewController == nil)
[self presentModalViewController:settingsHandle animated:YES];
else
[self.modalViewController presentViewController];
}
If you create the NSTimer in your 2. ViewController, you would of course need a reference to the first ViewController. You could just pass this reference like this
-(IBAction)Settings:(id)sender{
settingsHandle.myParentViewController = self; //You need to create this var in settingsHandle
[self presentModalViewController:settingsHandle animated:YES];
//...
}

Custom Detail Image View from Three20

I am pretty new with Three20. I have followed ray wenderlich's nice introduction to three20 and the examples within the three20 framework. When I click on a thumbnail in a thumbnail view (subclass of TTThumbsViewController) to launch a Details view, a standard Details image view (deployed by TTPhotoViewController or its super class). I would like to use my own implementation of a Details View instead of the default. I put the following code when I initiated the subclass of TTThumbsViewController and TTThumbsViewControllerDelegate method:
- (id)initWithDelegate:(id<TTThumbsViewControllerDelegate>)delegate {
[super initWithDelegate:delegate];
return self;
}
- (void)thumbsViewController: (TTThumbsViewController*)controller
didSelectPhoto: (id<TTPhoto>)photo {
[navigationController.pushViewController:photoDetailViewController
animated:Yes];
}
But the default TTPhotoViewController view still prevail. When I put a NSLog in the delegate method. I coud see the method was called. I think there is another delegate someone already set in TTThumViewController? Can someone recommend a way to display my detail photo view? Is there another thumbs view controller I can use? Any suggestion will be greatly appreciated.
I'm really new to all of this (coding, etc.) but I'll share what I've found. By looking up the definition of ttthumbsviewcontroller, I was able to find the following method(wrong term?):-
- (void)thumbsTableViewCell:(TTThumbsTableViewCell*)cell didSelectPhoto:(id<TTPhoto>)photo {
[_delegate thumbsViewController:self didSelectPhoto:photo];
BOOL shouldNavigate = YES;
if ([_delegate respondsToSelector:#selector(thumbsViewController:shouldNavigateToPhoto:)]) {
shouldNavigate = [_delegate thumbsViewController:self shouldNavigateToPhoto:photo];
}
if (shouldNavigate) {
NSString* URL = [self URLForPhoto:photo];
if (URL) {
TTOpenURLFromView(URL, self.view);
} else {
TTPhotoViewController* controller = [self createPhotoViewController];
controller.centerPhoto = photo;
[self.navigationController pushViewController:controller animated:YES];
}
}
}
In the else statement, I've found this calls the creation of the photoviewcontroller. By recalling this method (?) in the actual body of my own code and changing the body in the else statement I was able to add a custom detail view. Further down the definition of the ttthumbsnailviewcontroller, you can find that the creatPhotoViewController calls for an initiation of the PhotoViewController so calling that method(?) in the body of the code and initializing another view also works.
If someone can explain whether or not this is a good method of doing this (I have a feeling that is not), it would be appreciated. Also why does putting the method in the body of the code override the call there.

"Pushing the same view controller instance more than once is not supported" exception

I am using the following code to retrieve some messages and putting them into my inbox.
MyInboxVC *inboxVC=[MyInboxVC get ];
//upload all the pending messages
UINavigationController *devNavController=[[MyappMgr get]getDeveloperNavigationController ];
[devNavController pushViewController:inboxVC animated:YES];
[devNavController setNavigationBarHidden:NO];
I get the exception
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Pushing the same view controller instance more than once is not supported (<MyInboxVC: 0x1452a0>)'
What does it mean? What am I doing wrong?
I believe when you do some actions really fast this can happens too. I build something in like this:
if(![self.navigationController.topViewController isKindOfClass:[YOURCLASS class]]) {
Firstly handle the crash so it doesnt kill your app:
#try {
[self.navController pushViewController:viewController animated:NO];
} #catch (NSException * e) {
NSLog(#"Exception: %#", e);
} #finally {
//NSLog(#"finally");
}
Then if you get the error use popTo
- (void)pushViewController:(UIViewController *)viewController {
if (viewController) {
#try {
[self.navController pushViewController:viewController 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
[self.navController popToViewController:viewController animated:NO];
} else {
NSLog(#"ERROR:UNHANDLED EXCEPTION TYPE:%#", ex);
}
} #finally {
//NSLog(#"finally");
}
} else {
NSLog(#"ERROR:pushViewController: viewController is nil");
}
}
It means that the ViewController returned from [MyInboxVC get] is already in the navigation stack of devNavController. You can not add the same object to the stack multiple times.
Apparently, you already have a MyInboxVC pushed earlier. Insure that you've popped it when it was no longer needed.
That's the "what's it mean" answer, but don't have enough info to know what you need to do to fix it.
My guess is your Navigation Stack is growing larger than you are expecting, meaning you are not popping as often as you should.
Are you performing this as part of a segue? If you are, there is no need to push a VC onto your Navigation Controller because the segue will do it already. That is why your error is occurring - you are pushing a VC that is already on the stack of the NavController.
It means you are pushing the same viewcontroller object to stack again when it's already in there.
[self.navigationController pushViewController:viewControllerObj animated:NO];
[self.navigationController pushViewController:viewControllerObj animated:NO];
check if u r pushing inside a loop or if u've accidentally placed the code more than one time..
The Main Reason for this problem, obviously if the code that pushed the view controller is called more than once. This could occur for many reasons, most common mistake when a callback method is triggered from a background thread, where this method could be executed more than once while it is still pushing the view controller.
Example:
Calling a service api on background thread when tapping a button, which will allow you to press the button more than once, and therefore the callback which pushes the view controller get called more than once.
#Melvin and #Sam solution is valid as long as you do not want to fix the original problem by not pushing more than once.
This is an expected behavior of UINavigationController where an exception is thrown when trying to push a view controller which is already present in the stack (Its there from iOS 2.2).
This was happening to me on a bar button click happening too fast, and was hard to reproduce, unless you went nuts on the button taps. The following fixed it by disabling the the button, starting the nav push, then enabling the button on the main thread (because it would be called after animation from the push occurred).
- (void)showMore
{
self.navigationItem.leftBarButtonItem.enabled = NO;
[self.navigationController pushViewController:moreVC animated:YES];
[self.navigationItem.leftBarButtonItem performSelectorOnMainThread:#selector(setEnabled:) withObject:#(YES) waitUntilDone:NO];
}
Make sure you are not adding the view controller twice in the navigation stack.
Eg - in below example self.mainViewC is pushed twice because it is initially instantiated in the navController, and is then pushed onto the navController again in the last line, which would cause this issue.
navController=[[UINavigationController alloc] initWithRootViewController:self.mainViewC];
self.window.rootViewController = navController;
[self.window makeKeyAndVisible];
[navController pushViewController:self.mainViewC animated:NO];
In this case mainViewC has already been added to stack when initWithRootViewController was written. There is no need of pushViewController again.
In my case i was pushing a viewcontroller, but then also trying to clear the navigation stack so that there was no vc's to pop to after this new VC had shown
self.show(viewController, sender: nil)
if clearNavigationStack {
self.navigationController?.viewControllers = [viewcontroller]
}
you cant do this directly after pushing a viewcontroller, you will need to wait till the viewcontroller has fully shown before trying to reset the navigation stack
Another option that I have experienced is that [MyInboxVC get ] is not returning an instance of a MyInboxVC object at all. A tell tale sign of this would be that the error is saying 'Pushing the same view controller instance more than once is not supported (notTheInboxVC: 0x9e31660)' ie. the class being pushed more than once is not the MyInboxVC expected (a fall through from MyInboxVC not being allocated)
I fixed the same issue (Swift 4) with IB segue using :
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
return navigationController?.topViewController is MainController ? true : false
}
In my case, I was pushing view controller and then trying to set array of navigation stack view controllers immediately after that. So it resulted in random crashes on transition (sometimes it crashed, sometimes it did not):
navigationController?.pushViewController(newViewController, animated: true)
navigationController?.viewControllers = [newViewController]
What I needed to do, is to use single line instead of those two (https://developer.apple.com/documentation/uikit/uinavigationcontroller/1621861-setviewcontrollers):
navigationController?.setViewControllers([newViewController], animated: true)
[devNavController pushViewController:inboxVC animated:NO];
Set animated as NO

Reload the view in iphone (in viewWillAppear)

I got a little app that has a button whose click is handled via
- (IBAction)click:(id)sender { }
Now, what I want is after click() runs, I want the view to refresh/reload itself, so that viewWillAppear() is re-called automatically. Basically how the view originally appears.
Of course I can call viewWillAppear manually, but was wondering if I can get the framework to do it for me?
viewWillAppear is where to put code for when your view will appear, so it is more appropriate to put code that will be called repeatedly into another method like -resetView, which can then be called by both viewWillAppear and your click method. You can then call setNeedsDisplay from within resetView.
-(void)resetView
{
//reset your view components.
[self.view setNeedsDisplay];
}
-(void)viewWillAppear
{
[self resetView];
}
- (IBAction)click:(id)sender
{
[self resetView];
}