unexpectedly found nil when refreshing UI of presentingViewController within dismissViewController callback - ios

I'm trying to reload the UI of a view (MyMatches) thats presenting another view (BuyView). When BuyView is dismissed, I want to reload all of the views of MyMatches. However, when I try to do this within the completion of "dismissViewController", I run into an "unexpectedly found nil" error on the line "let mmvc = self.presentingViewController? as! MyMatchesViewController". Does anyone know why this happens, or if there's an easier way to accomplish what I'm trying to do? Code posted below is found within BuyViewController:
func itemBought() {
print("Confirm tapped!")
BoughtController.globalController.sendSellerNotification(seller, match: match)
BoughtController.globalController.updateBuyer(self.item, buyer: LocalUser.user, match: self.match)
BoughtController.globalController.updateMarket(self.item, match: self.match)
BoughtController.globalController.updateSeller(self.item, seller: seller, soldPrice: self.match.matchedPrice)
self.cancel = false
if self.fromInfo == true {
self.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
}
else {
self.dismissViewControllerAnimated(true) {
let mmvc = self.presentingViewController as! MyMatchesViewController
mmvc.setupMatchesScrollContent()
}
}
}

Likely, the dismissViewControllerAnimated() block runs after the view controller has been dismissed and thus self.presentingViewController has changed? Perhaps. Safer to use:
else {
let mmvc = self.presentingViewController as! MyMatchesViewController
self.dismissViewControllerAnimated(true) {
mmvc.setupMatchesScrollContent()
}
}
where you 'capture' mmvc before using it in the trailing closure.

Related

how can I instantiate a viewController with a containerView and it's containerView at the same time?

