From d91763b867271cec1484b6a06f296535bfc2259e Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Fri, 6 Nov 2026 18:01:10 -0800 Subject: [PATCH] Keep an explicitly set markerTilingOptions through the content builder MapViewContent.merge carried markerTilingOptions over only when it was enabled, because a fresh content's .Default and a deliberate .Disabled could not be told apart. Every builder closure goes through buildBlock, which merges into a fresh content, so .Disabled set inside the builder (e.g. the sample's `{ var c = MapViewContent(); ...; return c }()` form) came out as the default and the markers were tiled anyway. Returning the content from the map view's closure with `return` skipped the builder, which is why that form worked. markerTilingOptions now records whether it was set. merge takes an explicit setting from the other side whatever its value, so the later explicit one wins, and leaves an unset one alone, which still reads .Default. It is the only non-collection field of MapViewContent; the others are appended. The public property keeps its type and get/set; the API baseline only changes from a stored to a computed property. --- Sources/MapConductorCore/MapViewContent.swift | 23 +++- .../MapViewContentMergeTests.swift | 107 ++++++++++++++++++ api/MapConductorCore.api.swift | 5 +- 3 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 Tests/MapConductorCoreTests/MapViewContentMergeTests.swift diff --git a/Sources/MapConductorCore/MapViewContent.swift b/Sources/MapConductorCore/MapViewContent.swift index 82696f2..57353e7 100644 --- a/Sources/MapConductorCore/MapViewContent.swift +++ b/Sources/MapConductorCore/MapViewContent.swift @@ -24,7 +24,22 @@ public struct MapViewContent { public var groundImages: [GroundImage] = [] public var rasterLayers: [RasterLayer] = [] public var views: [AnyView] = [] - public var markerTilingOptions: MarkerTilingOptions = .Default + /// Marker tiling for this content. Reads ``MarkerTilingOptions/Default`` until something sets it. + /// + /// Whether it was set is tracked separately from its value, so that merging the pieces a + /// ``MapViewContentBuilder`` closure produces keeps an explicit choice — including + /// ``MarkerTilingOptions/Disabled`` — while pieces that never touched it leave it alone. + public var markerTilingOptions: MarkerTilingOptions { + get { explicitMarkerTilingOptions ?? .Default } + set { explicitMarkerTilingOptions = newValue } + } + /// `nil` until ``markerTilingOptions`` is assigned. + /// + /// `merge` used to carry the option over only when it was enabled, because a fresh + /// content's `.Default` and a deliberate `.Disabled` could not be told apart otherwise. + /// A single-expression builder closure always goes through `buildBlock`, which merges + /// into a fresh content, so `.Disabled` was silently replaced by the default (tiling on). + var explicitMarkerTilingOptions: MarkerTilingOptions? /// Handlers that manage a subset of polygons imperatively (e.g. cluster hull polygons). /// Map view coordinators call ``PolygonSyncHandler/bindPolygonSync(_:)`` on each handler /// to provide a direct polygon sync function, ensuring those polygons are committed before @@ -47,8 +62,10 @@ public struct MapViewContent { rasterLayers.append(contentsOf: other.rasterLayers) views.append(contentsOf: other.views) polygonSyncHandlers.append(contentsOf: other.polygonSyncHandlers) - if other.markerTilingOptions.enabled { - markerTilingOptions = other.markerTilingOptions + // An explicit setting wins over an unset one, whether it enables tiling or not; + // between two explicit settings the later one wins. + if let tiling = other.explicitMarkerTilingOptions { + explicitMarkerTilingOptions = tiling } } } diff --git a/Tests/MapConductorCoreTests/MapViewContentMergeTests.swift b/Tests/MapConductorCoreTests/MapViewContentMergeTests.swift new file mode 100644 index 0000000..060dde5 --- /dev/null +++ b/Tests/MapConductorCoreTests/MapViewContentMergeTests.swift @@ -0,0 +1,107 @@ +import XCTest + +@testable import MapConductorCore + +/// `MapViewContentBuilder` が組み立てた content で、明示した設定が残ることを確かめる。 +/// +/// 以前の `merge` は有効なタイル設定しか引き継がず、ビルダーの中で +/// `markerTilingOptions = .Disabled` を設定しても既定(タイル有効)に戻っていた。 +/// 1 式だけのビルダーも `buildBlock` で新しい content にマージされるので必ず踏む。 +final class MapViewContentMergeTests: XCTestCase { + private func build(@MapViewContentBuilder _ body: () -> MapViewContent) -> MapViewContent { + body() + } + + private func content(tiling: MarkerTilingOptions?) -> MapViewContent { + var content = MapViewContent() + if let tiling { content.markerTilingOptions = tiling } + return content + } + + func testUnsetTilingReadsDefault() { + let content = MapViewContent() + XCTAssertTrue(content.markerTilingOptions.enabled) + XCTAssertEqual(content.markerTilingOptions.minMarkerCount, MarkerTilingOptions.Default.minMarkerCount) + XCTAssertNil(content.explicitMarkerTilingOptions) + } + + /// 報告された形そのもの: ビルダーの中で即時実行クロージャが `.Disabled` を設定する。 + func testDisabledSetInsideBuilderSurvives() { + let built = build { + { () -> MapViewContent in + var content = MapViewContent() + content.markerTilingOptions = .Disabled + return content + }() + } + XCTAssertFalse(built.markerTilingOptions.enabled) + } + + func testDisabledSurvivesNextToItemsThatDoNotSetTiling() { + let built = build { + content(tiling: .Disabled) + content(tiling: nil) + Marker(state: MarkerState(position: GeoPoint(latitude: 35, longitude: 139))) + } + XCTAssertFalse(built.markerTilingOptions.enabled) + XCTAssertEqual(built.markers.count, 1) + } + + func testEnabledCustomOptionsSurvive() { + let built = build { + content(tiling: MarkerTilingOptions(minMarkerCount: 10, declutterPx: 4)) + content(tiling: nil) + } + XCTAssertTrue(built.markerTilingOptions.enabled) + XCTAssertEqual(built.markerTilingOptions.minMarkerCount, 10) + XCTAssertEqual(built.markerTilingOptions.declutterPx, 4) + } + + func testUnsetEverywhereFallsBackToDefault() { + let built = build { + content(tiling: nil) + Marker(state: MarkerState(position: GeoPoint(latitude: 35, longitude: 139))) + } + XCTAssertTrue(built.markerTilingOptions.enabled) + XCTAssertNil(built.explicitMarkerTilingOptions) + } + + func testLaterExplicitSettingWins() { + let disabledLast = build { + content(tiling: MarkerTilingOptions(minMarkerCount: 10)) + content(tiling: .Disabled) + } + XCTAssertFalse(disabledLast.markerTilingOptions.enabled) + + let enabledLast = build { + content(tiling: .Disabled) + content(tiling: MarkerTilingOptions(minMarkerCount: 10)) + } + XCTAssertTrue(enabledLast.markerTilingOptions.enabled) + XCTAssertEqual(enabledLast.markerTilingOptions.minMarkerCount, 10) + } + + func testDisabledSurvivesConditionalsAndLoops() { + let flag = true + let built = build { + if flag { + content(tiling: .Disabled) + } + for _ in 0..<2 { + content(tiling: nil) + } + } + XCTAssertFalse(built.markerTilingOptions.enabled) + + let nested = build { + for i in 0..<3 { + if i == 1 { + content(tiling: .Disabled) + } else { + content(tiling: nil) + } + } + } + XCTAssertFalse(nested.markerTilingOptions.enabled) + } +} diff --git a/api/MapConductorCore.api.swift b/api/MapConductorCore.api.swift index ecc15d9..54c7308 100644 --- a/api/MapConductorCore.api.swift +++ b/api/MapConductorCore.api.swift @@ -249,7 +249,10 @@ public struct MapViewContent { public var groundImages: [MapConductorCore::GroundImage] public var rasterLayers: [MapConductorCore::RasterLayer] public var views: [SwiftUICore::AnyView] - public var markerTilingOptions: MapConductorCore::MarkerTilingOptions + public var markerTilingOptions: MapConductorCore::MarkerTilingOptions { + get + set + } public var polygonSyncHandlers: [any MapConductorCore::PolygonSyncHandler] public init() }