UIViewRepresentableを外部から更新したいときに困った
おはようございます。ママさんエンジニアのトモヨです。
現在、SwiftUIで地図を用いたアプリを作成中ですが、表示中の領域の距離を画面に表示する方法に困ったことがあったので記載します。
環境
Xcode13.3
Swift5
やりたいこと
画面に表示されている地図の東西の距離を表示する
ズームイン、ズームアウトでリアルタイムで反映させる。
クラス構成
MapView ・・・delegate通知を受け取る。画面端から端の東西の距離を計算する。viewModelへ距離を通知する(send)
ViewModel・・・距離を受け取る
View・・・表示する
// MapView
import SwiftUI
import MapKit
import Combine
import UIKit
struct MapView: UIViewRepresentable {
@ObservedObject var viewModel: MapViewModel
private var coordinate: CLLocationCoordinate2D
private let mapView = MKMapView(frame: .zero)
private let defaultMeter = CLLocationDistance(TokyoDomeInfo.diameter)
init(viewModel: MapViewModel) {
self.viewModel = viewModel
self.coordinate = viewModel.mapCenter
}
func makeUIView(context: Context) -> MKMapView {
// 略
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
let region = MKCoordinateRegion(center: self.coordinate, latitudinalMeters: defaultMeter, longitudinalMeters: defaultMeter)
view.setRegion(region, animated: true)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
parent.viewModel.distanceSubject.send(mapView.regionInMeter() / 2 as Double)
parent.viewModel.centerLocation = mapView.centerCoordinate
}
}
}
extension MKMapView {
// 画面上に表示されている東西の距離(m)
func regionInMeter() -> CLLocationDistance {
return region.span.latitudeDelta * 111 * 1000
}
}
// ViewModel
import CoreLocation
import Combine
final class MapViewModel: ObservableObject {
var eastAndWestDistance: CLLocationDistance
var centerLocation = CLLocationCoordinate2D()
private(set) var distanceSubject = PassthroughSubject<Double, Never>()
private var cancellableSet = Set<AnyCancellable>()
init() {
distanceSubject
.throttle(for: .seconds(0.1), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] distance in
guard let self = self else { return }
eastAndWestDistance = distance
}.store(in: &cancellableSet) }
}
// View
Text("\(viewModel.eastAndWestDistance)")
困ったこと
mapViewDidChangeVisibleRegionが呼ばれたときに内部でupdateUIViewが呼ばれ、mapViewDidChangeVisibleRegionが再度呼ばれ・・・とループする
対応したこと
ViewModelにフラグを持たせる
// ViewModel
import CoreLocation
import Combine
final class MapViewModel: ObservableObject {
// MapViewのupdateUIViewを呼ばれないようにするフラグ
// updateUIViewで制御しないとmapViewDidChangeVisibleRegionが呼ばれたときにupdateUIViewが呼ばれループする
var shouldUpdateView = true
var eastAndWestDistance: CLLocationDistance
var centerLocation = CLLocationCoordinate2D()
private(set) var distanceSubject = PassthroughSubject<Double, Never>()
private var cancellableSet = Set<AnyCancellable>()
init() {
distanceSubject
.throttle(for: .seconds(0.1), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] distance in
guard let self = self else { return }
eastAndWestDistance = distance
}.store(in: &cancellableSet) }
}
// MapView
import SwiftUI
import MapKit
import Combine
import UIKit
struct MapView: UIViewRepresentable {
@ObservedObject var viewModel: MapViewModel
private var coordinate: CLLocationCoordinate2D
private let mapView = MKMapView(frame: .zero)
private let defaultMeter = CLLocationDistance(TokyoDomeInfo.diameter)
init(viewModel: MapViewModel) {
self.viewModel = viewModel
self.coordinate = viewModel.mapCenter
}
func makeUIView(context: Context) -> MKMapView {
// 略
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
guard viewModel.shouldUpdateView else {
viewModel.shouldUpdateView = true
return
}
let region = MKCoordinateRegion(center: self.coordinate, latitudinalMeters: defaultMeter, longitudinalMeters: defaultMeter)
view.setRegion(region, animated: true)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
// updateUIView内の処理をしないようにする
parent.viewModel.shouldUpdateView = false
parent.viewModel.distanceSubject.send(mapView.regionInMeter() / 2 as Double)
parent.viewModel.centerLocation = mapView.centerCoordinate
}
}
}
原始的なのですが、今ではこの対応しかなさそうです。
この記事が気に入ったらサポートをしてみませんか?