From e44697a6eb3e4f5bc8cd04459a4cf132c5e0d7b5 Mon Sep 17 00:00:00 2001 From: Ivan Persidsky Date: Wed, 17 Apr 2024 18:02:15 +0300 Subject: [PATCH] Allow to add slots at runtime (#2112) --- .../Examples.xcodeproj/project.pbxproj | 20 ++-- .../xcshareddata/xcschemes/Examples.xcscheme | 15 +++ .../All Examples/RuntimeSlotsExample.swift | 64 +++++++++++ Apps/Examples/Examples/Models/Examples.swift | 12 +- CHANGELOG.md | 1 + .../Documentation.docc/API Catalogs/Layers.md | 1 + .../Style/Generated/LayerWrapper.swift | 17 +++ .../Style/Generated/Layers/SlotLayer.swift | 68 ++++++++++++ Sources/MapboxMaps/Style/LayerType.swift | 3 + .../Layers/SlotLayerIntegrationTests.swift | 49 +++++++++ .../Generated/Layers/SlotLayerTests.swift | 104 ++++++++++++++++++ 11 files changed, 345 insertions(+), 9 deletions(-) create mode 100644 Apps/Examples/Examples/All Examples/RuntimeSlotsExample.swift create mode 100644 Sources/MapboxMaps/Style/Generated/Layers/SlotLayer.swift create mode 100644 Tests/MapboxMapsTests/Style/Generated/IntegrationTests/Layers/SlotLayerIntegrationTests.swift create mode 100644 Tests/MapboxMapsTests/Style/Generated/Layers/SlotLayerTests.swift diff --git a/Apps/Examples/Examples.xcodeproj/project.pbxproj b/Apps/Examples/Examples.xcodeproj/project.pbxproj index 4397f9bee7c2..8c5e25833d09 100644 --- a/Apps/Examples/Examples.xcodeproj/project.pbxproj +++ b/Apps/Examples/Examples.xcodeproj/project.pbxproj @@ -113,6 +113,7 @@ C953F022C91FCA59CFF06BE9 /* SymbolClusteringExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FD38B8D20C0695A03AE4E68 /* SymbolClusteringExample.swift */; platformFilters = (ios, ); }; CA2209956E93ECB18C4C9DEC /* CircleAnnotationExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1658938A5F0908D151A9B9 /* CircleAnnotationExample.swift */; platformFilters = (ios, ); }; CBCC60FF68BE9754DE0C6AF3 /* DynamicStylingExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61CC711054A032EE0446036 /* DynamicStylingExample.swift */; }; + CBD01BBA4E78796827A6E52D /* RuntimeSlotsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AB4D202692A8951BAF2D57 /* RuntimeSlotsExample.swift */; platformFilters = (ios, ); }; CF5C5513D659D4981706DDEC /* ViewAnnotationAnimationExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33E5647315A2357320BCF575 /* ViewAnnotationAnimationExample.swift */; platformFilters = (ios, ); }; D27F0573360A7234BCF7AB6C /* AnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC8E7B565C817D872CFBCAD /* AnnotationView.swift */; platformFilters = (ios, ); }; D4FFFAE49D4B805BDA014AAD /* BasicMapExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 257F10278F5E580C7ABA5570 /* BasicMapExample.swift */; platformFilters = (ios, ); }; @@ -217,6 +218,7 @@ 62DA0608D44DEF6C4A82777C /* LocateMeExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocateMeExample.swift; sourceTree = ""; }; 640198169EEDFC7CBEFCFCCF /* StandardStyleImportExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardStyleImportExample.swift; sourceTree = ""; }; 65535FB9F190778001AB847A /* MapScrollExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapScrollExample.swift; sourceTree = ""; }; + 65AB4D202692A8951BAF2D57 /* RuntimeSlotsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeSlotsExample.swift; sourceTree = ""; }; 67FA937DC17F7EA27931763A /* RasterTileSourceExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RasterTileSourceExample.swift; sourceTree = ""; }; 6A090EE21C9A65BE2697868F /* NavigationSimulatorExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSimulatorExample.swift; sourceTree = ""; }; 6BB52F9D3A810B1A9CEC832C /* Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example.swift; sourceTree = ""; }; @@ -516,6 +518,7 @@ 289434058C4AB25A17655FEF /* PointClusteringExample.swift */, 98D66333697502A83B85BCD9 /* RasterColorExample.swift */, 67FA937DC17F7EA27931763A /* RasterTileSourceExample.swift */, + 65AB4D202692A8951BAF2D57 /* RuntimeSlotsExample.swift */, 267AA1719061B281479BBBCB /* SceneKitExample.swift */, 93B8372871DB4BC991737A06 /* SkyLayerExample.swift */, D77368780E4DBCCFB2CBD400 /* SnapshotterCoreGraphicsExample.swift */, @@ -597,32 +600,32 @@ path = Lab; sourceTree = ""; }; - "TEMP_3B22CCC3-D6FC-48F0-9141-CC0AB3CBDA8B" /* All Examples */ = { + "TEMP_588CFFC1-4749-481B-BBDF-B659936E47B4" /* Sample Data */ = { isa = PBXGroup; children = ( ); - path = "All Examples"; + path = "Sample Data"; sourceTree = ""; }; - "TEMP_612F4B06-1BB2-450B-859E-16B56798EB12" /* SwiftUI */ = { + "TEMP_61A0AF9A-95B5-4FBE-96DA-E01092702FA7" /* iOS */ = { isa = PBXGroup; children = ( ); - path = SwiftUI; + path = iOS; sourceTree = ""; }; - "TEMP_65DC9708-DA51-4826-9E66-BF0BB7DD0378" /* iOS */ = { + "TEMP_C82D4BA9-D5D7-49FA-9898-1B62501CB97E" /* All Examples */ = { isa = PBXGroup; children = ( ); - path = iOS; + path = "All Examples"; sourceTree = ""; }; - "TEMP_E0F08E73-FFA3-4E93-BDDA-E10D0C39641C" /* Sample Data */ = { + "TEMP_E73E7D7D-B57B-4230-AB47-9D04225FAD33" /* SwiftUI */ = { isa = PBXGroup; children = ( ); - path = "Sample Data"; + path = SwiftUI; sourceTree = ""; }; /* End PBXGroup section */ @@ -893,6 +896,7 @@ E8CEBC697D805204F129C4FB /* RasterTileSourceExample.swift in Sources */, 191391C51FC69A6D36EB67F0 /* ResizableImageExample.swift in Sources */, 86AED5DD9F8C8BB2C9736483 /* ResizeMapViewExample.swift in Sources */, + CBD01BBA4E78796827A6E52D /* RuntimeSlotsExample.swift in Sources */, 4791CACAC0846107E4B0955B /* SceneKitExample.swift in Sources */, F613749DCDDDDC6F041032A0 /* SimpleMapExample.swift in Sources */, 1F860D5B445E75772C4C3B6C /* SkyLayerExample.swift in Sources */, diff --git a/Apps/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Examples.xcscheme b/Apps/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Examples.xcscheme index 829d5d93ab13..5884e48d2808 100644 --- a/Apps/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Examples.xcscheme +++ b/Apps/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Examples.xcscheme @@ -87,6 +87,11 @@ value = "1" isEnabled = "NO"> + + + + + + LayerAtPosition { + LayerAtPosition(layer: self, position: position) + } + + func visit(_ node: MapContentNode) { + node.mount(MountedLayer(layer: self)) + } +} + @_spi(Experimental) @available(iOS 13.0, *) extension LocationIndicatorLayer: MapStyleContent, PrimitiveMapContent { diff --git a/Sources/MapboxMaps/Style/Generated/Layers/SlotLayer.swift b/Sources/MapboxMaps/Style/Generated/Layers/SlotLayer.swift new file mode 100644 index 000000000000..0600c2cf635f --- /dev/null +++ b/Sources/MapboxMaps/Style/Generated/Layers/SlotLayer.swift @@ -0,0 +1,68 @@ +// This file is generated. +import UIKit + +/// Marks the position of a slot. +/// +/// - SeeAlso: [Mapbox Style Specification](https://www.mapbox.com/mapbox-gl-style-spec/#layers-slot) +public struct SlotLayer: Layer, Equatable { + + // MARK: - Conformance to `Layer` protocol + /// Unique layer name + public var id: String + + /// Rendering type of this layer. + public let type: LayerType + + /// The slot this layer is assigned to. If specified, and a slot with that name exists, it will be placed at that position in the layer order. + public var slot: Slot? + + /// No-op for slot layer. + public var minZoom: Double? + + /// No-op for slot layer. + public var maxZoom: Double? + + /// No-op for slot layer. + public var visibility: Value + + public init(id: String) { + self.id = id + self.type = LayerType.slot + self.visibility = .constant(.visible) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: RootCodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(type, forKey: .type) + try container.encodeIfPresent(slot, forKey: .slot) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: RootCodingKeys.self) + id = try container.decode(String.self, forKey: .id) + type = try container.decode(LayerType.self, forKey: .type) + slot = try container.decodeIfPresent(Slot.self, forKey: .slot) + visibility = .constant(.visible) + } + + enum RootCodingKeys: String, CodingKey { + case id = "id" + case type = "type" + case slot = "slot" + } +} + +@_documentation(visibility: public) +@_spi(Experimental) extension SlotLayer { + + /// The slot this layer is assigned to. + /// If specified, and a slot with that name exists, it will be placed at that position in the layer order. + @_documentation(visibility: public) + public func slot(_ newValue: Slot?) -> Self { + with(self, setter(\.slot, newValue)) + } + +} + +// End of generated file. diff --git a/Sources/MapboxMaps/Style/LayerType.swift b/Sources/MapboxMaps/Style/LayerType.swift index 9ee832f46185..65cfdd748829 100644 --- a/Sources/MapboxMaps/Style/LayerType.swift +++ b/Sources/MapboxMaps/Style/LayerType.swift @@ -41,6 +41,9 @@ public struct LayerType: ExpressibleByStringLiteral, RawRepresentable, Codable, /// Layer representing the sky public static let sky: LayerType = "sky" + /// Layer representing a place for other layers. + public static let slot: LayerType = "slot" + /// Layer used for a 3D model @_documentation(visibility: public) @_spi(Experimental) diff --git a/Tests/MapboxMapsTests/Style/Generated/IntegrationTests/Layers/SlotLayerIntegrationTests.swift b/Tests/MapboxMapsTests/Style/Generated/IntegrationTests/Layers/SlotLayerIntegrationTests.swift new file mode 100644 index 000000000000..3793925fdc1c --- /dev/null +++ b/Tests/MapboxMapsTests/Style/Generated/IntegrationTests/Layers/SlotLayerIntegrationTests.swift @@ -0,0 +1,49 @@ +// This file is generated +import XCTest +@testable import MapboxMaps + +final class SlotLayerIntegrationTests: MapViewIntegrationTestCase { + + internal func testBaseClass() throws { + // Do nothing + } + + internal func testWaitForIdle() throws { + let successfullyAddedLayerExpectation = XCTestExpectation(description: "Successfully added SlotLayer to Map") + successfullyAddedLayerExpectation.expectedFulfillmentCount = 1 + + let successfullyRetrievedLayerExpectation = XCTestExpectation(description: "Successfully retrieved SlotLayer from Map") + successfullyRetrievedLayerExpectation.expectedFulfillmentCount = 1 + + mapView.mapboxMap.styleURI = .streets + + didFinishLoadingStyle = { mapView in + + var layer = SlotLayer(id: "test-id") + layer.minZoom = 10.0 + layer.maxZoom = 20.0 + layer.visibility = .constant(.visible) + + + // Add the layer + do { + try mapView.mapboxMap.addLayer(layer) + successfullyAddedLayerExpectation.fulfill() + } catch { + XCTFail("Failed to add SlotLayer because of error: \(error)") + } + + // Retrieve the layer + do { + _ = try mapView.mapboxMap.layer(withId: "test-id", type: SlotLayer.self) + successfullyRetrievedLayerExpectation.fulfill() + } catch { + XCTFail("Failed to retrieve SlotLayer because of error: \(error)") + } + } + + wait(for: [successfullyAddedLayerExpectation, successfullyRetrievedLayerExpectation], timeout: 5.0) + } +} + +// End of generated file diff --git a/Tests/MapboxMapsTests/Style/Generated/Layers/SlotLayerTests.swift b/Tests/MapboxMapsTests/Style/Generated/Layers/SlotLayerTests.swift new file mode 100644 index 000000000000..9023eab63c6f --- /dev/null +++ b/Tests/MapboxMapsTests/Style/Generated/Layers/SlotLayerTests.swift @@ -0,0 +1,104 @@ +// This file is generated +import XCTest +@_spi(Experimental) @testable import MapboxMaps + +final class SlotLayerTests: XCTestCase { + + func testLayerProtocolMembers() { + + var layer = SlotLayer(id: "test-id") + layer.minZoom = 10.0 + layer.maxZoom = 20.0 + layer.slot = .testConstantValue() + + XCTAssertEqual(layer.id, "test-id") + XCTAssertEqual(layer.type, LayerType.slot) + XCTAssertEqual(layer.minZoom, 10.0) + XCTAssertEqual(layer.maxZoom, 20.0) + XCTAssertEqual(layer.slot, Slot.testConstantValue()) + } + + func testEncodingAndDecodingOfLayerProtocolProperties() { + var layer = SlotLayer(id: "test-id") + layer.minZoom = 10.0 + layer.maxZoom = 20.0 + layer.slot = .testConstantValue() + + var data: Data? + do { + data = try JSONEncoder().encode(layer) + } catch { + XCTFail("Failed to encode SlotLayer") + } + + guard let validData = data else { + XCTFail("Failed to encode SlotLayer") + return + } + + do { + let decodedLayer = try JSONDecoder().decode(SlotLayer.self, from: validData) + XCTAssertEqual(decodedLayer.id, "test-id") + XCTAssertEqual(decodedLayer.type, LayerType.slot) + XCTAssertEqual(layer.slot, Slot.testConstantValue()) + } catch { + XCTFail("Failed to decode SlotLayer") + } + } + + func testEncodingAndDecodingOfLayoutProperties() { + var layer = SlotLayer(id: "test-id") + layer.visibility = .constant(.visible) + + var data: Data? + do { + data = try JSONEncoder().encode(layer) + } catch { + XCTFail("Failed to encode SlotLayer") + } + + guard let validData = data else { + XCTFail("Failed to encode SlotLayer") + return + } + + do { + let decodedLayer = try JSONDecoder().decode(SlotLayer.self, from: validData) + XCTAssert(decodedLayer.visibility == .constant(.visible)) + } catch { + XCTFail("Failed to decode SlotLayer") + } + } + + func testEncodingAndDecodingOfPaintProperties() { + var layer = SlotLayer(id: "test-id") + + var data: Data? + do { + data = try JSONEncoder().encode(layer) + } catch { + XCTFail("Failed to encode SlotLayer") + } + + guard let validData = data else { + XCTFail("Failed to encode SlotLayer") + return + } + + do { + let decodedLayer = try JSONDecoder().decode(SlotLayer.self, from: validData) + XCTAssert(decodedLayer.visibility == .constant(.visible)) + } catch { + XCTFail("Failed to decode SlotLayer") + } + } + + func testSetPropertyValueWithFunction() { + let layer = SlotLayer(id: "test-id") + .slot(Slot.testConstantValue()) + + XCTAssertEqual(layer.slot, Slot.testConstantValue()) + } +} + +// End of generated file