Passing ObservableObject and StateObject between SwiftUI and UIKit

In this tutorial, I will be explaining how we can go pass data between SwiftUI and UIKit. It’s pretty straightforward to use observable and state objects when passing data between SwiftUI views. When it comes to UIKit, you cannot directly pass these objects. There is a lot more you need to do. To visualize, my UX will be as shown below.

I will declare an observable object in SwiftUI code. It will have a @published variable to show an error. This in turn will pop up an alert showing the error.

enum CameraResult {
	case success
	case denied
	case restricted
	case failedToGivePermission
	case notDetermined
}
class CameraDetails: ObservableObject {
	@Published var result = CameraResult.notDetermined
	@Published var showError: Bool = false
}

Next we will write the SwiftUI view. This will have the VStack and the alert. It will also initialize the StateObject. You need to remember that, StateObject needs to be declared and initialized just once. In order to pass the object, you will be using ObservableObject.

struct CameraView: View {
	
	@StateObject var cameraDetails = CameraDetails()
	var body: some View {
		VStack(alignment: .center) {
			PreviewHolderView(cameraDetails: cameraDetails)
		}
		.alert(isPresented: $cameraDetails.showError) {
			Alert(title: Text("Permission issues"), message: Text("We need access to your camera to record videos."), dismissButton: .default(Text("OK")))
		}
		.edgesIgnoringSafeArea(.all)
    }
}
struct PreviewHolderView: View {
	@ObservedObject var cameraDetails: CameraDetails
	var body: some View {
		ZStack{
			PreviewUIViewRepresentable(cameraDetails: cameraDetails)
		}
	}
}

PreviewUIViewRepresentable is the struct used to interact with UIKit views. PreviewUIView is the UIView in UIKit. Its defined as shown below

class PreviewUIView : UIView {
       init(delegate: CameraUIViewDelegate) {
		super.init(frame: .zero)
		self.delegate = delegate // initialize the delegate here
		askForCameraPermission()
	}
        // method to call the delegate
        func askForCameraPermission() {
                guard self.delegate != nil else {
			return
		}
                // call the delegate here
		self.delegate?.cameraOpened(.success)
        }
}

In the above class, we are defining a UIView in UIKit, and then setting a delegate when we open the camera. In order to access a UIView in SwiftUI, we need to do this via UIViewRepresentable class. This is as shown below

struct PreviewUIViewRepresentable : UIViewRepresentable {
	@ObservedObject var cameraDetails: CameraDetails
	func makeUIView(context: Context) -> PreviewUIView {
		let previewUIView = PreviewUIView(delegate: context.coordinator)
		previewUIView.delegate = context.coordinator
		return previewUIView
	}
	
	func updateUIView(_ uiView: UIViewType, context: Context) {
		print("inside updateuiview")
	}
	
	func makeCoordinator() -> Coordinator {
		Coordinator(cameraDetails: cameraDetails)
	}
	typealias UIViewType = PreviewUIView
}

Note we are also declaring the cameraDetails as ObservedObject here. We are going to use it.

extension PreviewUIViewRepresentable {
	class Coordinator: NSObject, CameraUIViewDelegate {
		func cameraOpened(_ result: CameraResult) {
			print("hello \(result)")
			cameraDetails.showError = true
		}
		
		let cameraDetails: CameraDetails

		init(cameraDetails: CameraDetails) {
			self.cameraDetails = cameraDetails
		}
	}
}

SwiftUI and UIKit are two different worlds. If you want to interact between them, you need to use a Coordinator. This class basically acts as a middle man between these two worlds. Lets define it here

extension PreviewUIViewRepresentable {
	class Coordinator: NSObject, CameraUIViewDelegate {
		func cameraOpened(_ result: CameraResult) {
			cameraDetails.showError = true
		}
		
		let cameraDetails: CameraDetails

		init(cameraDetails: CameraDetails) {
			self.cameraDetails = cameraDetails
		}
	}
}

As you noticed, we are passing the observable object in the init method. We initialize the coordinator in the UIViewRepresentable struct.

func makeCoordinator() -> Coordinator {
     Coordinator(cameraDetails: cameraDetails)
}

Also in the PreviewUIViewRepresentable struct, we pass the coordinator as the delegate.

let previewUIView = PreviewUIView(delegate: context.coordinator)

In this way you can interact between both the worlds.

Leave a Reply

Your email address will not be published. Required fields are marked *