Swift segue - Presenting view controllers on detached view controllers is discouraged - ios

I want to open "Select city" controller, when selectedCityId is 0 (Undefined) - to select a city. In my root view controller (MainVC) in viewDidLoad():
let selectedCityId = NSUserDefaults.standardUserDefaults().integerForKey("selectedCityId")
if(selectedCityId == 0){
self.performSegueWithIdentifier("startupSegue", sender: self)
} else {
....
}
So the startupSegue opens settingsViewController (SettingsVC) modally! Both - MainVC and SettingsVC have embed in Navigation View Controller.
There are 2 warnings:
Presenting view controllers on detached view controllers is
discouraged "myapp.MainVC: 0x7fee8871e670".
Unbalanced calls to begin/end appearance transitions for
"UINavigationController: 0x7fee8872c950".
Thanks for answers.

I had a same warning and fixed it by moving code to -viewDidAppear. One thing you should be aware is that -viewDidLoad is called only once while -viewDidAppear maybe called multiple times if page becomes visible so you need to add additional mechanism to skip performing segue again.

Moving code to -viewDidAppear is a good idea but I would also suggest to perform your seque call from the main thread:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.performSegueWithIdentifier("startupSegue", sender: self)
})
Swift 3
DispatchQueue.main.async
{
self.performSegue(withIdentifier: "startupSegue", sender: self)
}

Related

Close a viewcontroller after a segue

What I want is to close a viewController after performing a segue so that the back button of the navigation controller on the new view doesn't go back to the view that I just closed, but it goes to the view that precedes it in the storyboard like it is the first time that it is loaded.
I already tried stuff like dismiss and so but it doesn't really work for me as it only closes the view in which the button that I pressed for performing the function is located :
#objc func goToList(){
self.dismiss(animated: true, completion: nil)
performSegue(withIdentifier: "goToList", sender: nil)
}
The navigation controller maintains a stack (array) of ViewControllers that have been opened (pushed). It also has the ability to pop these ViewControllers off the stack until it gets to a specific one.
For example, if you wished to return to a previous view controller of type MyInitialVC then you'd want to search through the stack until you found that type of VC, and then pop to it:
let targetVC = navigationController?.viewControllers.first(where: {$0 is MyInitialVC})
if let targetVC = targetVC {
navigationController?.popToViewController(targetVC, animated: true)
}
NB. written from memory without XCode, so you may need to correct minor typos
You can use unwind segue to get back to each viewController that you want.
Read more here:
Unwind Segues Step-by-Step (and 4 Reasons to Use Them)

IOS swift UIBarButtonItem action

I've a table view with navigation controller embedded in. I've added a UIBarButtonItem (add) button. When I click this button it opens a new view where user enters the data and submits it (which makes a web service call) and returns back to the previous view. This navigation happens as shown below,
func addTapped(_ sender:UIBarButtonItem) {
print("Called Add")
let vc = (storyboard?.instantiateViewController( withIdentifier: "newNote")) as! newNoteVC
self.navigationController?.pushViewController(vc, animated: true)
}
And in new view I do following,
#IBAction func saveButton(_ sender: UIButton) {
if (self.noteDescription.text?.isEmpty)! {
print("Enter missing note description")
return
} else {
let desc = self.noteDescription.text
self.uploadNote(noteText: desc!, noteDate: self.dateInMilliseconds)
self.navigationController?.popViewController(animated: true)
}
}
This way a record gets saved and a view gets popped from the navigation controller stack but only thing I don't how to do is refresh the table view data in the parent view (where I might need to make a new http service call to read all the records).
I hope I'm able to explain the issue? Any help is appreciated.
As mentioned in the comments, making a service call just to update the tableview might be a overkill. However, if this is the business scenario which needs to be implemented, you can do the same in
func viewWillAppear
in the parent view controller. You can make the service call in this method and reload the table view with the data.
You would also need to check the overall navigation of the application as making service calls in viewWillAppear method is not a good approach as these gets called everytime the view is shown. For Ex: If coming back from a navigated view then also the method is called.

How should a custom segue support an unwind to a viewcontroller earlier in the call chain?

