Using a UINavigationController in Xamarin.iOS (targetting iOS 11/12) I'd like to detect, in the destination View Controller when I'm being navigated back to - i.e. a pop. I specifically want to exclude/detect when the destination VC is being pushed for the first time.
To be explicit, given that we've navigated from A to C, and back to B:
[A] -(push)-> [B] -(push)-> [C] -(pop)-> [B]
I'd like to detect/discriminate - in B - between the initial push from A -> B and a subsequent pop from C -> B.
Conceptually this is identical to the questions posed here and here and I should - apparently - be able to use a combination of isMovingToParentViewController / IsBeingPresented / View.Window in ViewWillAppear() but, having tried the approaches in both the linked questions (and a few other things), I'm not seeing the expected values for these properties; they're always False, and View.Window is always null.
Is this a Xamarin.iOS-specific quirk, or am I missing something? If it is Xamarin-related is there a workaround? And if not does anyone have a bare-bones C# example of this detection working?
I'd prefer not to have to maintain state in the navigation controller (or delegate, or individual VCs) since the app structure could change in the future. However, a solution that made use of the nav controller or delegate to indicate the direction of navigation (pop vs push) to the destination VC - 'B', above - would be acceptable.
If you want to manipulate navigation operation, isMovingToParentViewController should work. Place it in your B controller's view will appear lifecycle event:
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
if (IsMovingToParentViewController)
{
// Come from A
}
else
{
// Pop from C
}
}
Here is the effect, I tested it on iOS 12:
Another way to detect it is:
When you want to push B from A, you have to initialize a new B for pushing so B's ViewDidLoad event will be called. When popping from C, B has existed in your navigation stack so that the ViewDidLoad won't be fired.
Related
I working on alarm clock app and Im trying to pass data from B controller (the one in the middle of picture) to the previous one (which is A) . As you can see B view controller contains UIView which is “filled” with static table view controller (C).
I can pass data back from the B to A with delegate , it works fine, but I don’t know how pass data from the C to A (using segue and delegates?). C controller is inside B controller.
I just tried simply with segue (As you can see I created segue between C and A) and delegate, but It didn’t work and I really don’t know if it’s the good idea or practice.
To call protocol method I use this code in C controller (function parameter is just for testing purposes):
override func viewWillDisappear(_ animated: Bool) {
self.delegate?.passAlarmSettings(test: 1)
}
Or maybe I should pass from C to B and then from B to A? Or is any better way to do it?
Thanks for any suggestions
In my app I have a navigation controller with four controllers. The user navigate from A -> B -> C -> D -> A_1 -> B_2 -> ... etc. It's one way and every ViewController is always a new ViewController.
This cycle is intended. But in ViewController A and C I initialize GoogleMaps which is using a lot of memory. So after 15 loops (and 30 inits of GoogleMaps) my app crashes because of a memory leak.
Now I see different possibilities to solve this problem.
1) I do not init a new ViewController rather I reuse my VCs. So Google Maps just initialize two times.
I dislike this, because my VCs could have different states. A clean init would be more comfortable and a smaller source of errors
2) I remove the stack at the right time, because the navigation is just one way. When the transition from A -> B is done I could throw A away. Same thing for C -> D.
3) I deinit Google Maps after transitions. I don't know how to do this yet but I'm quite sure that I will figure it out.
I read about setViewControllers by which I can replace view controllers.
What is the best practice? What recomments Apple?
Start with the 3rd and the easiest option:
Use override func viewWillDisappear(){} to deinit google maps. You could use this method for 2nd option as well.
But I think, you should use delegates when you go back to A from D. What do you change on A, when you reach it again?
You mentioned that navigation is just one way. If so you can just replace UINavigationController's viewControllers array. In other words, you may not need navigation stack if a user does not go backward in the stack.
Alternatively, I got a similar issue in my app. When I am pushing a controller in the UINavigationController it checks the existing viewControllers and modifies it. For example, if an identical controller is already there, pop to that copy.
You can navigate view controllers from A -> B -> C -> D -> A_1 -> B_2 -> ... etc. using two method.
Method 1:
When you are going to D controller then pop to a specific controller(A controller) and again navigate cycle A to D with the push view controller.
self.navigationController?.popToViewController(A, animated: true)
Method 2:
When you are going to D controller then the present controller (A controller) and dismiss presented cycle flow when you again reached at D controller
viewController.present(A, animated:true, completion: nil)
I have three different view controllers in my project, all with their own overridden viewWillAppear functions, with print statements to identify which view controller's function is being called when the application is running. These controllers are all under a navigation controller, and for simplicity, I'll refer to them as View Controllers A, B, and C.
However, the problem is that when I navigate from View Controller A to View Controllers B and C, then use the "back" button on the navigation bar to return to viewController A, it seems like viewControllers B and C aren't dismissed completely. I checked the navigation stack count using self.navigationController.viewControllers.count and it's properly returning count = 1 (meaning that the only view controller on the stack is A), yet if I minimize the app and bring it back to the foreground again, the viewWillAppear functions are still being called by the B and C view controllers, even though they should have been popped off the stack. Furthermore, if I go back and forth between A and B multiple times, stay at A, then minimize the app and open it up again, the viewWillAppear method will be also be called multiple times, the number of occurrences being equal to however many times I moved between the two controllers.
Note: I am using a NotificationCenter Observer to tell the controllers to call viewWillAppear/viewDidDisappear when the app comes into the foreground or goes into the background.
Here is how I'm using the observer:
override func viewWillAppear(_ animated: Bool) {
NSLog("View Controller A: viewWillAppear called")
/*
Some code here
*/
}
func willEnterForeground(){
viewWillAppear(false)
}
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: .UIApplicationWillEnterForeground, object: nil)
Sorry for the wall of text, I didn't know how to better describe this issue. This seems like a somewhat generalized problem, so I didn't post any code. If anyone has any clue on what the problem may be here, or would like me to post some more code, please let me know.
Thanks!
Update: RESOLVED. For anyone who stumbles upon this problem and happens to see this post, I left my solution in the comments section below.
I have an iPad app which has a lot of screens and a lot of segue options.
At the moment, I am simply using performSegueWithIdentifier to initiate these segues, and my fear is that I'm taking up a lot of memory as the user performs more and more segues.
I've seen that people recommend using the function popToRootViewControllerAnimated: if using a UINavigationController, but the problem is that I'm not using one.
How can I stop the number of VC's proliferating?
The way the app works, the user does constantly return to the root VC - effectively a search screen. So if I could clear the stack of VC's when such a segue is needed, that would solve my issue I think, but I have no idea how to go about this.
Thanks for any suggestions.
When you are using segues the flow moves backwards and forwards. When the user moves backwards (ie presses "back") then it will not push to a new VC but it will pop to a VC that already existed. When you pop, the current VC is removed from the stack and memory.
If you have segues to move backwards in the flow then this is wrong. You only need segues to move forward.
A PROPER PREPARE FOR SEGUE
In prepare for segue you should never create your own view controllers and push to them. The storyboard is there to do all of this for you.
A proper prepareForSegue method should look something like this...
- (void)prepareForSegue:(UIStoryBoardSegue*)segue
{
if([segue.identifier isEqualToString:"SomeSegue"])
{
MyNewViewController *controller = segue.destinationViewController;
controller.someProperty = "some value to pass in";
}
}
That is all you need. Note that you only need this if you intend to pass some information to the new view controller. If you are not passing anything forward then you don't need this method at all.
When the method ends the new VC will get pushed onto the screen by the storyboard file.
UNWIND SEGUES
If you have a random flow (like in your comment) then you can use unwind segues to achieve this.
In you 'A' view controller have a function like...
- (IBAction)someUnwindAction:(UIStoryboardSegue*)sender
{
//some action to run when unwinding.
}
It needs to receive a UIStoryboardSegue object. If set up as an IBAction you can also access it from Interface Builder.
Now when you want to go A > B > C > B > A then just used the standard push and pop (from the segue and back button).
When you want to go A > B > C > A then you can use the unwind segue from controller C.
If you have a cancel button or something in controller C and this is in the Interface Builder and this should take you back to controller A. Then in the Interface Builder underneath controller C you will have a little green square with a door and an arrow pointing out of it. Just point the action of the cancel button to this symbol and choose "someUnwindAction". (Note, unwindAction is in A, button is in C.) XCode then uses this to pop you all the way back to A and deals with removing any memory and stuff. If you want you can send additional information back to A too.
If you want to access this unwind segue from C programmatically then you can run...
[self performSegueWithIdentifier:"someUnwindAction" sender:nil];
This will also pop back to A.
imho I don't see any issues with using segues, it is much more simple than anything else. If you worry about memory consumed then just profile your app and see how much it eats and how often your "memory pressure handler" is called.
ok I believe I've figured this out. It appears to work.
Since I keep returning to a common VC, I am placing code in the places where I want to segue back to this 'root' VC to clear the VC stack.
I still perform the actual segue in my normal code
[self performSegueWithIdentifier:#"editBackToSearch" sender:self];
However in the 'prepareForSegue:' method, I no longer create a new VC object, instead I perform any work I have outstanding (save my data), and then clear the VC stack with the following code.
UIViewController *vc = self;
while ([vc presentingViewController] != NULL)
{
vc = [vc presentingViewController];
}
[vc dismissViewControllerAnimated:NO completion:nil];
It appears to run smoothly and has the desired impact (confirmed via the profiler) of releasing the memory that these VC's sucked up.
One last comment - for some reason I could not animate the dismissal command, this appeared to trigger an infinite loop and resulted with an EXC_BAD_ACCESS. With animation set to NO, it works great though.
Thanks for your help.
Swift 3: this is to prevent memory leaks in Swift 3 & 4
#IBAction func entryViewSelected(_ sender: UIButton) {
self.dismiss(animated: true, completion: nil)
}
I have an iOS app using iOS 5 and Xcode 4.3.2 that is made up of 7 view controllers. VC1 links to VC2, VC2 can link to VC3-VC7 and each of those controllers can link to each other (think of it as a side bar navigation). If I use segues the views are repeatedly added to the stack and if a user goes back and forth it can use a large amount of memory. How can I implement this navigation where I release the previous controller? They are all small controllers so loading them takes little time/processor/memory. Can I presentViewController and then release the presentingViewController somehow? Thanks.
If you implement a UINavigationController, you can use the push and pop view controller methods to go back and forth. popToViewController:animated: is described here, along with 3 other helpful methods.
Well seems like there should be no problem from VC1 to VC2. For the VC3 - VC7 you could:
Present as modalViewController instead of pushing that to the stack.
Or:
- Use the popToViewController:animated: function of your UINavigationController if the Controller is already present in the stack of controllers, otherwise push it. Like
// Assuming u need to push VC6
for(UIViewController *controller in [urNavController viewControllers]){
if([controller isKindOfClass:[VC6 class]])
{
[urNavController popToViewController:controller animated:YES];
}
else{
VC6 *VC6controller = [[VC6 alloc] init];
[urNavController pushViewController:VC6controller];
}
}
You could use UINavigationController's - (void)setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated method to remove any view controllers below the topmost one. Since the navigation controller's viewControllers array is an immutable one, you could not use any NSMutableArray's removeObject... methods directly on the viewControllers array. You would have to make a mutableCopy into a mutable array, remove any (hidden) view controllers you wish to discard from the mutable array, and pass the resulting slimmed-down stack of view controllers to the above method. Since your topmost view controller would be unchanged, there would be no transition animation in your case (see discussion below), so you could also set the viewControllers property directly without bothering with the animated: argument.
From Apple's documentation:
Discussion
You can use this method to update or replace the current view controller stack without pushing or popping each controller explicitly. In addition, this method lets you update the set of controllers without animating the changes, which might be appropriate at launch time when you want to return the navigation controller to a previous state.
If animations are enabled, this method decides which type of transition to perform based on whether the last item in the items array is already in the navigation stack. If the view controller is currently in the stack, but is not the topmost item, this method uses a pop transition; if it is the topmost item, no transition is performed. If the view controller is not on the stack, this method uses a push transition. Only one transition is performed, but when that transition finishes, the entire contents of the stack are replaced with the new view controllers. For example, if controllers A, B, and C are on the stack and you set controllers D, A, and B, this method uses a pop transition and the resulting stack contains the controllers D, A, and B.