1

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?

Community
  • 1
  • 1
Alfred Ly
  • 123
  • 1
  • 8

2 Answers2

1

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.

David
  • 2,109
  • 1
  • 22
  • 27
  • Thanks for the clarification! I'll try out this implementation as soon as I can. As a quick sanity check, if I now wanted a UIImage of all view controllers to reflect the disconnect/connect status, I could set a NSNotification in the BTDeviceDelegate protocol, and in subsequent `ViewController`s just listen for a notification? – Alfred Ly Apr 13 '17 at 15:04
  • If you need to have more VCs responding to the bluetooth device status you could just give up the delegate and use notifications. Also you should not emit notifications from the protocol... The protocol's sole purpose is to tell someone (in this case your VC) that a change in your model has occured. You might want to create a new post if you have any further questions as we are getting a bit OT now. Notifications seems reasonable though. – David Apr 13 '17 at 20:45
0

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.

Alfred Ly
  • 123
  • 1
  • 8