Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to bind SwiftUI and UIViewController behavior

I have a UIKit project with UIViewControllers, and I'd like to present an action sheet built on SwiftUI from my ViewController. I need to bind the appearance and disappearance of the action sheet back to the view controller, enabling the view controller to be dismissed (and for the display animation to happen only on viewDidAppear, to avoid some weird animation behavior that happens when using .onAppear). Here is a code example of how I expect the binding to work and how it's not doing what I'm expecting:

import UIKit
import SwiftUI

class ViewController: UIViewController {
    let button = UIButton(type: .system)
    var show = true
    lazy var isShowing: Binding<Bool> = .init {
        self.show
    } set: { show in
        // This code gets called
        self.show = show
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        button.setTitle("TAP THIS BUTTON", for: .normal)
        view.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        button.addTarget(self, action: #selector(tapped), for: .touchUpInside)
    }
    
    @objc private func tapped() {
        let vc = UIHostingController(rootView: BindingProblemView(testBinding: isShowing))
        vc.modalPresentationStyle = .overCurrentContext
        present(vc, animated: false)
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [self] in
            isShowing.wrappedValue.toggle()
            isShowing.update()
        }
    }
}

struct BindingProblemView: View {
    @Binding var testBinding: Bool
    @State var state = "ON"
    
    var body: some View {
        ZStack {
            if testBinding {
                Color.red.ignoresSafeArea().padding(0)
            } else {
                Color.green.ignoresSafeArea().padding(0)
            }
            
            Button("Test Binding is \(state)") {
                testBinding.toggle()
            }.onChange(of: testBinding, perform: { value in
                // This code never gets called
                state = testBinding ? "ON" : "OFF"
            })
        }
    }
}

What happens is that onChange never gets called after viewDidAppear when I set the binding value true. Am I just completely misusing the new combine operators?

like image 929
Bruno Machado - vargero Avatar asked Nov 15 '22 19:11

Bruno Machado - vargero


1 Answers

You can pass the data through ObservableObjects, rather than with Bindings. The idea here is that ViewController has the reference to a PassedData instance, which is passed to the SwiftUI view which receives changes to the object as it's an @ObservedObject.

This now works, so you can click on the original button to present the SwiftUI view. The button in that view then toggles passedData.isShowing which changes the background color. Since this is a class instance, the ViewController also has access to this value. As an example, isShowing is also toggled within tapped() after 5 seconds to show the value can be changed from ViewController or BindingProblemView.

Although it is no longer needed, the onChange(of:perform:) still triggers.

Code:

class PassedData: ObservableObject {
    @Published var isShowing = true
}
class ViewController: UIViewController {
    let button = UIButton(type: .system)
    let passedData = PassedData()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white
        button.setTitle("TAP THIS BUTTON", for: .normal)
        view.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        button.addTarget(self, action: #selector(tapped), for: .touchUpInside)
    }

    @objc private func tapped() {
        let newView = BindingProblemView(passedData: passedData)
        let vc = UIHostingController(rootView: newView)
        vc.modalPresentationStyle = .overCurrentContext
        present(vc, animated: false)

        // Example of toggling from in view controller
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            self.passedData.isShowing.toggle()
        }
    }
}
struct BindingProblemView: View {
    @ObservedObject var passedData: PassedData

    var body: some View {
        ZStack {
            if passedData.isShowing {
                Color.red.ignoresSafeArea().padding(0)
            } else {
                Color.green.ignoresSafeArea().padding(0)
            }

            Button("Test Binding is \(passedData.isShowing ? "ON" : "OFF")") {
                passedData.isShowing.toggle()
            }
        }
    }
}

Result:

Result

like image 142
George Avatar answered Jan 08 '23 23:01

George