Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Accessing another UIViewRepresentable's controller or UIView

Tags:

uikit

swiftui

UIViewRepresentable is useful for bringing UIKit views into SwiftUI context. Their primary limitation is that the instantiation of the UIKit side of things is not under our control - it happens as-needed by the SwiftUI subsystem.

This creates difficulties when two UIViews need to have knowledge of each other in order to collaborate. An example could be an MKMapView and an MKCompassButton. The latter needs an instance of the former to sync with.

Passing such a reference between separate UIViewRepresentable values is difficult since the controller or view is not available to us directly.

struct MapView: UIViewRepresentable {
    func makeUIView(context: Context) -> MKMapView { .init() }
}
struct CompassButton: UIViewRepresentable {
    func makeUIView(context: Context) -> MKCompassButton { .init(mapView: ???) }
}
/// or
struct MapView: UIViewRepresentable {
    let compass = CompassButton()

    func makeUIView(context: Context) -> MKMapView { .init() }

    struct CompassButton: UIViewRepresentable {
        func makeUIView(context: Context) -> MKCompassButton { .init(mapView: ???) }
    }
}

Does anyone know of a mechanism by which we can allow two SwiftUI views based on UIViewRepresentable to collaborate using their underlying UIKit views, perhaps through sharing a controller instance, or other means?

My first thought would be to move the instantiation of the controller out of makeController and into the UIViewRepresentable directly as a var, but this would likely interfere with the SwiftUI life-cycle management of the controller.

like image 538
lhunath Avatar asked Oct 17 '25 21:10

lhunath


1 Answers

You can't access the internals of a UIViewRepresentable and if you hold on to the UIView variable you'll start getting the "updating view while updating view isn't allowed error" that is quite popular. Apple just doesn't allow access to internals with SwiftUI.

Creating a common "ViewModel"/Controller that is shared between UIKit and SwiftUI is the simplest way to do this. The UIView's would exist in a UIViewController so you get all the UIKit benefits.

import SwiftUI
import MapKit
///Source of truth for both SwiftUI and UIKit
class MapCompassViewModel: ObservableObject, MapController{
    @Published var provider: (any MapProvider)?
    
    func toggleCompassVisibility(){
        provider?.toggleCompassVisibility()
    }
    func addCompass(){
        provider?.addCompass()
    }
}

You can use protocols to hide the internal implementations.

protocol MapProvider{
    var map : MKMapView {get set}
    func toggleCompassVisibility()
    func addCompass()
}
protocol MapController{
    var provider: (any MapProvider)? {get set}
}

The UI part is just a View a UIViewControllerRepresentable and a UiViewController.

///Plain SwiftUI View
struct MapCompassView: View {
    @StateObject var vm: MapCompassViewModel = .init()
    var body: some View {
        VStack{
            //This is needed to for the very first frame,
            //when we are waiting for the provider
            //to be set for the UIViewController
            if let provider = vm.provider {
                Compass_UI(provider: provider)
                    .frame(width: 20, height: 20)
            }
            Button("Show/Hide compass", action: vm.toggleCompassVisibility)
            MapCompass_UI(vm: vm)
        }
    }
}
///Converts UIKit `UIView` to a `UIViewRepresentable`
struct Compass_UI: UIViewRepresentable{
    let provider: any MapProvider
    func makeUIView(context: Context) -> some UIView {
        let m = MKCompassButton(mapView: provider.map)
        m.compassVisibility = .visible
        return m
    }
    func updateUIView(_ uiView: UIViewType, context: Context) {
        
    }
}
///Converts UIKit `UIViewController` to a `UIViewControllerRepresentable`
struct MapCompass_UI: UIViewControllerRepresentable{
    let vm: any MapController
    func makeUIViewController(context: Context) -> some UIViewController {
        MapCompassViewController(vm: vm)
    }
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        
    }
}
///Regular `UIViewController` that uses `MapCompassViewModel`
///This can be as complex as needed.
class MapCompassViewController: UIViewController, MapProvider{
    var vm: any MapController
    lazy var map: MKMapView = {
        let m = MKMapView(frame: .zero)
        m.showsCompass = false
        return m
    }()
    lazy var compass: MKCompassButton = {
        let m = MKCompassButton(mapView: map)
        m.frame.origin = .init(x: 20, y: 20)
        m.compassVisibility = .visible
        return m
    }()
    init(vm: any MapController) {
        self.vm = vm
        super.init(nibName: nil, bundle: nil)
        //Critical connection between SwiftUI and UIKit
        DispatchQueue.main.async{
            self.vm.provider = self
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        //Add map
        view.addSubview(map)
        //Pin map to edges
        map.translatesAutoresizingMaskIntoConstraints = false
        map.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        map.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        map.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        map.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        //Add compass
        map.addSubview(compass)
    }
    func toggleCompassVisibility(){
        compass.compassVisibility = compass.compassVisibility == .visible ? .hidden : .visible
    }
    func addCompass() {
        print("\(#function) :: add your compass code")
    }
    deinit{
        vm.provider = nil
    }
}
struct MapCompassView_Previews: PreviewProvider {
    static var previews: some View {
        MapCompassView()
    }
}
like image 101
lorem ipsum Avatar answered Oct 21 '25 10:10

lorem ipsum



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!