Search code examples
iosswiftswiftuiuikit

SwiftUI calling delegate in UIViewController


This is the first time I am working with SwiftUI, and I have searched for this problem but did not find any working solution, so I would appreciate all help I can get.

I have a UIViewController, in which I present a SwiftUI View through UIHostingController. What I want to achieve is that when I press the button in my SwiftUI view, the action is going to trigger the delegate in UIViewController, alternatively, trigger a function in UIViewController and then from that function trigger the delegate.

class MyViewController: UIViewController {

public var delegate: MyViewControllerDelegate?
let facialView = UIHostingController(rootView: FacialTutorial())

override func viewWillAppear(_ animated: Bool) {

addChild(facialView)
view.addSubview(facialView.view)
setupConstraints()

}

extension MyViewController {

@objc private func buttonPressed() {

delegate?.buttonPressed()

}

}


}

And in my SwiftUI view FacialTutorial

struct FacialTutorial: View {

var body: some View {

        VStack() {
          Button {
              // I want to call delegate?.buttonPressed() action here
          } label: {
              Text("Press button")
          }
        
      }

}


}

EDIT

Okay to be more clear, my ViewController is configuring the page differently for a number of cases. So in practice, I do not initiate the SwiftUI view from viewWillAppear. Rather this is how I do

public var delegate: MyViewControllerDelegate?
let facialView = UIHostingController(rootView: FacialTutorial())


override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    
    addChild(facialView)
    configureSubviews()
}



private func configureForInstructionMode() {

  view.addSubview(facialUIView.view)
  setupConstraints()
}

I must have it this way because I need to configure the view differently depending on which mode I am going to configure for. When I declare the facialView inside the viewDidLoad or viewWillAppear, I cannot access the instance in configureForInstructionMode(), or it's value is nil..


Solution

  • You simply need to pass a reference to your MyViewController instance through to MyUIView (Which should probably be MyView since it isn't a subclass of UIView) via its initialiser. You could use a delegation pattern or you could pass a closure and then invoke the delegate method from the closure. The second way is more "Swifty".

    (I have moved the code to viewDidLoad since if you have it in viewWillAppear the view may be added multiple times if the view controller appears and disappears and appears again)

    class MyViewController: UIViewController {
    
        public var delegate: MyViewControllerDelegate? {
            
        weak var myView: UIHostingController<MyUIView>!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            let myView = UIHostingController(rootView: MyUIView(buttonHandler: { [weak self] in
                    self?.delegate?.buttonPressed()
                }))
            self.myView = myView
            view.addSubview(myView.view)
            setupConstraints()
        }
    }
    

    Then, in your SwiftUI view you can declare a property to hold the closure and invoke that closure in your button

    struct MyUIView: View {
        
        var buttonHandler: (()->Void)?
        
        var body: some View {
            VStack() {
                Button {
                    buttonHandler?()
                } label: {
                    Text("Press button")
                }
                
            }
        }
    }