6

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?

Bruno Machado - vargero
  • 2,690
  • 5
  • 33
  • 52
  • 1
    I can't get the example to show anything on the screen, appears to be an issue with `CustomActionSheet`, can you make a gist/repo so we can try directly? – George Aug 05 '21 at 17:08
  • @George here's the link to the Gist https://gist.github.com/vargero/7a018471856be3be06ef45910f6203df I've removed rounded corners as this was custom code, but the code should work now. As it is it doesn't show anything, only if you call `showActionSheet` on `.onAppear` – Bruno Machado - vargero Aug 05 '21 at 17:36
  • Even in a completely blank UIKit project, nothing shows on the screen (code from gist too) – George Aug 05 '21 at 17:41
  • @George I updated the Gist with all the classes needed. This works when there's a new Storyboard project that starts with `ViewController` – Bruno Machado - vargero Aug 05 '21 at 17:59

1 Answers1

2

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

George
  • 25,988
  • 10
  • 79
  • 133