I want to instantiate a viewController with a container with the following:
let vc = self.storyboard?.instantiateViewController(withIdentifier: ContainerViewController") as? ContainerViewController
I also need a reference to the containerView so I try the following:
let vc2 = vc.childViewControllers[0] as! ChildViewController
The app crashes with a 'index 0 beyond bounds for empty NSArray'
How can I instantiate the containerViewController and it's childViewController at the same time prior to loading the containerViewController?
EDIT
The use case is for AWS Cognito to go to the signInViewController when the user is not authenticated. This code is in the appDelegate:
func startPasswordAuthentication() -> AWSCognitoIdentityPasswordAuthentication {
if self.containerViewController == nil {
self.containerViewController = self.storyboard?.instantiateViewController(withIdentifier: "ContainerViewController") as? ContainerViewController
}
if self.childViewController == nil {
self.childViewController = self.containerViewController!.childViewControllers[0] as! ChildViewController
}
DispatchQueue.main.async {
self.window?.rootViewController?.present(self.containerViewController!, animated: true, completion: nil)
}
return self.childViewController!
}
The reason I am instantiating the container and returning the child is that the return needs to conform to the protocol which only the child does. I suppose I can remove the container but it has functionality that I would have wanted.
Short answer: You can't. At the time you call instantiateViewController(), a view controller's view has not yet been loaded. You need to present it to the screen somehow and then look for it's child view once it's done being displayed.
We need more info about your use-case in order to help you.
EDIT:
Ok, several things:
If your startPasswordAuthentication() function is called on the main thread, there's no reason to use DispatchQueue.main.async for the present() call.
If, on the other hand, your startPasswordAuthentication() function is called on a background thread, the call to instantiateViewController() also belongs inside a DispatchQueue.main.async block so it's performed on the main thread. In fact you might just want to put the whole body of your startPasswordAuthentication() function inside a DispatchQueue.main.async block.
Next, there is no way that your containerViewController's child view controllers will be loaded after the call to instantiateViewController(withIdentifier:). That's not how it works. You should look for the child view in the completion block of your present call.
Next, you should not be reaching into your containerViewController's view hierarchy. You should add methods to that class that let you ask for the view you are looking for, and use those.
If you are trying to write your function to synchronously return a child view controller, you can't do that either. You need to rewrite your startPasswordAuthentication() function to take a completion handler, and pass the child view controller to the completion handler
So the code might be rewritten like this:
func startPasswordAuthentication(completion: #escaping (AWSCognitoIdentityPasswordAuthentication?)->void ) {
DispatchQueue.main.async { [weak self] in
guard strongSelf = self else {
completion(nil)
return
}
if self.containerViewController == nil {
self.containerViewController = self.storyboard?.instantiateViewController(withIdentifier: "ContainerViewController") as? ContainerViewController
}
self.window?.rootViewController?.present(self.containerViewController!, animated: true, completion: {
if strongSelf == nil {
strongSelf.childViewController = self.containerViewController.getChildViewController()
}
completion(strongSelf.childViewController)
}
})
}
(That code was typed into the horrible SO editor, is totally untested, and is not meant to be copy/pasted. It likely contains errors that need to be fixed. It's only meant as a rough guide.)

How to properly unwrap UIViewController

I can't really understand how to unwrap this piece of code, I tried with if, guard and with forced unwrapping, but they all crash when the id doesn't exist. Is it possible just to make it so that it shows print() in the console and not execute any further code
guard let historyViewController = self.storyboard?.instantiateViewController(withIdentifier: "historyViewNav") else{
fatalError("No history view controller found ");
}
historyViewController.modalTransitionStyle = .flipHorizontal
present(historyViewController, animated: true, completion: nil)
I am not sure if I understand what you are trying to do. Here: fatalError("No history view controller found "); you are crashing the app if the controller can not be initialized from storyboard. If you dont want the app to crash, just use print() in your else statement. If you dont want anything else to happen there you can return afterwards:
guard let historyViewController = self.storyboard?.instantiateViewController(withIdentifier: "historyViewNav") else{
print("No history view controller found ");
return;
}
instantiateViewController(withIdentifier: "historyViewNav") does not return nil if it fails, it raises an exception. See here: link. So your guard statement does not help. Unfortunately I am not aware of straight forward way to check this in runtime, because NSExceptions are not meant to be catchable in swift, as answered here: link
However I suggest to look into the RSwift library, so you do not have to use hardcoded strings as identifiers in your code.
This works IF you put the ID in the field:
override func viewDidAppear(_ animated: Bool) {
if let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "myTestID") as? UIViewController
{
vc.modalTransitionStyle = .flipHorizontal
present(vc, animated: true, completion: nil)
}else{
print("error")
}
}

Best practice to change current view from a class

I'm working with Swift 3 and I'd like to change my view from a function in my class when login succeed.
I've got a LoginViewController which contains this function:
static let sharedInstance = LoginViewController()
//...
func showNextView() {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
guard let eventVC = storyboard.instantiateViewController(
withIdentifier: "EventsTVC") as? EventsTableViewController else {
assert(false, "Misnamed view controller")
return
}
self.present(eventVC, animated: false, completion: nil)
}
In my class APIManager, I call this function inside my asynchronous method using Alamofire:
func processAuth(_ data: [String: String]) {
print("-- auth process started")
//... defining vars
Alamofire.request(tokenPath, method: .post, parameters: tokenParams, encoding: JSONEncoding.default, headers: tokenHeader)
.responseJSON { response in
guard response.result.error == nil else {
print(response.result.error!)
return
}
guard let value = response.result.value else {
print("No string received in response when swapping data for token")
return
}
guard let result = value as? [String: Any] else {
print("No data received or data not JSON")
return
}
// -- HERE IS MY CALL
LoginViewController.sharedInstance.showNextView()
print("-- auth process ended")
}
}
My console returns this error message:
-- auth process started 2017-03-18 20:38:14.078043 Warning: Attempt to present on
whose view is not in the window
hierarchy!
-- auth process ended
I think it's not the best practice to change my view when my asynchronous method has ended.
I don't know what I've got to do. Currently, this is the process:
User opens the app and the LoginViewController is displayed, if no token is saved (Facebook Login)
In the case where it has to login, a button "Login with Facebook" is displayed
When login succeed, I send Facebook data in my processAuth() function in my APIManager class
When my API returns me the token, I saved it and change the view to EventsTVC
I put in bold where the problem is. And I would like to know if it's the best practice in my case. If so, how to avoid my error message?
I hope I made myself understood. Thanks for your help!
What actually happens is that your singleton instance LoginViewController wants to present itself while not being in the view hierarchy. Let me explain it thoroughly:
class LoginViewController: UIViewController {
static let sharedInstance = LoginViewController()
func showNextView() {
...
// presentation call
self.present(eventVC, animated: false, completion: nil)
}
In this function you are calling present() from your singleton instance on itself. You have to call it from a view which is (preferably) on top of the view hierarchy stack. The solution would probably be not using a singleton on a VC in the first place. You should be instantiating and presenting it from the VC that is currently on the screen. Hope this helps!

Swift : fatal error: unexpectedly found nil and EXC_BAD_INSTRUCTION

I m new to Xcode and i m building a project with a login button. After clicking the login with details in the text field, it will redirect to the second view which is the scroll view controller. However, i got two parts of "Error"
Normally, it work with a normal view controller(login and move to the second view). I have just recreate a view controller to a scroll view controller and it did not work.
By the way, I got a build success but i just got error when i try to "login"
Can anyone explain why i got the thread error and the fatal error?
How can i resolve the problem?
login view Controller
#IBAction func LoginBtn(sender: AnyObject) {
LoginIn()
}
func LoginIn(){
let user = PFUser()
user.username = usernameTF.text!
user.password = passwordTF.text!
PFUser.logInWithUsernameInBackground(usernameTF.text!,
password:passwordTF.text! , block: { (User: PFUser?, Error
:NSError? ) -> Void in
if Error == nil{
dispatch_async(dispatch_get_main_queue()){
let Storyboard = UIStoryboard(name: "Main", bundle: nil)
let MainVC : UIViewController = Storyboard.instantiateViewControllerWithIdentifier("scrollV") as UIViewController
self.presentViewController(MainVC, animated: true, completion: nil)
}
}
else{
NSLog("Wrong!!!!")
}
})
}
Scroll View Controller
import UIKit
class ScrollViewController: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
let V1 : ScrollViewController = ScrollViewController(nibName: "ScrollViewController", bundle: nil)
self.addChildViewController(V1)
self.scrollView!.addSubview(V1.view!)
V1.didMoveToParentViewController(self)
}
You are doing some forced unwrapping, which is not recommended (in general forced stuff in Swift is not recommended), and it will crash your application if the variable being unwrapped is nil. That's why forced unwrap is recommended only when you're 100% sure that you have a valid value in your optional.
In your case, it seems that V1.view! causes the crash, and this might be caused by the fact that V1 didn't successfully loaded from the nib.
Swift has made it very easy to work with nullable values. Just don't force unwrap and use optional binding instead.
#Cristik is correct. Instead of force unwrapping the variable, you should check to see if it is instantiated using the "if let" statement:
if let scrollView = self.scrollView {
if let subView = V1.view {
scrollView.addSubview(subView)
}
}

