[SwiftUI] UIViewRepresentable 에서 PassthroughSubject 구독하기

UIViewRepresentable 을 상속받은 클래스에서 PassthroughSubject 를 구독하는 방법을 찾아냈다.

메인 화면에는 변수를 바꾸는 것과 지도를 바꾸는 버튼과 지도를 배치한다.

struct ContentView: View {
  @EnvironmentObject var mapViewEnvironment: MapViewEnvironment

  var body: some View {
    VStack {
      Button(action: { self.mapViewEnvironment.value1 = "aaa" }) { Text("set aaa") }
      Button(action: { self.mapViewEnvironment.value1 = "bbb" }) { Text("set bbb") }
      Button(action: { self.mapViewEnvironment.currentMapCompany = "apple" }) { Text("set apple") }
      Button(action: { self.mapViewEnvironment.currentMapCompany = "google" }) { Text("set google") }

      CommonMapView()
    }
  }
}

테스트를 위해 일반 타입의 변수와 PassthroughSubject 타입의 변수를 생성한다.

class MapViewEnvironment: ObservableObject {
  @Published var value1 = "aaa"
  var value2 = PassthroughSubject<String, Never>()
  @Published var currentMapCompany = "apple"
}

지도를 표시하는 뷰에서는 지도 선택말고는 할 수 있는게 없었다. currentMapView 같은 변수를 만들어서 현재 표시 중인 지도를 넣고 싶었는데, 변수 타입을 뭘 넣어야하는지 끝내 찾이 못했다.

struct CommonMapView: View {
  @EnvironmentObject var mapViewEnvironment: MapViewEnvironment

  var body: some View {
    ZStack {
      if mapViewEnvironment.currentMapCompany == "apple" {
        AppleMapView()
      } else {
        GoogleMapView()
      }
    }
  }
}

테스트를 위한 임시 함수들 추가해주고

protocol MapViewProtocol {
  func aaa()
  func bbb()
}

실제 지도 화면에서는 깔끔함을 위해 willApear()라는 함수를 만들어주고, 그 안에서 구독을 시작한다.

struct AppleMapView: UIViewRepresentable {
  @EnvironmentObject var mapViewEnvironment: MapViewEnvironment

  func makeUIView(context: Context) -> MKMapView {
    let view = MKMapView()
    view.mapType = .standard

    willAppear(context)

    return view
  }

  func makeCoordinator() -> AppleMapView.Coordinator {
    return Coordinator()
  }

  func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<AppleMapView>) {}

  static func dismantleUIView(_ uiView: MKMapView, coordinator: AppleMapView.Coordinator) {
    print("apple dismantleUIView")
    coordinator.cancellable.removeAll()
  }

  final class Coordinator {
    var cancellable = Set<AnyCancellable>()
  }
}

extension AppleMapView: MapViewProtocol {
  func willAppear(_ context: Context) {
    mapViewEnvironment.$value1
      .filter { $0 == "aaa" }
      .sink {
        print("=============== apple 1111 \($0)")
        self.aaa()
      }.store(in: &context.coordinator.cancellable)

    mapViewEnvironment.$value1
      .filter { $0 == "bbb" }
      .sink {
        print("=============== apple 1111 \($0)")
        self.bbb()
      }.store(in: &context.coordinator.cancellable)

    mapViewEnvironment.value2
      .filter { $0 == "aaa" }
      .sink {
        print("--------------- apple 2222 \($0)")
        self.aaa()
      }.store(in: &context.coordinator.cancellable)

    mapViewEnvironment.value2
      .filter { $0 == "bbb" }
      .sink {
        print("--------------- apple 2222 \($0)")
        self.bbb()
      }.store(in: &context.coordinator.cancellable)
  }

  func aaa() {
    print("AppleMapView - aaa")
  }

  func bbb() {
    print("AppleMapView - bbb")
  }
}

일반 타입의 변수인 value1 에 @Published를 붙이면 CurrentValueSubject 형태로 바뀌면서 구독하자마자 현재 값이 표시되는데, Rx의 BehaviorSubject와 같다고 생각하면 되고, PassthroughSubject 타입인 value2는 구독 후에 값이 새로 들어올 때부터 발행되는 것이 Rx의 PublishSubject라고 보면 될 것 같다.