App crashing when instantiating ViewController from different storyboard - swift

I'm writing in Swift 3 (latest Xcode)
I'm controlling, if the user is logged in (async tasks checking parameters between device and database).
If the response says, that the device is invalid, I'm showing the login screen. Looks like this:
extension UIViewController {
func forceLogin() {
let storyboard = UIStoryboard(name: "Login", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "LoginViewController")
let navController = UINavigationController(rootViewController: controller)
self.present(navController, animated: true, completion: nil)
}
}
Login storyboard looks just like this.
When user successfully logs in, the function starts:
DispatchQueue.main.async {
self.dismiss(animated: true, completion: nil)
}
The dismissal may happen in first and second ViewController.
Imagine the situation:
User logs in and comes back to main application after dismissing 2nd VC
Device is being deleted from DB
After check, user needs to log in again
forceLogin() and...
libc++abi.dylib: terminating with uncaught exception of type NSException
It happens when self.present(navController, animated: true, completion: nil) is used.
I have some ideas what may cause the crash but I'm not sure:
Creating navigation controller in code instead in storyboard
Dismiss is not enough - storyboard somehow stays in memory and can't be instantiated again
What can cause the problem and how can I avoid crashing?
If any more info is necessary, please ask.

I always forget this. Remeber to place your interface tasks like this:
DispatchQueue.main.async {
self.present(navController, animated: true, completion: nil)
}

Related

Navigation after google sign in | UIKit

I am trying to Navigate to my main content after integration google sign in a separate Login Page. Before creating the login page, my app's entry point was the Navigation controller.. Now I needed users to login before that so I added a View Controller with google sign in button, and the sign in is working fine. this is my storyboard:
Now I want if the sign in succeed, to display the app's main content view (which is as if the entry point is the Navigation Controller from now on). I tried the following approach:
#IBAction func signIn(_ sender: Any) {
let signInConfig = GIDConfiguration.init(clientID: "xyz.apps.googleusercontent.com")
GIDSignIn.sharedInstance.signIn(with: signInConfig, presenting: self) { user, error in
guard error == nil else { return }
// If sign in succeeded, display the app's main content View.
let vc = self.storyboard?.instantiateViewController(withIdentifier: "scanReceiptsViewController") as! ViewController
self.navigationController?.pushViewController(vc, animated: true)
self.present(vc, animated: true, completion: nil)
}
}
There are 2 problems:
the new viewcontroller is presented modally, users can very easily go back to the login page and it distrubs the flow of my app
user will need to login everytime to navigate to the main content view controller, I want that if signed in the entry point becomes the main storyboard from now on. if that makes sense
Thank you and sorry I'm new into learning swift!
I would recommend that you keep your navigation controller (I will refer to it from now on as ScanRecipientsViewController) as the default app entry point, since users only need to see the login screen if they are not logged in, which is most of the time not the case. This will give us the benefit of a slightly faster and smoother performance at the app start when the user is logged in.
You could handle this case in your AppDelegate's func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool method. For example, check if the user is logged in, if not, set your LoginController as the new root controller, which will replace your ScanRecipientsViewController that was set as root by the storyboard.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if GIDSignIn.sharedInstance.currentUser == nil { // just a guess, please refer to the documentation on how to check if the user is logged in
let storyboard = UIStoryboard(name: "Login", bundle: nil)
/*
If you use the SceneDelegate in your project, you will need to retrieve the window object from it instead of from the AppDelegate.
let window = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window
*/
window?.rootViewController = storyboard.instantiateViewController(withIdentifier: "LoginController")
}
return true
}
After the user successfully logs in, you can create a new instance of your ScanRecipientsViewController and set it as the root controller back again. You can do this by e.g. creating a public method in your AppDelegate:
extension AppDelegate {
func openRecipientsController() {
let storyboard = UIStoryboard(name: "Home", bundle: nil)
/*
If you use the SceneDelegate in your project, you will need to retrieve the window object from it instead of from the AppDelegate.
let window = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window
*/
window?.rootViewController = storyboard.instantiateInitialViewController()
}
}
and in your LoginViewController:
#IBAction func signIn(_ sender: Any) {
let signInConfig = GIDConfiguration.init(clientID: "xyz.apps.googleusercontent.com")
GIDSignIn.sharedInstance.signIn(with: signInConfig, presenting: self) { user, error in
guard error == nil else { return }
// If sign in succeeded, display the app's main content View.
(UIApplication.shared.delegate as? AppDelegate)?.openRecipientsController()
}
}
As a last thing, you normally don't use both pushing and presenting at the same time, you need to choose one of the two options.
let vc = self.storyboard?.instantiateViewController(withIdentifier: "scanReceiptsViewController") as! ViewController
self.navigationController?.pushViewController(vc, animated: true)
self.present(vc, animated: true, completion: nil)
I wish you lots of fun learning iOS development with Swift!