I have a storyboarded app using segues without a navigation controller. We have a simple custom segue like the code below that works fine for normal cases. However we have a situation where after invoking a chain of view controllers A->B->C we’d sometimes like C to unwind directly back to A. This works fine with the built-in transitions as UIKit seems to dismiss the intervening controller without any trouble, properly showing a transition from C back to A.
However in my custom segue I am not certain if or how I should dismiss the intervening view controllers. Assume that the code below knows both that it is unwinding and that the destination view controller is not the one presenting the source view controller. Here is what I’ve tried:
1) If I simply leave the code as is seems to work fine! However I get errors “Unbalanced calls to begin/end appearance transitions” for view controller C.
2) If I change the code to dismiss view controller B first I see the transition to B. I don’t see any way to suppress this.
From what I've read the unbalanced calls error comes up often when you have overlapping calls to transitions. However I don't see how that could be the case here since UIKit is dismissing them (presumably). I've also tried dispatching the completion work to the main thread (to try to get it into the next run loop) but no change.
class MyCustomSegue: UIStoryboardSegue {
override func perform()
{
let sourceView = sourceViewController.view
let destView = destinationViewController.view
let unwinding = // …
// Put the destination view on top
sourceView.superview?.insertSubview(destView, aboveSubview: sourceView)
// Do the animation
UIView.animateWithDuration(1.0, animations: { complete in
} ) { (Finished) -> Void in
if unwinding {
// calling this on presenter or presentee is equivalent
self.sourceViewController.dismissViewControllerAnimated(false, completion: nil)
} else {
self.sourceViewController.presentViewController(self.destinationViewController, animated: false, completion: nil)
}
}
}
}

navigating through desired views (while skipping some views) using UINavigationBar back button

I am using a UICollectionView to show a list of images.
1)By clicking on any cell the image and some description about it is displayed in the resulting view controller(using a push segue).
2)When i swipe from the left/right edge(using PanGesture) i need to display the details of the previous/next image in the collection view.
3)But the back button of the navigation bar has to take me back to the collection view and not to the previous displayed details (shown by the PanGesture).
I know how to get 1 and 2 done but don't have a concrete idea to get the 3rd job done.
Any help would be appreciated.
You can find your desired UIViewController in your navigation stack using for loop. Try this. This is in Swift
for (var i = 0; i < self.navigationController?.viewControllers.count; i++) {
if (self.navigationController?.viewControllers[i].isKindOfClass(YourViewController) == true) {
println("is sw \(self.navigationController!.viewControllers[i])")
(self.navigationController!.viewControllers[i] as! YourViewController)
self.navigationController?.popToViewController(self.navigationController!.viewControllers[i] as! YourViewController, animated: true)
break;
}
When you navigate from one view to another (e.g. by showing a detail view of your images one after the other, then all these views are internally piled up as a stack.
Thus, if you want to jump directly to a view somewhere in between this stack (e.g. the collection view), then you can use the Unwind Segue.
In your case it should work like this:
First, in your collection view (i.e. your back button destination) you need to implement a UIStoryboard Segue as follows
#IBAction func myGoBackPoint(segue: UIStoryboardSegue) {
println("Jump directly back here from any other view")
}
Then, in the storyboard of your detail view you ctrl-drag directly to top rightmost Exit marker and choose the previously created back button destination:
In the code of the detail view implement the "go back instruction"
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "My Unwind Segue" {
if let myUnwindSegue = segue.destinationViewController as? MyCollectionViewController {
// prepare for segue
}
}
}
Hope this helps.
Finally after researching a lot i found that using a collection view with each cell taking up the whole screen is the best way to go about solving this issue.
Thankyou

How to detect if view controller is being popped of from the navigation controller?

