How can I have a UIButton (UIViewControllerRepresentable) trigger an action in my model?
Asked Answered
P

0

2

I wish to have a UIButton in a UIViewController run a function in my ObservableObject in my SwiftUI app. My needs are larger than this, so I've trimmed out things to this code:

UIViewController & Delegate

protocol TestVCDelegate {
    func runTest()
}
class TestVC: UIViewController {
    let btnTest = UIButton()
    let lblTest = UILabel()
    var delegate:TestVCDelegate! = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.red

        btnTest.translatesAutoresizingMaskIntoConstraints = false
        lblTest.translatesAutoresizingMaskIntoConstraints = false

        btnTest.backgroundColor = UIColor.green
        btnTest.addTarget(self, action: #selector(runTest), for: .touchUpInside)
        view.addSubview(btnTest)
        btnTest.heightAnchor.constraint(equalToConstant: 100).isActive = true
        btnTest.widthAnchor.constraint(equalToConstant: 100).isActive = true
        btnTest.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
        btnTest.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100).isActive = true

        view.addSubview(lblTest)
        lblTest.heightAnchor.constraint(equalToConstant: 100).isActive = true
        lblTest.widthAnchor.constraint(equalToConstant: 100).isActive = true
        lblTest.topAnchor.constraint(equalTo: btnTest.bottomAnchor, constant: 10).isActive = true
        lblTest.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 100).isActive = true
}

    @objc func runTest() {
        delegate.runTest()
        lblTest.text = "hello world!"
    }
}

UIViewControllerRepresentable & Coordinator

struct Take2: UIViewControllerRepresentable {
    @EnvironmentObject var model: Model
    let testVC = TestVC()
    func makeUIViewController(context: Context) -> TestVC {
        testVC.delegate = context.coordinator
        return testVC
    }
    func updateUIViewController(_ uiViewController: TestVC, context: Context) {
        //
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    class Coordinator: NSObject, TestVCDelegate {
        var parent: Take2
        init(_ testViewController: Take2) {
            parent = testViewController
        }
        func runTest() {
            print("hello world")
        }
    }
}

Model

class Model : ObservableObject {
    var objectWillChange = PassthroughSubject<Void, Never>()
    @Published var lblText = "" {
        willSet {
            objectWillChange.send()
        }
    }
    func runTest() {
        print("yet again, hello world")
        lblText = "hello world!"
    }
}

Every time I try to execute runTest() in my model I get this error:

Fatal error: Reading EnvironmentObject outside View.body

Now, this error happens whether I try to do this in Take2, it's Coordinator, or in my TestVC. I (mostly) understand the error - if I should try to execute this in some View via onAppear it works. BUT - I thought a UIViewControllerRepresentable is a View to the SwiftUI stack (even though it isn't some View).

I've succeeded in exposing UIKit properties (both get and set) to the SwiftUI stack. I also have UIKit delegates (specifically, UIimagepickerController) delegate successfully updating SwiftUI properties.

But how can I execute a function in an ObservableObject from a UIKit UIButton?

Persecution answered 27/8, 2019 at 13:54 Comment(3)
There is probably background why View.body is required. Have you tried indirection - a wrapper View having the environment object and passing into Take2 a PassthroughSubject<Void, Never>? This publisher you can use in the wrapper with .onReceive(publisher) and call methods on the @EnvironmentObject while Take2 just sends on this publisher to signal the function call. I know it's a workaround but any other way is probably may be more complicated. Or you can use a @Published setter since setter variables are allowed...... / willSet / didSet which can call more. Or can they?Frederique
Intertesting. I'll definitely look into this. Thanks!Persecution
Turns out I likely mistagged my question. I haven't (yet) been able to use the suggestion by @Frederique of creating a wrapper View - but so far I have something better - Notification. (Doh!) I want to see if I can successfully apply the Publisher-Subscriber pattern from Combine (thus, the mistag) and I'll post my answer here for others within a day.Persecution

© 2022 - 2024 — McMap. All rights reserved.