How to update UIViewRepresentable with ObservableObject
Asked Answered
G

3

21

I'm trying to learn Combine with SwiftUI and I'm struggling how to update my view (from UIKit) with ObservableObject (previously BindableObject). The issue is that, obviously, method updateUIView will not fire once the @Published object sends the notification it was changed.

class DataSource: ObservableObject {
    @Published var locationCoordinates = [CLLocationCoordinate2D]()
    var value: Int = 0

    init() {
        Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { timer in
            self.value += 1
            self.locationCoordinates.append(CLLocationCoordinate2D(latitude: 52, longitude: 16+0.1*Double(self.value)))
        }
    }
}

struct MyView: UIViewRepresentable {
    @ObservedObject var dataSource = DataSource()

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

    func updateUIView(_ view: MKMapView, context: Context) {
        let newestCoordinate = dataSource.locationCoordinates.last ?? CLLocationCoordinate2D(latitude: 52, longitude: 16)
        let annotation = MKPointAnnotation()
        annotation.coordinate = newestCoordinate
        annotation.title = "Test #\(dataSource.value)"
        view.addAnnotation(annotation)
    }
}

How to bind that locationCoordinates array to the view in such a way, that a new point is in fact added each time it refreshes?

Gheber answered 13/8, 2019 at 12:52 Comment(1)
did you find a way to do this without pulling it out?Depreciatory
B
16

To make sure your ObservedObject does not get created multiple times (you only want one copy of it), you can put it outside your UIViewRepresentable:

import SwiftUI
import MapKit

struct ContentView: View {
    @ObservedObject var dataSource = DataSource()

    var body: some View {
        MyView(locationCoordinates: dataSource.locationCoordinates, value: dataSource.value)
    }
}
class DataSource: ObservableObject {
    @Published var locationCoordinates = [CLLocationCoordinate2D]()
    var value: Int = 0

    init() {
        Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { timer in
            self.value += 1
            self.locationCoordinates.append(CLLocationCoordinate2D(latitude: 52, longitude: 16+0.1*Double(self.value)))
        }
    }
}

struct MyView: UIViewRepresentable {
    var locationCoordinates: [CLLocationCoordinate2D]
    var value: Int

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

    func updateUIView(_ view: MKMapView, context: Context) {
        print("I am being called!")
        let newestCoordinate = locationCoordinates.last ?? CLLocationCoordinate2D(latitude: 52, longitude: 16)
        let annotation = MKPointAnnotation()
        annotation.coordinate = newestCoordinate
        annotation.title = "Test #\(value)"
        view.addAnnotation(annotation)
    }
}
Buntline answered 13/8, 2019 at 13:48 Comment(2)
I'd rather inject the model by another thing (an interactor), which does not inherit from View class. Is there any other way to observe it without creating the struct inheriting from View? What you did is basically wrapping it around, and while it does work, it doesn't show to fix the UIViewRepresentable problem itself. Also, I don't think your solution fixes creating ObservedObject multiple times. UIViewRepresentable is created exactly once here and there, so it's not created multiple times. If you mean that it will be recreated once view gets recreated- then it also happens in both cases.Gheber
Yeah - I had same issue, only fix was to hold my Datasource in the env - otherwise it would get recreated, no matter where I put it.Marc
C
1

this solution worked for me but with EnvironmentObject https://gist.github.com/svanimpe/152e6539cd371a9ae0cfee42b374d7c4

Casto answered 12/11, 2019 at 21:30 Comment(0)
W
1

I'm gonna provide a general solution for any UI/NS view representable using combine. There are performance benefits to my method.

  1. Created an Observable Object and wrap the desired properties with @Published wrapper
  2. Inject the Observed object via the updateView method in the view representable using a method you'll make in step 3
  3. Subclass the desired view with the view model as a parameter. Create an addViewModel method and use combine operators/ subscribers and add them to cancellable.

Note - Works great with environment objects.

    struct swiftUIView : View {
      @EnvironmentObject var env : yourViewModel
      ...
      ...
          UIViewRep(wm : env)
     }

    struct UIViewRep : UIViewRepresentable {

     var wm : yourViewModel
     
     func makeUIView {
      let yv = yourView()
      yv.addViewModel(wm)
      return yv
      }}

    class yourView : UIView {
     var viewModel : yourViewModel?
     var cancellable = Set<AnyCancellable>()
     ...
     ...
     func addViewModel( _ wm : yourViewModel) {
     self.viewModel = wm

      self.viewModel?.desiredProperty
          .sink(receiveValue: { [unowned self] w in
        print("Make changes with ", w)
       }).store(in: &cancellable)
      }
    }
Williwaw answered 27/11, 2021 at 12:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.