I currently need to implement some code when the top view controller is being popped off from my navigation controller. Is there a way to detect when the view controller is being popped off the navigation controller stack?
As much as possible I want to stay away from using viewWillDisappear or viewDidDisappear because I'm using a splitview in my project, and selecting a different row in the master view will also trigger the viewWillDisappear/viewDidDisappear methods.
You can detect whether a view is being popped using the isMovingFromParentViewController property for a view controller as shown below:
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if ([self isMovingFromParentViewController])
{
NSLog(#"View controller was popped");
}
else
{
NSLog(#"New view controller was pushed");
}
}
isMovingFromParentViewController
Returns a Boolean value that indicates that the view controller is in
the process of being removed from its parent.
UPDATE 2015-04-30
Based on phatmann's feedback (first comment below), I was curious if something had changed since I answer this question over a year ago. I put together a simple, example app, and have some results that are interesting.
Option 1, example
https://github.com/greymouser/TestNVC
I don't have the ability to easily test pre-8.x, so I'm not sure if something has changed since then. However, the behavior I originally described does still happen. However, thanks to puting together the test app, I did notice an oddity I didn't before.
If I just rely on {will,did}MoveToParentViewController, I noticed a spurious didMoveToParentViewController: call when pushing the first non-rootVC, on the rootVC, with parent != nil (implying it is added, not being removed). I didn't encounter this around the time of my original answer, as I usually have "permanent" rootVC's on my NVC's, and hadn't implemented the callbacks there. See the example app with logging set to LOG_WILL_DID_MTPVC (in ViewController.m). This is an -- edited for space -- snapshot of what I saw:
TestNVC[] -[vc(rootVC) willMoveToParentViewController [entering]
TestNVC[] -[vc(rootVC) didMoveToParentViewController [entering]
TestNVC[] -[vc(1) willMoveToParentViewController [entering]
TestNVC[] -[vc(rootVC) didMoveToParentViewController [entering] # <-- this is odd
TestNVC[] -[vc(1) didMoveToParentViewController [entering]
...
My original answer suggested using {will,did}MoveToParentViewController alone, as it was a "one stop shop" to handle this behavior. However, now that I've seen the spurious call to the rootVC, I suggest a mix of {will,did}MoveToParentViewController as well as the standard UINavigationControllerDelegate callbacks. For this behavior in the example app, set logging to LOG_WILL_DID_MTPVC_LEAVING_AND_NVC_WILL_DID_SHOW_VC. Now we see the following:
TestNVC[] -[nvcD willShowViewController]: rootVC
TestNVC[] -[nvcD didShowViewController]: rootVC
TestNVC[] -[nvcD willShowViewController]: 1
TestNVC[] -[nvcD didShowViewController]: 1
TestNVC[] -[nvcD willShowViewController]: 2
TestNVC[] -[nvcD didShowViewController]: 2
TestNVC[] -[vc(2) willMoveToParentViewController [leaving]
TestNVC[] -[nvcD willShowViewController]: 1
TestNVC[] -[vc(2) didMoveToParentViewController [leaving]
TestNVC[] -[nvcD didShowViewController]: 1
TestNVC[] -[vc(1) willMoveToParentViewController [leaving]
TestNVC[] -[nvcD willShowViewController]: rootVC
TestNVC[] -[vc(1) didMoveToParentViewController [leaving]
TestNVC[] -[nvcD didShowViewController]: rootVC
... and this makes much more sense now.
Option 2
Another option I didn't explore is using your NVC sublcass, overriding - pushViewController:animated: and - popViewControllerAnimated:, and applying whatever behaviors you want to the VC being pushed, or the VC that was returned from the pop. (Make sure to remember to call super in your overrides if you attempt this.)
Update summary
So, thanks to phatmann for the chance to readdress this. I think my answer is more correct now. However, I'm not so sure that it was ever "fully non-truthy". ;-)
ORIGINAL
If the exact behavior you described is what you are looking for, then override the following on your child view controller:
- (void)willMoveToParentViewController:(UIViewController *)parent;
- (void)didMoveToParentViewController:(UIViewController *)parent;
willMoveToParentViewController: will get called with parent != nil when entering, and parent == nil when leaving. didMoveToParentViewController: will always have parent != nil.
Sometimes, viewDidDisappear may make sense. However, if you're truly looking for push and pop from the parent container view controller, those methods above are what you want.
For Swift Users (Swift 3 - 4.2):
I wanted to detect when the view controller is being popped from the stack, so I wasn't able to use viewWillDisappear or viewDidDisappear callbacks, since these callbacks will be called when the view controller is not visible anymore, and not when it's popped from the stack.
but you can use the navigation controller Delegates UINavigationControllerDelegate by doing the below:
let your controller conform to UINavigationControllerDelegate:
class ViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.delegate = self
}
}
extension ViewController : UINavigationControllerDelegate {
override func willMove(toParent parent: UIViewController?) {
/*You can detect here when the viewcontroller is being popped*/
}
}
hope this helps, goodluck
If you don't need to know before the view controller is removed, and just need to know it has been popped, you can also use deinit.
class ViewController: UIViewController {
deinit {
// View controller has been popped/dismissed and it's being released
}
}
This method works well to notify coordinators or other delegates.
My experience with iOS 13 is that the property value of isMovingFromParent is not always consistent. When you have search controller being in active mode (search text field is tapped), back to parent view will have false value for this property.
Here is my way to determine if a view is from parent or not:
class MyBaseViewController: UIViewController {
private var _isPushedToAnotherView = false
var isPushedToAnotherView: Bool {
return _isPushedToAnotherView
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
...
_isPushedToAnotherView = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
...
_isPushedToAnotherView = false
}
...
}
class MyExtendedClass: MyBaseViewController {
...
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
...
if !isPushedToAnotherView {
// clear resources hold by this class
}
}

Resources