Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - ObservableObject Update from Coordinator

Tags:

swift

swiftui

A UIViewControllerRepresentable receives an image from a ViewController. This image should be made available to the SwiftUI view.

The ObservableObject:

class CurrentImage: ObservableObject  {
 var didChange = PassthroughSubject<Void, Never>()
 @Published var photoTaken: UIImage? = nil {didSet{didChange.send()}}
}

UIViewControllerRepresentable:

struct CameraPreviewView: UIViewControllerRepresentable  {

 var currentImage: CurrentImage

 func makeCoordinator() -> Coordinator {
  Coordinator(self)
 }

 class Coordinator: NSObject {
  let parent: CameraPreviewView

  init(_ cameraPreviewController: CameraPreviewView) {
   self.parent = cameraPreviewController
   super.init()

   // Notification receives a copy of the image taken from the ViewController:         
   NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "imageDidChange"), object: nil, queue: OperationQueue.main){(notification) in
    if let image = notification.userInfo?["image"] as? UIImage {

      // *****ERROR: self.parent.videoStatus.photoTaken is not accessible
      self.parent.currentImage.photoTaken = image
    }
   }
  }
 }


    func makeUIViewController(context: UIViewControllerRepresentableContext<CameraPreviewView>) -> CameraViewController {
        let cameraViewController = CameraViewController()
        return cameraViewController
    }

    // View is not updating so we don't need this
    func updateUIViewController(_ cameraViewController: CameraViewController, context: UIViewControllerRepresentableContext<CameraPreviewView>) {
    }
}

The ObservableObject is 'not accessible' and so I can't set the image within it from the Coordinator. Am I overlooking something simple here?

UPDATE 2: Passing value to an ObservableObject still not working

I've struggled with this and tried a dozen combinations of approaches. But to simplify the issue, we have a Coordinator with a var representing an ObservableObject. In this case, I'm going to keep it simple and try to set the value of a string.

The Coordinator:

struct CameraPreviewView: UIViewControllerRepresentable  {

 var cameraPhoto = CurrentPhoto()

 func makeCoordinator() -> Coordinator {
  Coordinator(self)
 }

 class Coordinator: NSObject {
  let parent: CameraPreviewView
  init(_ cameraPreviewController: CameraPreviewView) {
   self.parent = cameraPreviewController
   super.init()

   NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "imageDidChange"), object: nil, queue: OperationQueue.main){(notification) in
    print("notice received")
    self.parent.cameraPhoto.testString = "This changed"
   }
  }
 }
}...

The ObservableObject:

 class CurrentPhoto: ObservableObject  {
  @Published var testString = "Testing string"
 }

In order to test this, I also tried the following:

class CurrentPhoto: ObservableObject  {

 let objectWillChange = ObservableObjectPublisher()

 var testString = "Testing String" {
  willSet {
   print("set string")
   objectWillChange.send()
  }
 }
}

In the second example, the willSet method DOES get called. (It prints "set string") however the value remains "Testing String".

The ObservableObject IS being called from the Controller but the value does not set or change.

Therefore the view doesn't receive a new value:

struct ContentView: View {
 @ObservedObject var cameraState = CurrentPhoto()
 var body: some View {
  VStack{
   Text("test this \(cameraState.testString)")
   ...   

NOTE: passing a value from the VIEW to the ObservableObject works, so the issue is somehow connected to how the Coordinator passes value to an ObservedObject.

The following works and passes a value from the View to the OO:

Button(action : {
 self.cameraState.testString = "from view"
}, label : {
 Text("Change string")
}
like image 243
dot3 Avatar asked Nov 29 '19 16:11

dot3


1 Answers

This doesn't entirely solve the problem of how to bind a Coordinator to an ObservableObject. However, this did achieve the intended goal, which was to return the image from the Coordinator to SwiftUI.

The view:

struct ContentView: View {

 @State private var photoInView: Image?
 @State var isRecording = true

 var body: some View {
  VStack{
   photoInView?
   .resizable()
   .scaledToFit() 
   ZStack (alignment: .bottom) {
   // Start up the CameraPreview and this will 2-way bind.  
   CameraPreviewView(isRecording: $isRecording, photoReceivedFromCoordinator: $photoInView)
   ....

And then in the UIViewRepresentable:

struct CameraPreviewView: UIViewControllerRepresentable  {

  @Binding var isRecording: Bool
  @Binding var photoReceivedFromCoordinator: Image?

  func makeCoordinator() -> Coordinator {
   Coordinator(self)
  }

  class Coordinator: NSObject {
   // Setting the coordinator to parent allows the coordinator to use the    @Binding
   let parent: CameraPreviewView
   init(_ cameraPreviewController: CameraPreviewView) {
    self.parent = cameraPreviewController
    super.init()

    NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "imageDidChange"), object: nil, queue: OperationQueue.main){(notification) in
    if let uiImage = notification.userInfo?["image"] as? UIImage {
     // We need an Image not a UI Image, so first we convert it
     let image = Image(uiImage: uiImage)
     // Assign the value to the parent's binding value
     self.parent.photoReceivedFromCoordinator = image
    }
   }
  }
 }
like image 118
dot3 Avatar answered Nov 15 '22 09:11

dot3