Swift - How switch between storyboards with back button?

I'm in a storyboard and I need to go to another storyboard by tapping a button. Well, this I've already done, but I need to go back to previous storyboard. Here is my code:
func goContact() {
let storyboard = UIStoryboard(name: "FAQ", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "ContactViewController")
self.navigationController?.show(vc, sender: nil)
}
By tapping a button on my UITableViewCell (where I have a protocol), this function (goContact) is called and I switch to another storyboard (FAQ). But, how can I go back to main.storyboard?
I've already tried do this too:
func goContact() {
let storyboard = UIStoryboard(name: "FAQ", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "ContactViewController")
self.navigationController?.show(vc, sender: nil)
// self.navigationController?.pushViewController(vc, animated: true)
// self.present(vc, animated: true, completion: nil)
}
Use only storyboard property , it references to Main storyboard of the app or you can write it like let storyboard = UIStoryboard(name: "Main", bundle: nil)
To go back after push do
self.navigationController?.popViewController(animated: true)
You should push your new view controller onto the navigation controller using push, like in your commented-out code. That will cause the new view controller to have a back button, which will pop it for you without needing any extra code.

How to programmatically show modal in a UINavigationController

In my app, I have a UINavigationController that i'm pushing and popping ViewControllers from. At some point, I want to show a VC modally (showing the previous controller "underneath"). I can get it to work by setting up a segue in a storyboard, however I'm in a spot where I need to do it programmatically, and I can't seem to find the right magic incantation to make it work.
I saw a couple of similar questions but they seemed to be showing the UINavigationController modally, not showing one of the VC's on the UINavigationController stack modally.
(I put up a test application here: https://github.com/SuperTango/ModalNavController, and that's where this code and images come from)
The "Manual" code does:
#IBAction func goToVC2Tapped(_ sender: Any) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let destinationViewController = storyboard.instantiateViewController(withIdentifier: "VC2ViewController") as! VC2ViewController
destinationViewController.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
self.navigationController?.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
self.navigationController?.pushViewController(destinationViewController, animated: true)
}
but it's not working (see the second transition in the gif below).
The segue that works is setup like this:
This gif is from the test app and shows how it works with the segue, but not manually.
Any ideas? Thanks!
To present modally you need to use:
present(destinationViewController, animated: true, completion: { })
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let destinationViewController = storyboard.instantiateViewController(withIdentifier: "YourVC") as! YourVC
destinationViewController.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
destinationViewController.modalTransitionStyle = .crossDissolve
self.present(destinationViewController, animated: false, completion: nil)
I think this helps show your ViewController in a modal way of presenting. Write the above code under outlet actions or where you want to present your ViewController.

App crashes when presenting view controller

I modally present a view controller, and then present an activity view controller. However when I do, the app crashes with error:
libc++abi.dylib: terminating with uncaught exception of type nsexception ios 10 "self.present"
This is what causes the app to crash:
let vc = UIActivityViewController(activityItems: [shareText], applicationActivities: [])
self.present(vc, animated: true, completion: nil)
I modally present the view controller with just a segue
self.performSegue(withIdentifier: "showContainer", sender: nil)

Swift viewWillAppear redirect

I'm using "viewWillAppear" to check UserDefaults.standard if i have data on the defaults i want redirect to Dashboard View and pass out of Login.
When i Log In correctly the app redirect normally but not in viewWillAppear in case of have data.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let defaults = UserDefaults.standard
if defaults.string(forKey: "username") != nil {
print("Exist")
print(defaults.string(forKey: "username")!)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "Dashboard")
self.present(vc, animated: true, completion: nil)
}
}
And i get this error con debug.
2016-11-04 17:44:32.130024 RHiOS[1155:278240] Warning: Attempt to present <RHiOS.MenuViewController: 0x100f0d480> on <RHiOS.ViewController: 0x100e0b880> whose view is not in the window hierarchy!
How can i redirect to Dash in case of have data? Thanks.
You cannot present a ViewController in viewWillAppear. Simply put this code in viewDidAppear and it should fix your problem.
When the view is not visible, it's not in the window hierarchy as the error mentioned. You could also create a segue in your storyboard and call performSegue(withIdentifier:sender:) instead.
You won't be able to present anything when the view hasn't appeared yet, so you either have to move your code into viewDidAppear or prevent this view from showing in the first place and redirect directly to the dashboard from a parent view controller that presents either the dashboard or login screen depending on whether or not you have data.