Home > OS >  Accessing Value of SwiftUI View through a UIKit View
Accessing Value of SwiftUI View through a UIKit View

Time:06-27

I read this question online and found it to be very interesting. If you have a SwiftUI view as shown below. How can you access the selected rating in a UIKit view without changing a single line in RatingView.

struct RatingView: View {
    
    @Binding var rating: Int?
    
    private func starType(index: Int) -> String {
        
        if let rating = rating {
            return index <= rating ? "star.fill" : "star"
        } else {
            return "star"
        }
    }
    
    var body: some View {
        HStack {
            ForEach(1...5, id: \.self) { index in
                Image(systemName: self.starType(index: index))
                    .foregroundColor(Color.orange)
                    .onTapGesture {
                        rating = index
                }
            }
        }
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white
        
        // .constant(3) will be replaced by something in the ViewController so it can handle the changes for the rating
        let hostingController = UIHostingController(rootView: RatingView(rating: .constant(3)))
        
        guard let ratingsView = hostingController.view else { return }
        self.addChild(hostingController)
        
        self.view.addSubview(ratingsView)
}

UPDATE:

Original source: https://twitter.com/azamsharp/status/1540838477599752192?s=20&t=bbDp3VT9m0Ce4W3Sgr7Iyg

I typed most of the code from the original source. I did not change much.

CodePudding user response:

We can decorate the RatingView and use an ObservableObject to hold on to the source of truth.

class RatingObserver: ObservableObject {
    @Published var rating: Int?
}

struct WrappedRatingView: View {

    @ObservedObject var ratingObserver: RatingObserver

    var body: some View {
        RatingView(rating: $ratingObserver.rating)
    }
}

Then we can use it in the following way.

class ViewController: UIViewController {

    let ratingObserver = RatingObserver()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white

        let hostingController = UIHostingController(
            rootView: WrappedRatingView(ratingObserver: ratingObserver)
        )
        self.addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.didMove(toParent: self)
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false

        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setTitle("Reset Rating", for: .normal)
        button.setTitleColor(.blue, for: .normal)
        button.addTarget(self, action: #selector(resetRating), for: .touchUpInside)
        view.addSubview(button)

        NSLayoutConstraint.activate([
            hostingController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            hostingController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 100)
        ])
    }

    @objc func resetRating() {
        ratingObserver.rating = nil
    }
}

This allows for updating both the ViewController and the SwiftUI view.

  • Related