From 68cfbe9fb2068e0019411b3e79c55c6c2785a9e6 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 18:53:23 -0800 Subject: [PATCH 1/2] Make clearOverlays() clear the overlay controllers clearOverlays() only removed native annotations (or map items, or did nothing at all), leaving every MapConductor overlay on the map and in its manager. It now calls clear() on the marker, polyline, polygon, ground image, circle and raster layer controllers, as android-sdk does. The controllers live in the host, so the host hands the view controller a handler when it creates them. --- Sources/MapConductorForMapbox/MapboxMapHost.swift | 12 ++++++++++++ .../controller/MapboxViewController.swift | 8 +++++++- .../groundimage/MapboxGroundImageController.swift | 11 +++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/Sources/MapConductorForMapbox/MapboxMapHost.swift b/Sources/MapConductorForMapbox/MapboxMapHost.swift index 8c79e77..afc2457 100644 --- a/Sources/MapConductorForMapbox/MapboxMapHost.swift +++ b/Sources/MapConductorForMapbox/MapboxMapHost.swift @@ -218,6 +218,7 @@ public final class MapboxMapHost: MapViewCoordinatorBase { controller.registerOverlayController(polygonController) controller.registerOverlayController(groundImageController) controller.registerOverlayController(rasterController) + controller.clearOverlaysHandler = { [weak self] in await self?.clearOverlays() } let overlayScope = MapOverlayScope() self.overlayScope = overlayScope @@ -303,6 +304,17 @@ public final class MapboxMapHost: MapViewCoordinatorBase { } } + /// android-sdk の `clearOverlays()` と同じく、各オーバーレイのコントローラを空にする。 + /// `controller.clearOverlaysHandler` から呼ばれる。 + func clearOverlays() async { + await markerController?.clear() + await polylineController?.clear() + await polygonController?.clear() + await groundImageController?.clear() + await circleController?.clear() + await rasterController?.clear() + } + public func unbind() { // 登録した capability を取り下げる。レジストリの持ち主は state で、ビューより長生きするため、 // ここで外さないと破棄済みのコントローラを掴んだまま残る。 diff --git a/Sources/MapConductorForMapbox/controller/MapboxViewController.swift b/Sources/MapConductorForMapbox/controller/MapboxViewController.swift index 781794a..ee932f5 100644 --- a/Sources/MapConductorForMapbox/controller/MapboxViewController.swift +++ b/Sources/MapConductorForMapbox/controller/MapboxViewController.swift @@ -33,7 +33,13 @@ final class MapboxViewController: MapViewControllerProtocol { self.holder = AnyMapViewHolder(typedHolder) } - func clearOverlays() async {} + /// ``clearOverlays()`` の中身。オーバーレイのコントローラはホストが持っているので、 + /// ホストが生成時に設定する(android-sdk と同じく、各コントローラの `clear()` を呼ぶ)。 + var clearOverlaysHandler: (@MainActor () async -> Void)? + + func clearOverlays() async { + await clearOverlaysHandler?() + } func setCameraMoveStartListener(listener: OnCameraMoveHandler?) { cameraMoveStartListener = listener } func setCameraMoveListener(listener: OnCameraMoveHandler?) { cameraMoveListener = listener } diff --git a/Sources/MapConductorForMapbox/groundimage/MapboxGroundImageController.swift b/Sources/MapConductorForMapbox/groundimage/MapboxGroundImageController.swift index 31c75b2..52c3057 100644 --- a/Sources/MapConductorForMapbox/groundimage/MapboxGroundImageController.swift +++ b/Sources/MapConductorForMapbox/groundimage/MapboxGroundImageController.swift @@ -116,6 +116,17 @@ final class MapboxGroundImageController: SlottedOverlayController { } } + /// `clearOverlays()` 用。描画済みの画像を外して空にする(地図と renderer は残す)。 + /// + /// コアの GroundImageController を継承していないので、他の種類と同じ `clear()` をここで持つ。 + /// 空の一式を styleGate へ渡すので、スタイル読み込み前でも後でも、読み込み直しても戻らない。 + func clear() async { + groundImageSubscriptions.values.forEach { $0.cancel() } + groundImageSubscriptions.removeAll() + groundImageStatesById.removeAll() + styleGate.submit([]) + } + func unbind() { groundImageSubscriptions.values.forEach { $0.cancel() } groundImageSubscriptions.removeAll() From 14ca29a7f941109c4cbd8cf6d2433a2363b8a3be Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 20:27:13 -0800 Subject: [PATCH 2/2] Keep a cleared ground image cleared until the content changes clear() forgot the states it had been sent, so the next syncGroundImages -- SwiftUI re-evaluates the body right after clearOverlays() -- saw them as new and drew them again. The controller now remembers the set that was on screen at clear() and ignores the same instances sent again, like the core OverlayCollector: only a change of membership or a rebuilt state redraws the whole set. --- .../groundimage/MapboxGroundImageController.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Sources/MapConductorForMapbox/groundimage/MapboxGroundImageController.swift b/Sources/MapConductorForMapbox/groundimage/MapboxGroundImageController.swift index 52c3057..46e798c 100644 --- a/Sources/MapConductorForMapbox/groundimage/MapboxGroundImageController.swift +++ b/Sources/MapConductorForMapbox/groundimage/MapboxGroundImageController.swift @@ -9,6 +9,13 @@ final class MapboxGroundImageController: SlottedOverlayController { private var groundImageSubscriptions: [String: AnyCancellable] = [:] private var groundImageStatesById: [String: GroundImageState] = [:] + /// `clear()` の時点で送られていた一式。同じ一式が送り直されている間は描かない。 + /// + /// core の `OverlayCollector` と振る舞いをそろえるため。あちらは clear 後も一式を覚えていて、 + /// 同じインスタンスの再送では何もせず、顔ぶれが変わったか作り直された state が来たときだけ + /// 全部を描き直す。これが無いと、SwiftUI が body を再評価するたび(clear の直後にも来る)に + /// 消したはずの画像が戻っていた。 + private var clearedStates: [String: GroundImageState]? /// スタイル読み込み待ちの取り込み。捨てずに保留し、`onStyleLoaded` で流す。 /// 「なぜ待つ必要があるか」は `DeferredUntilReady` の説明にある。 private lazy var styleGate = DeferredUntilReady<[GroundImageState]> { [weak self] states in @@ -34,6 +41,12 @@ final class MapboxGroundImageController: SlottedOverlayController { } func syncGroundImages(_ groundImages: [GroundImage]) { + if let cleared = clearedStates { + let resent = groundImages.count == cleared.count + && groundImages.allSatisfy { cleared[$0.state.id] === $0.state } + if resent { return } + clearedStates = nil + } let newIds = Set(groundImages.map { $0.id }) let oldIds = Set(groundImageStatesById.keys) var newStatesById: [String: GroundImageState] = [:] @@ -123,6 +136,7 @@ final class MapboxGroundImageController: SlottedOverlayController { func clear() async { groundImageSubscriptions.values.forEach { $0.cancel() } groundImageSubscriptions.removeAll() + clearedStates = groundImageStatesById groundImageStatesById.removeAll() styleGate.submit([]) } @@ -131,6 +145,7 @@ final class MapboxGroundImageController: SlottedOverlayController { groundImageSubscriptions.values.forEach { $0.cancel() } groundImageSubscriptions.removeAll() groundImageStatesById.removeAll() + clearedStates = nil styleGate.reset() renderer.unbind() groundImageManager.destroy()