Alfred Ly
Alfred Ly

Reputation: 123

How to programmatically change a view controller while not in a ViewController Class

I know this question has been asked countless times already, and I've seen many variations including

func performSegue(withIdentifier identifier: String, 
       sender: Any?)

and all these other variations mentioned here: How to call a View Controller programmatically

but how would you change a view controller outside of a ViewController class? For example, a user is currently on ViewController_A, when a bluetooth device has been disconnected (out of range, weak signal, etc) the didDisconnectPeripheral method of CBCentral gets triggered. In that same method, I want to change current view to ViewController_B, however this method doesn't occur in a ViewController class, so methods like performSegue won't work.

One suggestion I've implemented in my AppDelegate that seems to work (used to grab the appropriate storyboard file for the iphone screen size / I hate AutoLayout with so much passion)

    var storyboard: UIStoryboard = self.grabStoryboard()
    display storyboard
    self.window!.rootViewController = storyboard.instantiateInitialViewController()
    self.window!.makeKeyAndVisible()

And then I tried to do the same in my non-ViewController class

    var window: UIWindow?
    var storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) //assume this is the same storyboard pulled in `AppDelegate`
    self.window!.rootViewController = storyboard.instantiateViewController(withIdentifier: "ViewController_B")
    self.window!.makeKeyAndVisible()

However I get an exception thrown saying fatal error: unexpectedly found nil while unwrapping an Optional value presumably from the window!

Any suggestions on what I can do, and what the correct design pattern is?

Upvotes: 1

Views: 532

Answers (2)

dvdblk
dvdblk

Reputation: 2955

Try this:

protocol BTDeviceDelegate {
    func deviceDidDisconnect()
    func deviceDidConnect()
}

class YourClassWhichIsNotAViewController {

    weak var deviceDelegate: BTDeviceDelegate?


    func yourMethod() {
        deviceDelegate?.deviceDidDisconnect()
    }
}


class ViewController_A {

    var deviceManager: YourClassWhichIsNotAViewController?

    override func viewDidLoad() {

        deviceManager = YourClassWhichIsNotAViewController()
        deviceManager.delegate = self
    }
}

extension ViewController_A: BTDeviceDelegate {
    func deviceDidDisconnect() {
        DispatchQueue.main.async {
             // change the VC however you want here :)

             // updated answer with 2 examples.
             // The DispatchQueue.main.async is used here because you always want to do UI related stuff on the main queue 
             // and I am fairly certain that yourMethod is going to get called from a background queue because it is handling 
             // the status of your BT device which is usually done in the background...

             // There are numerous ways to change your current VC so the decision is up to your liking / use-case.
             // 1. If you are using a storyboard - create a segue from VC_A to VC_B with an identifier and use it in your code like this
             performSegue(withIdentifier: "YourSegueIdentifierWhichYouveSpecifiedInYourSeguesAttibutesInspector", sender: nil)

             // 2. Instantiate your VC_B from a XIB file which you've created in your project. You could think of a XIB file as a 
             // mini-storyboard made for one controller only. The nibName argument is the file's name.
             let viewControllerB = ViewControllerB(nibName: "VC_B", bundle: nil)
             // This presents the VC_B modally 
             present(viewControllerB, animated: true, completion: nil)

        }
    }

    func deviceDidConnect() {}

}

YourClassWhichIsNotAViewController is the class which handles the bluetooth device status. Initiate it inside the VC_A and respond to the delegate methods appropriately. This should be the design pattern you are looking for.

Upvotes: 1

Alfred Ly
Alfred Ly

Reputation: 123

I prefer dvdblk's solution, but I wasn't sure how to implement DispatchQueue.main.async (I'm still pretty new at Swift). So this is my roundabout, inefficient solution:

In my didDisconnectPeripheral I have a singleton with a boolean attribute that would signify whenever there would be a disconnect.

In my viewdidload of my ViewController I would run a scheduledTimer function that would periodically check the state of the boolean attribute. Subsequently, in my viewWillDisappear I invalidated the timer.

Upvotes: 0

Related Questions