lukecsmith.co.uk

Calling a storyboard segue from SwiftUI

Luke, 11 Nov 2021

The scenario is this: you've got a mix of SwiftUI and UIKit in your app, which also includes a storyboard. You want to call back from a SwiftUI view, to a UIHostingController which wraps the view and contains it within the storyboard. Normally this is the sort of scenario that would be a good candidate for delegation: make the UIHostingController (subclass) the delegate of the SwiftUI view so that you can call back and ask it to perform the segue you wish.

But this isnt possible. When you create the UIHostingController, you name the SwiftUI view in the init. For you to set the UIHostingController as its delegate, you need to refer to the UIHostingController during that init. But that requires referring to self, and you cant do that until the init has completed.

This is actually quite a tricky problem to resolve, but Ive found an elegant and simple solution. It involves adding a class within the UIHostingController, that handles the callbacks.

First, heres what the UIHostingController subclass looks like initially. My SwiftUI view is called InitialLoginView so Ive named the UIHostingController LoginHostingController. Here I just create that SwiftUI view in the init of the UIHostingController.

class LoginHostingController: UIHostingController<AnyView> {

    @objc required dynamic init?(coder aDecoder: NSCoder) {
        let loginView = InitialLoginView(loginCallbacks: loginCallbacks)
        super.init(coder: aDecoder, rootView: AnyView(loginView))
    }
}

The solution looks like this. I create a class called LoginCallbacks (within the LoginHostingController) and this will handle any instructions I need to send up to the UIHostingController, and then on, i.e. up to the storyboard in terms of a segue. I create an instance of this during the init, and pass that instance on to my SwiftUI view.

This LoginCallbacks class has a closure property on it called segueCallback. It allows me to pass in a String - this is where I will pass the name of the segue I want to call.

Then during the init, i initialize that property so that it calls a local function that will call to perform the segue.

class LoginHostingController: UIHostingController<AnyView> {
    
    class LoginCallbacks {
        var segueCallback: ((String) -> Void)?
    }

    @objc required dynamic init?(coder aDecoder: NSCoder) {
        // object to handle the callbacks
        let loginCallbacks = LoginCallbacks()
        
        let loginView = InitialLoginView(loginCallbacks: loginCallbacks)
        super.init(coder: aDecoder, rootView: AnyView(loginView))
        
        //set up that callbacks object with the instruction to perform a segue
        loginCallbacks.segueCallback = { [weak self] segueName in
            self?.performSegue(name: segueName)
        }
    }
}

Finally within my SwiftUI view, obviously first I have that loginCallbacks property that I pass in, in the init above:

var loginCallbacks: LoginHostingController.LoginCallbacks

Then anywhere within my view, I can call to perform that segue, for example like this with a button:

Button(action: {
    loginCallbacks.segueCallback?("HostToSignUp")
}, label: {
    Text("SIGN UP")
})

And thats it. I have a weak reference to that object in the UIHostingController to prevent a retain cycle. If theres other commands you wish to pass up to the UIHostingController you could just add another closure property onto the LoginCallbacks class.

Tagged with: