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() }