single function to dismiss all open view controllers

I have an app which is a single view application. I have a navigation controller linked up to all child controllers from the root view controller.
In each child controller, I have a logout button. I'm wondering if I can have a single function I can call which will dismiss all the controllers which have been open along along the way, no matter which controller was currently open when the user presses logout?
My basic start:
func tryLogout(){
self.dismissViewControllerAnimated(true, completion: nil)
let navigationController = UINavigationController(rootViewController: UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("LoginViewController") )
self.presentViewController(navigationController, animated: true, completion: nil)
}
I am looking for the most memory efficient way of carrying out this task. I will put my logout function in a separate utils file, but then I can't use self. And I still have the issue of knowing which controllers to dismiss dynamically.
Update
Pop to root view controller has been suggested. So my attempt is something like:
func tryLogout(ViewController : UIViewController){
print("do something")
dispatch_async(dispatch_get_main_queue(), {
ViewController.navigationController?.popToRootViewControllerAnimated(true)
return
})
}
Would this be the best way to achieve what I'm after?
You can call :
self.view.window!.rootViewController?.dismiss(animated: false, completion: nil)
Should dismiss all view controllers above the root view controller.
Updated answer for Swift 4 and swift 5
UIApplication.shared.keyWindow?.rootViewController?.dismiss(animated: true, completion: nil)
and when you use navigationController
self.navigationController?.popToRootViewController(animated: true)
Works for Swift 4 and Swift 5
To dismiss any unwanted residue Modal ViewControllers, I used this and worked well without retaining any navigation stack references.
UIApplication.shared.keyWindow?.rootViewController?.dismiss(animated: false, completion: nil)
self.view.window! did crash for my case possibly because its a Modal screen and had lost the reference to the window.
Swift3
navigationController?.popToRootViewControllerAnimated(true)
Take a look at how unwind segues work. Its super simple, and lets you dismiss/pop to a certain viewcontroller in the heirarchy, even if it consists of a complex navigation (nested pushed and or presented view controllers), without much code.
Here's a very good answer (by smilebot) that shows how to use unwind segues to solve your problem
https://stackoverflow.com/a/27463286/503527
To dismiss all modal Views.
Swift 5
view.window?.rootViewController?.dismiss(animated: true, completion: nil)
If you have a customed UITabbarController, then try dismiss top viewController in UITabbarController by:
class MainTabBarController: UITabBarController {
func aFuncLikeLogout() {
self.presentedViewController?.dismiss(animated: false, completion: nil)
//......
}
}
If you have access to Navigation Controller, you can try something like this. Other solutions didn't work for me.
func popAndDismissAllControllers(animated: Bool) {
var presentedController = navigationController?.presentedViewController
while presentedController != nil {
presentedController?.dismiss(animated: animated)
presentedController = presentedController?.presentedViewController
}
navigationController?.popToRootViewController(animated: animated)
}
works for Swift 3.0 +
self.view.window!.rootViewController?.dismiss(animated: true, completion: nil)
I have figured out a generic function to dismiss all presented controllers using the completion block.
extension UIWindow {
static func keyWindow() -> UIWindow? {
UIApplication.shared.windows.filter({ $0.isKeyWindow }).first
}
}
func getVisibleViewController(_ rootViewController: UIViewController?) -> UIViewController? {
var rootVC = rootViewController
if rootVC == nil {
rootVC = UIWindow.keyWindow()?.rootViewController
}
var presented = rootVC?.presentedViewController
if rootVC?.presentedViewController == nil {
if let isTab = rootVC?.isKind(of: UITabBarController.self), let isNav = rootVC?.isKind(of: UINavigationController.self) {
if !isTab && !isNav {
return rootVC
}
presented = rootVC
} else {
return rootVC
}
}
if let presented = presented {
if presented.isKind(of: UINavigationController.self) {
if let navigationController = presented as? UINavigationController {
return navigationController.viewControllers.last!
}
}
if presented.isKind(of: UITabBarController.self) {
if let tabBarController = presented as? UITabBarController {
if let navigationController = tabBarController.selectedViewController! as? UINavigationController {
return navigationController.viewControllers.last!
} else {
return tabBarController.selectedViewController!
}
}
}
return getVisibleViewController(presented)
}
return nil
}
func dismissedAllAlert(completion: (() -> Void)? = nil) {
if let alert = UIViewController.getVisibleViewController(nil) {
// If you want to dismiss a specific kind of presented controller then
// comment upper line and uncomment below one
// if let alert = UIViewController.getVisibleViewController(nil) as? UIAlertController {
alert.dismiss(animated: true) {
self.dismissedAllAlert(completion: completion)
}
} else {
completion?()
}
}
Note: You call anywhere in code at any class
Use:-
dismissedAllAlert() // For dismiss all presented controller
dismissedAllAlert { // For dismiss all presented controller with completion block
// your code
}
Swift
Used this to jump directly on your ROOT Navigation controller.
self.navigationController?.popToRootViewController(animated: true)

Resources