diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5448d44..5b6a42c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release Maplibre-SwiftUI +name: Release on: push: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b810ec5..9275cb5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Test maplibre-swiftui-dsl-playground +name: Test on: push: @@ -7,6 +7,19 @@ on: branches: [ main ] jobs: + format-lint: + runs-on: macos-14 + + steps: + - name: Install tools + run: brew install swiftformat + + - name: Checkout maplibre-swiftui-dsl-playground + uses: actions/checkout@v3 + + - name: Check format + run: swiftformat . --lint + test: runs-on: macos-14 strategy: @@ -20,15 +33,15 @@ jobs: ] steps: - - name: Install xcbeautify + - name: Install tools run: brew install xcbeautify - + - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: '15.0' - name: Checkout maplibre-swiftui-dsl-playground uses: actions/checkout@v3 - + - name: Test ${{ matrix.scheme }} on ${{ matrix.destination }} run: xcodebuild -scheme ${{ matrix.scheme }} test -skipMacroValidation -destination '${{ matrix.destination }}' | xcbeautify && exit ${PIPESTATUS[0]} diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..8229b0f --- /dev/null +++ b/.swiftformat @@ -0,0 +1,15 @@ +# file options + +--exclude .build + +# format options + +--header "" +--indent 4 +--importgrouping testable-bottom +--maxwidth 120 +--swiftversion 5.9 + +# rules + +--enable isEmpty \ No newline at end of file diff --git a/Package.swift b/Package.swift index 0035198..432cf36 100644 --- a/Package.swift +++ b/Package.swift @@ -1,8 +1,8 @@ // swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. -import PackageDescription import CompilerPluginSupport +import PackageDescription let package = Package( name: "MapLibreSwiftUI", @@ -13,10 +13,12 @@ let package = Package( products: [ .library( name: "MapLibreSwiftUI", - targets: ["MapLibreSwiftUI"]), + targets: ["MapLibreSwiftUI"] + ), .library( name: "MapLibreSwiftDSL", - targets: ["MapLibreSwiftDSL"]), + targets: ["MapLibreSwiftDSL"] + ), ], dependencies: [ .package(url: "https://github.com/maplibre/maplibre-gl-native-distribution.git", from: "6.1.0"), @@ -32,37 +34,38 @@ let package = Package( .target(name: "InternalUtils"), .target(name: "MapLibreSwiftDSL"), .product(name: "MapLibre", package: "maplibre-gl-native-distribution"), - .product(name: "Mockable", package: "Mockable") + .product(name: "Mockable", package: "Mockable"), ], swiftSettings: [ - .define("MOCKING", .when(configuration: .debug)) - ]), + .define("MOCKING", .when(configuration: .debug)), + ] + ), .target( name: "MapLibreSwiftDSL", dependencies: [ .target(name: "InternalUtils"), .product(name: "MapLibre", package: "maplibre-gl-native-distribution"), - .product(name: "MapLibreSwiftMacros", package: "maplibre-swift-macros") + .product(name: "MapLibreSwiftMacros", package: "maplibre-swift-macros"), ] ), .target( name: "InternalUtils" ), - + // MARK: Tests - + .testTarget( name: "MapLibreSwiftUITests", dependencies: [ "MapLibreSwiftUI", .product(name: "MockableTest", package: "Mockable"), - .product(name: "SnapshotTesting", package: "swift-snapshot-testing") + .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), ] ), .testTarget( name: "MapLibreSwiftDSLTests", dependencies: [ - "MapLibreSwiftDSL" + "MapLibreSwiftDSL", ] ), ] diff --git a/README.md b/README.md index 9ad876c..cd2ab75 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,20 @@ Then, for each target add either the DSL (for just the DSL) or both (for the Swi Check out the (super basic) [previews at the bottom of MapView.swift](Sources/MapLibreSwiftUI/MapView.swift) or more detailed [Examples](Sources/MapLibreSwiftUI/Examples) to see how it works in practice. +## Developer Quick Start + +This project uses [`swiftformat`](https://github.com/nicklockwood/SwiftFormat) to automatically handle basic swift formatting +as well as to lint and ensure conformance in PRs. Check out the swiftformat [Install Guide](https://github.com/nicklockwood/SwiftFormat?tab=readme-ov-file#how-do-i-install-it) +to add swiftformat to your machine. + +Once installed, you can autoformat code using the command: + +```sh +swiftformat . +``` + +Swiftformat can occasionally poorly resolve a formatting issue (e.g. when you've already line-broken a large comment). Issues like this are typically easy to manually correct. + ## Structure This package is structured into a few targets. `InternalUtils` is pretty much what it says. `MapLibreSwiftDSL` and @@ -85,4 +99,4 @@ things like fleshing out the expresison API helpers (which will be highly repeti check out the issue tracker, as these sholud be noted there. TODO: Look at PointFree's Snapshot testing that generates images of SwiftUI views. Also look at inline snapshotting. -https://github.com/pointfreeco/swift-snapshot-testing +https://github.com/pointfreeco/swift-snapshot-testing \ No newline at end of file diff --git a/Sources/InternalUtils/Utilities.swift b/Sources/InternalUtils/Utilities.swift index c492987..28f21c5 100644 --- a/Sources/InternalUtils/Utilities.swift +++ b/Sources/InternalUtils/Utilities.swift @@ -25,12 +25,12 @@ public func addSourceIfNecessary(_ source: MLNSource, to mlnStyle: MLNStyle) -> } } -extension UIImage{ +public extension UIImage { /// Computes a SHA256 hash of the image data. /// /// This is used internally to generate identifiers for images that can be used in the MapLibre GL /// style which uniquely identify `UIImage`s to the renderer. - public func sha256() -> String{ + func sha256() -> String { if let imageData = cgImage?.dataProvider?.data as? Data { return imageData.digest.hexString } diff --git a/Sources/MapLibreSwiftDSL/Enums.swift b/Sources/MapLibreSwiftDSL/Enums.swift index e47f684..709a714 100644 --- a/Sources/MapLibreSwiftDSL/Enums.swift +++ b/Sources/MapLibreSwiftDSL/Enums.swift @@ -1,5 +1,5 @@ -import MapLibre import InternalUtils +import MapLibre // This file exists for convenience until / unless // this is merged into the MapLibre Native Swift module OR Swift gains the @@ -11,13 +11,12 @@ public enum LineCap { case square } - extension LineCap: MLNRawRepresentable { public var mlnRawValue: MLNLineCap { switch self { - case .butt: return .butt - case .round: return .round - case .square: return .square + case .butt: .butt + case .round: .round + case .square: .square } } } @@ -31,9 +30,9 @@ public enum LineJoin { extension LineJoin: MLNRawRepresentable { public var mlnRawValue: MLNLineJoin { switch self { - case .bevel: return .bevel - case .miter: return .miter - case .round: return .round + case .bevel: .bevel + case .miter: .miter + case .round: .round } } } @@ -52,19 +51,19 @@ extension MLNVariableExpression { var nsExpression: NSExpression { switch self { case .featureAccumulated: - return .featureAccumulatedVariable + .featureAccumulatedVariable case .featureAttributes: - return .featureAttributesVariable + .featureAttributesVariable case .featureIdentifier: - return .featureIdentifierVariable + .featureIdentifierVariable case .geometryType: - return .geometryTypeVariable + .geometryTypeVariable case .heatmapDensity: - return .heatmapDensityVariable + .heatmapDensityVariable case .lineProgress: - return .lineProgressVariable + .lineProgressVariable case .zoomLevel: - return .zoomLevelVariable + .zoomLevelVariable } } } diff --git a/Sources/MapLibreSwiftDSL/Expressions.swift b/Sources/MapLibreSwiftDSL/Expressions.swift index 272b4a4..00f8375 100644 --- a/Sources/MapLibreSwiftDSL/Expressions.swift +++ b/Sources/MapLibreSwiftDSL/Expressions.swift @@ -2,12 +2,16 @@ import Foundation import MapLibre - // TODO: Parameters and stops need nicer interfaces // TODO: Expression should be able to accept other expressions like variable getters. Probably should be a protocol? -public func interpolatingExpression(expression: MLNVariableExpression, curveType: MLNExpressionInterpolationMode, parameters: NSExpression?, stops: NSExpression) -> NSExpression { - return NSExpression(forMLNInterpolating: expression.nsExpression, - curveType: curveType, - parameters: parameters, - stops: stops) +public func interpolatingExpression( + expression: MLNVariableExpression, + curveType: MLNExpressionInterpolationMode, + parameters: NSExpression?, + stops: NSExpression +) -> NSExpression { + NSExpression(forMLNInterpolating: expression.nsExpression, + curveType: curveType, + parameters: parameters, + stops: stops) } diff --git a/Sources/MapLibreSwiftDSL/MapLibre Extensions.swift b/Sources/MapLibreSwiftDSL/MapLibre Extensions.swift index e6a1273..9b17931 100644 --- a/Sources/MapLibreSwiftDSL/MapLibre Extensions.swift +++ b/Sources/MapLibreSwiftDSL/MapLibre Extensions.swift @@ -1,17 +1,17 @@ -// Various quality-of-life extensions to MapLibre APIs. +// Various quality-of-life extensions to MapLibre APIs. import MapLibre // TODO: Upstream this? -extension MLNPolyline { +public extension MLNPolyline { /// Constructs a polyline (aka LineString) from a list of coordinates. - public convenience init(coordinates: [CLLocationCoordinate2D]) { + convenience init(coordinates: [CLLocationCoordinate2D]) { self.init(coordinates: coordinates, count: UInt(coordinates.count)) } } -extension MLNPointFeature { - public convenience init(coordinate: CLLocationCoordinate2D, configure: ((MLNPointFeature) -> Void)? = nil) { +public extension MLNPointFeature { + convenience init(coordinate: CLLocationCoordinate2D, configure: ((MLNPointFeature) -> Void)? = nil) { self.init() self.coordinate = coordinate diff --git a/Sources/MapLibreSwiftDSL/MapViewContentBuilder.swift b/Sources/MapLibreSwiftDSL/MapViewContentBuilder.swift index aa82220..bd28beb 100644 --- a/Sources/MapLibreSwiftDSL/MapViewContentBuilder.swift +++ b/Sources/MapLibreSwiftDSL/MapViewContentBuilder.swift @@ -3,44 +3,44 @@ import Foundation @resultBuilder public enum MapViewContentBuilder: DefaultResultBuilder { public static func buildExpression(_ expression: StyleLayerDefinition) -> [StyleLayerDefinition] { - return [expression] + [expression] } - + public static func buildExpression(_ expression: [StyleLayerDefinition]) -> [StyleLayerDefinition] { - return expression + expression } - - public static func buildExpression(_ expression: Void) -> [StyleLayerDefinition] { - return [] + + public static func buildExpression(_: Void) -> [StyleLayerDefinition] { + [] } - + public static func buildBlock(_ components: [StyleLayerDefinition]...) -> [StyleLayerDefinition] { - return components.flatMap { $0 } + components.flatMap { $0 } } - + public static func buildArray(_ components: [StyleLayerDefinition]) -> [StyleLayerDefinition] { - return components + components } - + public static func buildArray(_ components: [[StyleLayerDefinition]]) -> [StyleLayerDefinition] { - return components.flatMap { $0 } + components.flatMap { $0 } } - + public static func buildEither(first components: [StyleLayerDefinition]) -> [StyleLayerDefinition] { - return components + components } - + public static func buildEither(second components: [StyleLayerDefinition]) -> [StyleLayerDefinition] { - return components + components } - + public static func buildOptional(_ components: [StyleLayerDefinition]?) -> [StyleLayerDefinition] { - return components ?? [] + components ?? [] } - + // MARK: Custom Handler for StyleLayerCollection type. - + public static func buildExpression(_ styleCollection: StyleLayerCollection) -> [StyleLayerDefinition] { - return styleCollection.layers + styleCollection.layers } } diff --git a/Sources/MapLibreSwiftDSL/ShapeDataBuilder.swift b/Sources/MapLibreSwiftDSL/ShapeDataBuilder.swift index 1b64c04..ea5b3b0 100644 --- a/Sources/MapLibreSwiftDSL/ShapeDataBuilder.swift +++ b/Sources/MapLibreSwiftDSL/ShapeDataBuilder.swift @@ -19,69 +19,66 @@ public enum ShapeData { case features([MLNShape & MLNFeature]) } - - public struct ShapeSource: Source { public let identifier: String let data: ShapeData public init(identifier: String, @ShapeDataBuilder _ makeShapeDate: () -> ShapeData) { self.identifier = identifier - self.data = makeShapeDate() + data = makeShapeDate() } public func makeMGLSource() -> MLNSource { // TODO: Options! These should be represented via modifiers like .clustered() switch data { - case .geoJSONURL(let url): - return MLNShapeSource(identifier: identifier, url: url) - case .shapes(let shapes): - return MLNShapeSource(identifier: identifier, shapes: shapes) - case .features(let features): - return MLNShapeSource(identifier: identifier, features: features) + case let .geoJSONURL(url): + MLNShapeSource(identifier: identifier, url: url) + case let .shapes(shapes): + MLNShapeSource(identifier: identifier, shapes: shapes) + case let .features(features): + MLNShapeSource(identifier: identifier, features: features) } } } - @resultBuilder public enum ShapeDataBuilder: DefaultResultBuilder { public static func buildExpression(_ expression: MLNShape) -> [MLNShape] { - return [expression] + [expression] } - + public static func buildExpression(_ expression: [MLNShape]) -> [MLNShape] { - return expression + expression } - - public static func buildExpression(_ expression: Void) -> [MLNShape] { - return [] + + public static func buildExpression(_: Void) -> [MLNShape] { + [] } - + public static func buildBlock(_ components: [MLNShape]...) -> [MLNShape] { - return components.flatMap { $0 } + components.flatMap { $0 } } - + public static func buildArray(_ components: [MLNShape]) -> [MLNShape] { - return components + components } - + public static func buildArray(_ components: [[MLNShape]]) -> [MLNShape] { - return components.flatMap { $0 } + components.flatMap { $0 } } - + public static func buildEither(first components: [MLNShape]) -> [MLNShape] { - return components + components } - + public static func buildEither(second components: [MLNShape]) -> [MLNShape] { - return components + components } - + public static func buildOptional(_ components: [MLNShape]?) -> [MLNShape] { - return components ?? [] + components ?? [] } - + // Convert the collected MLNShape array to ShapeData public static func buildFinalResult(_ components: [MLNShape]) -> ShapeData { let features = components.compactMap { $0 as? MLNShape & MLNFeature } diff --git a/Sources/MapLibreSwiftDSL/Style Layers/Background.swift b/Sources/MapLibreSwiftDSL/Style Layers/Background.swift index 3a8dfd2..e472fa3 100644 --- a/Sources/MapLibreSwiftDSL/Style Layers/Background.swift +++ b/Sources/MapLibreSwiftDSL/Style Layers/Background.swift @@ -1,6 +1,6 @@ import Foundation -import MapLibre import InternalUtils +import MapLibre import MapLibreSwiftMacros @MLNStyleProperty("backgroundColor", supportsInterpolation: true) diff --git a/Sources/MapLibreSwiftDSL/Style Layers/Circle.swift b/Sources/MapLibreSwiftDSL/Style Layers/Circle.swift index 8a57aef..f835f98 100644 --- a/Sources/MapLibreSwiftDSL/Style Layers/Circle.swift +++ b/Sources/MapLibreSwiftDSL/Style Layers/Circle.swift @@ -13,67 +13,67 @@ public struct CircleStyleLayer: SourceBoundStyleLayerDefinition { public var isVisible: Bool = true public var maximumZoomLevel: Float? = nil public var minimumZoomLevel: Float? = nil - + public var source: StyleLayerSource - + public init(identifier: String, source: Source) { self.identifier = identifier self.source = .source(source) } - + public init(identifier: String, source: MLNSource) { self.identifier = identifier self.source = .mglSource(source) } - + public func makeStyleLayer(style: MLNStyle) -> StyleLayer { let styleSource = addSource(to: style) - + return CircleStyleLayerInternal(definition: self, mglSource: styleSource) } - + // MARK: - Modifiers - } private struct CircleStyleLayerInternal: StyleLayer { private var definition: CircleStyleLayer private let mglSource: MLNSource - + public var identifier: String { definition.identifier } public var insertionPosition: LayerInsertionPosition { get { definition.insertionPosition } set { definition.insertionPosition = newValue } } + public var isVisible: Bool { get { definition.isVisible } set { definition.isVisible = newValue } - } + public var maximumZoomLevel: Float? { get { definition.maximumZoomLevel } set { definition.maximumZoomLevel = newValue } } + public var minimumZoomLevel: Float? { get { definition.minimumZoomLevel } set { definition.minimumZoomLevel = newValue } } - + init(definition: CircleStyleLayer, mglSource: MLNSource) { self.definition = definition self.mglSource = mglSource } - + public func makeMLNStyleLayer() -> MLNStyleLayer { let result = MLNCircleStyleLayer(identifier: identifier, source: mglSource) - + result.circleRadius = definition.radius result.circleColor = definition.color - + result.circleStrokeWidth = definition.strokeWidth result.circleStrokeColor = definition.strokeColor - - + return result } } diff --git a/Sources/MapLibreSwiftDSL/Style Layers/Line.swift b/Sources/MapLibreSwiftDSL/Style Layers/Line.swift index 0511855..93cd143 100644 --- a/Sources/MapLibreSwiftDSL/Style Layers/Line.swift +++ b/Sources/MapLibreSwiftDSL/Style Layers/Line.swift @@ -1,6 +1,6 @@ import Foundation -import MapLibre import InternalUtils +import MapLibre import MapLibreSwiftMacros // TODO: Other properties and their modifiers @@ -43,14 +43,17 @@ private struct LineStyleLayerInternal: StyleLayer { get { definition.insertionPosition } set { definition.insertionPosition = newValue } } + public var isVisible: Bool { get { definition.isVisible } set { definition.isVisible = newValue } } + public var maximumZoomLevel: Float? { get { definition.maximumZoomLevel } set { definition.maximumZoomLevel = newValue } } + public var minimumZoomLevel: Float? { get { definition.minimumZoomLevel } set { definition.minimumZoomLevel = newValue } diff --git a/Sources/MapLibreSwiftDSL/Style Layers/Style Layer.swift b/Sources/MapLibreSwiftDSL/Style Layers/Style Layer.swift index 2491b3f..6dda066 100644 --- a/Sources/MapLibreSwiftDSL/Style Layers/Style Layer.swift +++ b/Sources/MapLibreSwiftDSL/Style Layers/Style Layer.swift @@ -25,11 +25,11 @@ public enum StyleLayerSource { case mglSource(MLNSource) } -extension StyleLayerSource { - public var identifier: String { +public extension StyleLayerSource { + var identifier: String { switch self { - case .mglSource(let s): return s.identifier - case .source(let s): return s.identifier + case let .mglSource(s): s.identifier + case let .source(s): s.identifier } } } @@ -46,7 +46,6 @@ public protocol StyleLayerDefinition { /// Whether this layer is displayed. var isVisible: Bool { get set } - /// The minimum zoom level at which the layer gets processed and rendered. /// /// This type is optional since the default values in the C++ code base signifying that the @@ -83,10 +82,10 @@ extension SourceBoundStyleLayerDefinition { let tmpSource: MLNSource switch source { - case .source(let s): + case let .source(s): let source = s.makeMGLSource() tmpSource = source - case .mglSource(let s): + case let .mglSource(s): tmpSource = s } @@ -100,41 +99,40 @@ public protocol StyleLayer: StyleLayerDefinition { func makeMLNStyleLayer() -> MLNStyleLayer } -extension StyleLayer { - public func makeStyleLayer(style: MLNStyle) -> StyleLayer { - return self +public extension StyleLayer { + func makeStyleLayer(style _: MLNStyle) -> StyleLayer { + self } } - -extension StyleLayer { +public extension StyleLayer { // MARK: - Common modifiers - public func visible(_ value: Bool) -> Self { - return modified(self) { $0.isVisible = value } + func visible(_ value: Bool) -> Self { + modified(self) { $0.isVisible = value } } - public func minimumZoomLevel(_ value: Float) -> Self { - return modified(self) { $0.minimumZoomLevel = value } + func minimumZoomLevel(_ value: Float) -> Self { + modified(self) { $0.minimumZoomLevel = value } } - public func maximumZoomLevel(_ value: Float) -> Self { - return modified(self) { $0.maximumZoomLevel = value } + func maximumZoomLevel(_ value: Float) -> Self { + modified(self) { $0.maximumZoomLevel = value } } - public func renderAbove(_ layerID: String) -> Self { - return modified(self) { $0.insertionPosition = .above(layerID: layerID) } + func renderAbove(_ layerID: String) -> Self { + modified(self) { $0.insertionPosition = .above(layerID: layerID) } } - public func renderBelow(_ layerID: String) -> Self { - return modified(self) { $0.insertionPosition = .below(layerID: layerID) } + func renderBelow(_ layerID: String) -> Self { + modified(self) { $0.insertionPosition = .below(layerID: layerID) } } - public func renderAboveOthers() -> Self { - return modified(self) { $0.insertionPosition = .aboveOthers } + func renderAboveOthers() -> Self { + modified(self) { $0.insertionPosition = .aboveOthers } } - public func renderBelowOthers() -> Self { - return modified(self) { $0.insertionPosition = .belowOthers } + func renderBelowOthers() -> Self { + modified(self) { $0.insertionPosition = .belowOthers } } } diff --git a/Sources/MapLibreSwiftDSL/Style Layers/StyleLayerCollection.swift b/Sources/MapLibreSwiftDSL/Style Layers/StyleLayerCollection.swift index 468c967..bd79654 100644 --- a/Sources/MapLibreSwiftDSL/Style Layers/StyleLayerCollection.swift +++ b/Sources/MapLibreSwiftDSL/Style Layers/StyleLayerCollection.swift @@ -1,6 +1,5 @@ import Foundation public protocol StyleLayerCollection { - @MapViewContentBuilder var layers: [StyleLayerDefinition] { get } } diff --git a/Sources/MapLibreSwiftDSL/Style Layers/Symbol.swift b/Sources/MapLibreSwiftDSL/Style Layers/Symbol.swift index f0baaf8..4b240c2 100644 --- a/Sources/MapLibreSwiftDSL/Style Layers/Symbol.swift +++ b/Sources/MapLibreSwiftDSL/Style Layers/Symbol.swift @@ -43,14 +43,15 @@ public struct SymbolStyleLayer: SourceBoundStyleLayerDefinition { // MARK: - Modifiers public func iconImage(constant image: UIImage) -> Self { - return modified(self) { it in + modified(self) { it in it.iconImageName = NSExpression(forConstantValue: image.sha256()) it.iconImages = [image] } } // FIXME: This appears to be broken upstream; waiting for a new release -// public func iconImage(attribute: String, mappings: [AnyHashable: UIImage], default defaultImage: UIImage) -> Self { +// public func iconImage(attribute: String, mappings: [AnyHashable: UIImage], default defaultImage: UIImage) -> Self +// { // return modified(self) { it in // it.iconImageName = NSExpression(forMLNMatchingKey: NSExpression(forConstantValue: attribute), // in: Dictionary(uniqueKeysWithValues: mappings.map({ (k, v) in @@ -62,7 +63,7 @@ public struct SymbolStyleLayer: SourceBoundStyleLayerDefinition { // } public func iconRotation(expression: NSExpression) -> Self { - return modified(self) { it in + modified(self) { it in it.iconRotation = expression } } @@ -77,15 +78,17 @@ private struct SymbolStyleLayerInternal: StyleLayer { get { definition.insertionPosition } set { definition.insertionPosition = newValue } } + public var isVisible: Bool { get { definition.isVisible } set { definition.isVisible = newValue } - } + public var maximumZoomLevel: Float? { get { definition.maximumZoomLevel } set { definition.maximumZoomLevel = newValue } } + public var minimumZoomLevel: Float? { get { definition.minimumZoomLevel } set { definition.minimumZoomLevel = newValue } diff --git a/Sources/MapLibreSwiftDSL/Support/DefaultResultBuilder.swift b/Sources/MapLibreSwiftDSL/Support/DefaultResultBuilder.swift index 7d40850..ccff976 100644 --- a/Sources/MapLibreSwiftDSL/Support/DefaultResultBuilder.swift +++ b/Sources/MapLibreSwiftDSL/Support/DefaultResultBuilder.swift @@ -4,36 +4,35 @@ import Foundation /// /// This is just a tool to make a result builder easier to build, maintain sorting, etc. protocol DefaultResultBuilder { - associatedtype Component - + static func buildExpression(_ expression: Component) -> [Component] - + static func buildExpression(_ expression: [Component]) -> [Component] - + // MARK: Handle void - + static func buildExpression(_ expression: Void) -> [Component] - + // MARK: Combine elements into an array - + static func buildBlock(_ components: [Component]...) -> [Component] - + // MARK: Handle Arrays - + static func buildArray(_ components: [Component]) -> [Component] - + // MARK: Handle for in loops - + static func buildArray(_ components: [[Component]]) -> [Component] - + // MARK: Handle if statements - + static func buildEither(first components: [Component]) -> [Component] - + static func buildEither(second components: [Component]) -> [Component] - + // MARK: Handle Optionals - + static func buildOptional(_ components: [Component]?) -> [Component] } diff --git a/Sources/MapLibreSwiftUI/Examples/Camera.swift b/Sources/MapLibreSwiftUI/Examples/Camera.swift index 4d4e483..3d7d025 100644 --- a/Sources/MapLibreSwiftUI/Examples/Camera.swift +++ b/Sources/MapLibreSwiftUI/Examples/Camera.swift @@ -26,11 +26,11 @@ struct CameraDirectManipulationPreview: View { ) .padding(.bottom, 42) }) - .task { - try? await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC) + .task { + try? await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC) - camera = MapViewCamera.center(switzerland, zoom: 6) - } + camera = MapViewCamera.center(switzerland, zoom: 6) + } } } @@ -38,5 +38,5 @@ struct CameraDirectManipulationPreview: View { CameraDirectManipulationPreview( styleURL: URL(string: "https://demotiles.maplibre.org/style.json")! ) - .ignoresSafeArea(.all) + .ignoresSafeArea(.all) } diff --git a/Sources/MapLibreSwiftUI/Examples/Layers.swift b/Sources/MapLibreSwiftUI/Examples/Layers.swift index a6b34f7..098281a 100644 --- a/Sources/MapLibreSwiftUI/Examples/Layers.swift +++ b/Sources/MapLibreSwiftUI/Examples/Layers.swift @@ -29,7 +29,7 @@ let pointSource = ShapeSource(identifier: "points") { .backgroundColor(constant: .systemPink.withAlphaComponent(0.3)) .renderAboveOthers() } - .ignoresSafeArea(.all) + .ignoresSafeArea(.all) } #Preview("Simple Symbol") { @@ -38,7 +38,7 @@ let pointSource = ShapeSource(identifier: "points") { SymbolStyleLayer(identifier: "simple-symbols", source: pointSource) .iconImage(constant: UIImage(systemName: "mappin")!) } - .ignoresSafeArea(.all) + .ignoresSafeArea(.all) } #Preview("Rotated Symbols (Const)") { @@ -48,7 +48,7 @@ let pointSource = ShapeSource(identifier: "points") { .iconImage(constant: UIImage(systemName: "location.north.circle.fill")!) .iconRotation(constant: 45) } - .ignoresSafeArea(.all) + .ignoresSafeArea(.all) } #Preview("Rotated Symbols (Dynamic)") { @@ -58,7 +58,7 @@ let pointSource = ShapeSource(identifier: "points") { .iconImage(constant: UIImage(systemName: "location.north.circle.fill")!) .iconRotation(featurePropertyNamed: "heading") } - .ignoresSafeArea(.all) + .ignoresSafeArea(.all) } #Preview("Circles with Symbols") { @@ -69,7 +69,7 @@ let pointSource = ShapeSource(identifier: "points") { .color(constant: .systemRed) .strokeWidth(constant: 2) .strokeColor(constant: .white) - + SymbolStyleLayer(identifier: "simple-symbols", source: pointSource) .iconImage(constant: UIImage(systemName: "mappin")!.withRenderingMode(.alwaysTemplate)) .iconColor(constant: .white) @@ -78,7 +78,7 @@ let pointSource = ShapeSource(identifier: "points") { } // TODO: Fixme -//#Preview("Multiple Symbol Icons") { +// #Preview("Multiple Symbol Icons") { // MapView(styleURL: demoTilesURL) { // // Simple symbol layer demonstration with an icon // SymbolStyleLayer(identifier: "simple-symbols", source: pointSource) @@ -90,4 +90,4 @@ let pointSource = ShapeSource(identifier: "points") { // default: UIImage(systemName: "mappin")!) // } // .edgesIgnoringSafeArea(.all) -//} +// } diff --git a/Sources/MapLibreSwiftUI/Examples/Other.swift b/Sources/MapLibreSwiftUI/Examples/Other.swift index b4b1343..0d155ad 100644 --- a/Sources/MapLibreSwiftUI/Examples/Other.swift +++ b/Sources/MapLibreSwiftUI/Examples/Other.swift @@ -10,24 +10,27 @@ import SwiftUI let pointSource = ShapeSource(identifier: "points") { // Uses the DSL to quickly construct point features inline MLNPointFeature(coordinate: CLLocationCoordinate2D(latitude: 51.47778, longitude: -0.00139)) - + MLNPointFeature(coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0)) { feature in feature.attributes["icon"] = "missing" feature.attributes["heading"] = 45 } - + MLNPointFeature(coordinate: CLLocationCoordinate2D(latitude: 39.02001, longitude: 1.482148)) { feature in feature.attributes["icon"] = "club" feature.attributes["heading"] = 145 } } - - // Demonstrates how to use the unsafeMapModifier to set MLNMapView properties that have not been exposed as modifiers yet. + + // Demonstrates how to use the unsafeMapModifier to set MLNMapView properties that have not been exposed as + // modifiers yet. SymbolStyleLayer(identifier: "simple-symbols", source: pointSource) .iconImage(constant: UIImage(systemName: "mappin")!) } .unsafeMapViewModifier { mapView in - // Not all properties have modifiers yet. Until they do, you can use this 'escape hatch' to the underlying MLNMapView. Be careful: if you modify properties that the DSL controls already, they may be overridden. This modifier is a "hack", not a final function. + // Not all properties have modifiers yet. Until they do, you can use this 'escape hatch' to the underlying + // MLNMapView. Be careful: if you modify properties that the DSL controls already, they may be overridden. This + // modifier is a "hack", not a final function. mapView.logoView.isHidden = false mapView.compassViewPosition = .topLeft } diff --git a/Sources/MapLibreSwiftUI/Examples/Polyline.swift b/Sources/MapLibreSwiftUI/Examples/Polyline.swift index 849cdde..001e146 100644 --- a/Sources/MapLibreSwiftUI/Examples/Polyline.swift +++ b/Sources/MapLibreSwiftUI/Examples/Polyline.swift @@ -1,14 +1,15 @@ -import SwiftUI -import MapLibreSwiftDSL -import MapLibre import InternalUtils +import MapLibre +import MapLibreSwiftDSL +import SwiftUI struct PolylinePreview: View { let styleURL: URL var body: some View { MapView(styleURL: styleURL, - constantCamera: .center(samplePedestrianWaypoints.first!, zoom: 14)) { + constantCamera: .center(samplePedestrianWaypoints.first!, zoom: 14)) + { // Note: This line does not add the source to the style as if it // were a statement in an imperative programming language. // The source is added automatically if a layer references it. diff --git a/Sources/MapLibreSwiftUI/Extensions/CoreLocation/CLLocationCoordinate2D.swift b/Sources/MapLibreSwiftUI/Extensions/CoreLocation/CLLocationCoordinate2D.swift index 0046099..2b5d57a 100644 --- a/Sources/MapLibreSwiftUI/Extensions/CoreLocation/CLLocationCoordinate2D.swift +++ b/Sources/MapLibreSwiftUI/Extensions/CoreLocation/CLLocationCoordinate2D.swift @@ -1,13 +1,13 @@ import CoreLocation -// TODO: We can delete chat about this. I'm not 100% on it, even though I want Hashable +// TODO: We can delete chat about this. I'm not 100% on it, even though I want Hashable // on the MapCameraView (so we can let a user present a MapView with a designated camera from NavigationLink) extension CLLocationCoordinate2D: Hashable { public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { - return lhs.latitude == rhs.latitude - && lhs.longitude == rhs.longitude + lhs.latitude == rhs.latitude + && lhs.longitude == rhs.longitude } - + public func hash(into hasher: inout Hasher) { hasher.combine(latitude) hasher.combine(longitude) diff --git a/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNCameraChangeReason.swift b/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNCameraChangeReason.swift index 40f206b..35ec23c 100644 --- a/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNCameraChangeReason.swift +++ b/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNCameraChangeReason.swift @@ -1,24 +1,23 @@ import Foundation import MapLibre -extension MLNCameraChangeReason { - +public extension MLNCameraChangeReason { /// Get the MLNCameraChangeReason from the option set with the largest /// bitwise value. - public var largestBitwiseReason: MLNCameraChangeReason { + var largestBitwiseReason: MLNCameraChangeReason { // Start at 1 var mask: UInt = 1 var result: UInt = 0 - - while mask <= self.rawValue { + + while mask <= rawValue { // If the raw value matches the remaining mask. - if self.rawValue & mask != 0 { + if rawValue & mask != 0 { result = mask } // Shift all the way until the rawValue has been allocated and we have the true last value. mask <<= 1 } - + return MLNCameraChangeReason(rawValue: result) } } diff --git a/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNMapViewCameraUpdating.swift b/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNMapViewCameraUpdating.swift index d3f2e17..ee2c05f 100644 --- a/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNMapViewCameraUpdating.swift +++ b/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNMapViewCameraUpdating.swift @@ -1,5 +1,5 @@ -import Foundation import CoreLocation +import Foundation import MapLibre import Mockable diff --git a/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift b/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift index 0e2e914..22d376e 100644 --- a/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift +++ b/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift @@ -1,9 +1,8 @@ import Foundation -import SwiftUI import MapLibre +import SwiftUI extension MapView { - /// Register a gesture recognizer on the MapView. /// /// - Parameters: @@ -12,45 +11,48 @@ extension MapView { /// - gesture: The gesture definition. func registerGesture(_ mapView: MLNMapView, _ context: Context, gesture: MapGesture) { switch gesture.method { - - case .tap(numberOfTaps: let numberOfTaps): + case let .tap(numberOfTaps: numberOfTaps): let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.captureGesture(_:))) gestureRecognizer.numberOfTapsRequired = numberOfTaps mapView.addGestureRecognizer(gestureRecognizer) gesture.gestureRecognizer = gestureRecognizer - - case .longPress(minimumDuration: let minimumDuration): + + case let .longPress(minimumDuration: minimumDuration): let gestureRecognizer = UILongPressGestureRecognizer(target: context.coordinator, - action: #selector(context.coordinator.captureGesture(_:))) + action: #selector(context.coordinator + .captureGesture(_:))) gestureRecognizer.minimumPressDuration = minimumDuration - + mapView.addGestureRecognizer(gestureRecognizer) gesture.gestureRecognizer = gestureRecognizer } } - + /// Runs on each gesture change event and filters the appropriate gesture behavior based on the /// user definition. /// - /// Since the gestures run "onChange", we run this every time, event when state changes. The implementer is responsible for guarding + /// Since the gestures run "onChange", we run this every time, event when state changes. The implementer is + /// responsible for + /// guarding /// and handling whatever state logic they want. /// /// - Parameters: - /// - mapView: The MapView emitting the gesture. This is used to calculate the point and coordinate of the gesture. + /// - mapView: The MapView emitting the gesture. This is used to calculate the point and coordinate of the + /// gesture. /// - sender: The UIGestureRecognizer func processGesture(_ mapView: MLNMapView, _ sender: UIGestureRecognizer) { - guard let gesture = self.gestures.first(where: { $0.gestureRecognizer == sender }) else { + guard let gesture = gestures.first(where: { $0.gestureRecognizer == sender }) else { assertionFailure("\(sender) is not a registered UIGestureRecongizer on the MapView") return } - + // Process the gesture into a context response. let context = processContextFromGesture(mapView, gesture: gesture, sender: sender) // Run the context through the gesture held on the MapView (emitting to the MapView modifier). gesture.onChange(context) } - + /// Convert the sender data into a MapGestureContext /// /// - Parameters: @@ -58,19 +60,19 @@ extension MapView { /// - gesture: The gesture definition for this event. /// - sender: The UIKit gesture emitting from the map view. /// - Returns: The calculated context from the sending UIKit gesture - func processContextFromGesture(_ mapView: MLNMapView, gesture: MapGesture, sender: UIGestureRecognizing) -> MapGestureContext { + func processContextFromGesture(_ mapView: MLNMapView, gesture: MapGesture, + sender: UIGestureRecognizing) -> MapGestureContext + { // Build the context of the gesture's event. - var point: CGPoint - switch gesture.method { - - case .tap(numberOfTaps: let numberOfTaps): + var point: CGPoint = switch gesture.method { + case let .tap(numberOfTaps: numberOfTaps): // Calculate the CGPoint of the last gesture tap - point = sender.location(ofTouch: numberOfTaps - 1, in: mapView) + sender.location(ofTouch: numberOfTaps - 1, in: mapView) case .longPress: // Calculate the CGPoint of the long process gesture. - point = sender.location(in: mapView) + sender.location(in: mapView) } - + return MapGestureContext(gestureMethod: gesture.method, state: sender.state, point: point, diff --git a/Sources/MapLibreSwiftUI/Extensions/UIKit/UIGestureRecognizing.swift b/Sources/MapLibreSwiftUI/Extensions/UIKit/UIGestureRecognizing.swift index eb3b480..09b68d8 100644 --- a/Sources/MapLibreSwiftUI/Extensions/UIKit/UIGestureRecognizing.swift +++ b/Sources/MapLibreSwiftUI/Extensions/UIKit/UIGestureRecognizing.swift @@ -1,5 +1,5 @@ -import UIKit import Mockable +import UIKit @Mockable protocol UIGestureRecognizing: AnyObject { diff --git a/Sources/MapLibreSwiftUI/MapView.swift b/Sources/MapLibreSwiftUI/MapView.swift index 159d7f5..3212f9e 100644 --- a/Sources/MapLibreSwiftUI/MapView.swift +++ b/Sources/MapLibreSwiftUI/MapView.swift @@ -1,18 +1,17 @@ -import SwiftUI import InternalUtils import MapLibre import MapLibreSwiftDSL +import SwiftUI public struct MapView: UIViewRepresentable { - @Binding var camera: MapViewCamera let styleSource: MapStyleSource let userLayers: [StyleLayerDefinition] - + var gestures = [MapGesture]() var onStyleLoaded: ((MLNStyle) -> Void)? - + /// 'Escape hatch' to MLNMapView until we have more modifiers. /// See ``unsafeMapViewModifier(_:)`` var unsafeMapViewModifier: ((MLNMapView) -> Void)? @@ -22,8 +21,8 @@ public struct MapView: UIViewRepresentable { camera: Binding = .constant(.default()), @MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] } ) { - self.styleSource = .url(styleURL) - self._camera = camera + styleSource = .url(styleURL) + _camera = camera userLayers = makeMapContent() } @@ -32,18 +31,18 @@ public struct MapView: UIViewRepresentable { constantCamera: MapViewCamera, @MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] } ) { - self.init(styleURL: styleURL, + self.init(styleURL: styleURL, camera: .constant(constantCamera), makeMapContent) } - + public func makeCoordinator() -> MapViewCoordinator { MapViewCoordinator( parent: self, onGesture: { processGesture($0, $1) } ) } - + public func makeUIView(context: Context) -> MLNMapView { // Create the map view let mapView = MLNMapView(frame: .zero) @@ -51,40 +50,41 @@ public struct MapView: UIViewRepresentable { context.coordinator.mapView = mapView switch styleSource { - case .url(let styleURL): + case let .url(styleURL): mapView.styleURL = styleURL } context.coordinator.updateCamera(mapView: mapView, camera: $camera.wrappedValue, animated: false) - + // TODO: Make this settable via a modifier mapView.logoView.isHidden = true - + // Link the style loaded to the coordinator that emits the delegate event. context.coordinator.onStyleLoaded = onStyleLoaded - + // Add all gesture recognizers for gesture in gestures { registerGesture(mapView, context, gesture: gesture) } - + return mapView } - + public func updateUIView(_ mapView: MLNMapView, context: Context) { context.coordinator.parent = self - + // MARK: Modifiers + unsafeMapViewModifier?(mapView) - + // MARK: End Modifiers - + // FIXME: This should be a more selective update context.coordinator.updateStyleSource(styleSource, mapView: mapView) context.coordinator.updateLayers(mapView: mapView) - + // FIXME: This isn't exactly telling us if the *map* is loaded, and the docs for setCenter say it needs to be. let isStyleLoaded = mapView.style != nil @@ -98,7 +98,7 @@ public struct MapView: UIViewRepresentable { MapView(styleURL: demoTilesURL) .ignoresSafeArea(.all) .previewDisplayName("Vanilla Map") - + // For a larger selection of previews, // check out the Examples directory, which // has a wide variety of previews, diff --git a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift index 9f9ceff..336412a 100644 --- a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift +++ b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift @@ -3,10 +3,9 @@ import MapLibre import MapLibreSwiftDSL public class MapViewCoordinator: NSObject { - // This must be weak, the UIViewRepresentable owns the MLNMapView. weak var mapView: MLNMapView? - var parent: MapView + var parent: MapView // Storage of variables as they were previously; these are snapshot // every update cycle so we can avoid unnecessary updates @@ -14,29 +13,32 @@ public class MapViewCoordinator: NSObject { private var snapshotCamera: MapViewCamera? var onStyleLoaded: ((MLNStyle) -> Void)? var onGesture: (MLNMapView, UIGestureRecognizer) -> Void - + init(parent: MapView, - onGesture: @escaping (MLNMapView, UIGestureRecognizer) -> Void) { + onGesture: @escaping (MLNMapView, UIGestureRecognizer) -> Void) + { self.parent = parent self.onGesture = onGesture } - + // MARK: Core UIView Functionality - + @objc func captureGesture(_ sender: UIGestureRecognizer) { guard let mapView else { return } - + onGesture(mapView, sender) } // MARK: - Coordinator API - Camera + Manipulation - + /// Update the camera based on the MapViewCamera binding change. /// /// - Parameters: - /// - mapView: This is the camera updating protocol representation of the MLNMapView. This allows mockable testing for camera related MLNMapView functionality. + /// - mapView: This is the camera updating protocol representation of the MLNMapView. This allows mockable testing + /// for + /// camera related MLNMapView functionality. /// - camera: The new camera from the binding. /// - animated: Whether to animate. func updateCamera(mapView: MLNMapViewCameraUpdating, camera: MapViewCamera, animated: Bool) { @@ -44,9 +46,9 @@ public class MapViewCoordinator: NSObject { // No action - camera has not changed. return } - + switch camera.state { - case .centered(onCoordinate: let coordinate): + case let .centered(onCoordinate: coordinate): mapView.userTrackingMode = .none mapView.setCenter(coordinate, zoomLevel: camera.zoom, @@ -65,25 +67,25 @@ public class MapViewCoordinator: NSObject { // TODO: Need a method these/or to finalize a goal here. break } - + // Set the correct pitch range. mapView.minimumPitch = camera.pitch.rangeValue.lowerBound mapView.maximumPitch = camera.pitch.rangeValue.upperBound - + snapshotCamera = camera } - + // MARK: - Coordinator API - Styles + Layers - + func updateStyleSource(_ source: MapStyleSource, mapView: MLNMapView) { switch (source, parent.styleSource) { - case (.url(let newURL), .url(let oldURL)): + case let (.url(newURL), .url(oldURL)): if newURL != oldURL { mapView.styleURL = newURL } } } - + func updateLayers(mapView: MLNMapView) { // TODO: Figure out how to selectively update layers when only specific props changed. New function in addition to makeMLNStyleLayer? @@ -98,12 +100,12 @@ public class MapViewCoordinator: NSObject { if let specWithSource = layer as? SourceBoundStyleLayerDefinition { switch specWithSource.source { - case .mglSource(_): + case .mglSource: // Do Nothing // DISCUSS: The idea is to exclude "unmanaged" sources and only manage the ones specified via the DSL and attached to a layer. // This is a really hackish design and I don't particularly like it. continue - case .source(_): + case .source: // Mark sources for removal after all user layers have been removed. // Sources specified in this way should be used by a layer already in the style. sourcesToRemove.insert(specWithSource.source.identifier) @@ -148,14 +150,14 @@ public class MapViewCoordinator: NSObject { } switch layerSpec.insertionPosition { - case .above(layerID: let id): + case let .above(layerID: id): if let layer = mglStyle.layer(withIdentifier: id) { mglStyle.insertLayer(newLayer, above: layer) } else { NSLog("Failed to find layer with ID \(id). Adding layer on top.") mglStyle.addLayer(newLayer) } - case .below(layerID: let id): + case let .below(layerID: id): if let layer = mglStyle.layer(withIdentifier: id) { mglStyle.insertLayer(newLayer, below: layer) } else { @@ -174,28 +176,30 @@ public class MapViewCoordinator: NSObject { // MARK: - MLNMapViewDelegate extension MapViewCoordinator: MLNMapViewDelegate { - - public func mapView(_ mapView: MLNMapView, didFinishLoading mglStyle: MLNStyle) { + public func mapView(_: MLNMapView, didFinishLoading mglStyle: MLNStyle) { addLayers(to: mglStyle) onStyleLoaded?(mglStyle) } /// The MapView's region has changed with a specific reason. - public func mapView(_ mapView: MLNMapView, regionDidChangeWith reason: MLNCameraChangeReason, animated: Bool) { + public func mapView(_ mapView: MLNMapView, regionDidChangeWith reason: MLNCameraChangeReason, animated _: Bool) { // Validate that the mapView.userTrackingMode still matches our desired camera state for each tracking type. let isFollowing = parent.camera.state == .trackingUserLocation && mapView.userTrackingMode == .follow - let isFollowingHeading = parent.camera.state == .trackingUserLocationWithHeading && mapView.userTrackingMode == .followWithHeading - let isFollowingCourse = parent.camera.state == .trackingUserLocationWithCourse && mapView.userTrackingMode == .followWithCourse - - // If any of these are a mismatch, we know the camera is no longer following a desired method, so we should detach and revert - // to a .centered camera. - // If any one of these is true, the desired camera state still matches the mapView's userTrackingMode + let isFollowingHeading = parent.camera.state == .trackingUserLocationWithHeading && mapView + .userTrackingMode == .followWithHeading + let isFollowingCourse = parent.camera.state == .trackingUserLocationWithCourse && mapView + .userTrackingMode == .followWithCourse + + // If any of these are a mismatch, we know the camera is no longer following a desired method, so we should + // detach and revert to a .centered camera. If any one of these is true, the desired camera state still matches + // the mapView's userTrackingMode if isFollowing || isFollowingHeading || isFollowingCourse { // User tracking is still active, we can ignore camera updates until we unset/fail this boolean check return } - - // The user's desired camera is not a user tracking method, now we need to publish the MLNMapView's camera state to the MapView camera binding. + + // The user's desired camera is not a user tracking method, now we need to publish the MLNMapView's camera state + // to the MapView camera binding. parent.camera = .center(mapView.centerCoordinate, zoom: mapView.zoomLevel, reason: CameraChangeReason(reason)) diff --git a/Sources/MapLibreSwiftUI/MapViewModifiers.swift b/Sources/MapLibreSwiftUI/MapViewModifiers.swift index 71319f9..bc8aa96 100644 --- a/Sources/MapLibreSwiftUI/MapViewModifiers.swift +++ b/Sources/MapLibreSwiftUI/MapViewModifiers.swift @@ -1,22 +1,18 @@ -// This file contains modifiers that are internal and specific to the MapView. -// They are not intended to be exposed directly in the public interface. - import Foundation -import SwiftUI import MapLibre +import SwiftUI -extension MapView { - +public extension MapView { /// Perform an action when the map view has loaded its style and all locally added style definitions. /// /// - Parameter perform: The action to perform with the loaded style. /// - Returns: The modified map view. - public func onStyleLoaded(_ perform: @escaping (MLNStyle) -> Void) -> MapView { + func onStyleLoaded(_ perform: @escaping (MLNStyle) -> Void) -> MapView { var newMapView = self newMapView.onStyleLoaded = perform return newMapView } - + /// Allows you to set properties of the underlying MLNMapView directly /// in cases where these have not been ported to DSL yet. /// Use this function to modify various properties of the MLNMapView instance. @@ -41,47 +37,49 @@ extension MapView { /// } /// ``` /// - public func unsafeMapViewModifier(_ modifier: @escaping (MLNMapView) -> Void) -> MapView { + func unsafeMapViewModifier(_ modifier: @escaping (MLNMapView) -> Void) -> MapView { var newMapView = self newMapView.unsafeMapViewModifier = modifier return newMapView } - + // MARK: Default Gestures - + /// Add an tap gesture handler to the MapView /// /// - Parameters: /// - count: The number of taps required to run the gesture. /// - onTapChanged: Emits the context whenever the gesture changes (e.g. began, ended, etc). /// - Returns: The modified map view. - public func onTapMapGesture(count: Int = 1, - onTapChanged: @escaping (MapGestureContext) -> Void) -> MapView { + func onTapMapGesture(count: Int = 1, + onTapChanged: @escaping (MapGestureContext) -> Void) -> MapView + { var newMapView = self - + // Build the gesture and link it to the map view. let gesture = MapGesture(method: .tap(numberOfTaps: count), onChange: onTapChanged) newMapView.gestures.append(gesture) - + return newMapView } - + /// Add a long press gesture handler ot the MapView /// /// - Parameters: /// - minimumDuration: The minimum duration in seconds the user must press the screen to run the gesture. /// - onPressChanged: Emits the context whenever the gesture changes (e.g. began, ended, etc). /// - Returns: The modified map view. - public func onLongPressMapGesture(minimumDuration: Double = 0.5, - onPressChanged: @escaping (MapGestureContext) -> Void) -> MapView { + func onLongPressMapGesture(minimumDuration: Double = 0.5, + onPressChanged: @escaping (MapGestureContext) -> Void) -> MapView + { var newMapView = self - + // Build the gesture and link it to the map view. let gesture = MapGesture(method: .longPress(minimumDuration: minimumDuration), onChange: onPressChanged) newMapView.gestures.append(gesture) - + return newMapView } } diff --git a/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift b/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift index f299030..35ebff0 100644 --- a/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift +++ b/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift @@ -1,31 +1,29 @@ import UIKit public class MapGesture: NSObject { - public enum Method: Equatable { - /// A standard tap gesture (UITapGestureRecognizer) /// /// - Parameters: /// - numberOfTaps: The number of taps required for the gesture to trigger case tap(numberOfTaps: Int = 1) - + /// A standard long press gesture (UILongPressGestureRecognizer) /// /// - Parameters: /// - minimumDuration: The minimum duration of the press in seconds. case longPress(minimumDuration: Double = 0.5) } - + /// The Gesture's method, this is used to register it for the correct user interaction on the MapView. let method: Method - + /// The onChange action that runs when the gesture changes on the map view. let onChange: (MapGestureContext) -> Void - + /// The underlying gesture recognizer weak var gestureRecognizer: UIGestureRecognizer? - + /// Create a new gesture recognizer definition for the MapView /// /// - Parameters: diff --git a/Sources/MapLibreSwiftUI/Models/Gesture/MapGestureContext.swift b/Sources/MapLibreSwiftUI/Models/Gesture/MapGestureContext.swift index 0394e1b..b6ea65a 100644 --- a/Sources/MapLibreSwiftUI/Models/Gesture/MapGestureContext.swift +++ b/Sources/MapLibreSwiftUI/Models/Gesture/MapGestureContext.swift @@ -1,18 +1,17 @@ -import UIKit import CoreLocation +import UIKit /// The contextual representation of the gesture. public struct MapGestureContext { - /// The map gesture that produced the context. public let gestureMethod: MapGesture.Method - + /// The state of the on change event. public let state: UIGestureRecognizer.State - + /// The location that the gesture occured on the screen. public let point: CGPoint - + /// The underlying geographic coordinate at the point of the gesture. public let coordinate: CLLocationCoordinate2D } diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraChangeReason.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraChangeReason.swift index adc82d0..368ae84 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraChangeReason.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraChangeReason.swift @@ -12,7 +12,7 @@ public enum CameraChangeReason: Hashable { case gestureOneFingerZoom case gestureTilt case transitionCancelled - + /// Initialize a Swift CameraChangeReason from the MLN NSOption. /// /// This method will only show the largest bitwise reason contained in MLNCameraChangeReason. @@ -21,26 +21,25 @@ public enum CameraChangeReason: Hashable { /// - Parameter mlnCameraChangeReason: The camera change reason options list from the MapLibre MapViewDelegate init?(_ mlnCameraChangeReason: MLNCameraChangeReason) { switch mlnCameraChangeReason.largestBitwiseReason { - - case .programmatic: + case .programmatic: self = .programmatic - case .resetNorth: + case .resetNorth: self = .resetNorth - case .gesturePan: + case .gesturePan: self = .gesturePan - case .gesturePinch: + case .gesturePinch: self = .gesturePinch case .gestureRotate: self = .gestureRotate - case .gestureZoomIn: + case .gestureZoomIn: self = .gestureZoomIn - case .gestureZoomOut: + case .gestureZoomOut: self = .gestureZoomOut - case .gestureOneFingerZoom: + case .gestureOneFingerZoom: self = .gestureOneFingerZoom - case .gestureTilt: + case .gestureTilt: self = .gestureTilt - case .transitionCancelled: + case .transitionCancelled: self = .transitionCancelled default: return nil diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitch.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitch.swift index 56dbc83..d6a9252 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitch.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitch.swift @@ -3,28 +3,26 @@ import MapLibre /// The current pitch state for the MapViewCamera public enum CameraPitch: Hashable { - /// The user is free to control pitch from it's default min to max. case free - + /// The user is free to control pitch within the minimum and maximum range. case freeWithinRange(minimum: Double, maximum: Double) - + /// The pitch is fixed to a certain value. case fixed(Double) - + /// The range of acceptable pitch values. /// /// This is applied to the map view on camera updates. var rangeValue: ClosedRange { switch self { - case .free: - return 0...60 // TODO: set this to a maplibre constant (this is available on Android, but maybe not iOS)? - case .freeWithinRange(minimum: let minimum, maximum: let maximum): - return minimum...maximum - case .fixed(let value): - return value...value + 0 ... 60 // TODO: set this to a maplibre constant (this is available on Android, but maybe not iOS)? + case let .freeWithinRange(minimum: minimum, maximum: maximum): + minimum ... maximum + case let .fixed(value): + value ... value } } } diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift index 1ff3e42..f6fdd8e 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift @@ -3,31 +3,30 @@ import MapLibre /// The CameraState is used to understand the current context of the MapView's camera. public enum CameraState: Hashable { - /// Centered on a coordinate case centered(onCoordinate: CLLocationCoordinate2D) - + /// Follow the user's location using the MapView's internal camera. /// /// This feature uses the MLNMapView's userTrackingMode to .follow which automatically /// follows the user from within the MLNMapView. case trackingUserLocation - + /// Follow the user's location using the MapView's internal camera with the user's heading. /// /// This feature uses the MLNMapView's userTrackingMode to .followWithHeading which automatically /// follows the user from within the MLNMapView. case trackingUserLocationWithHeading - + /// Follow the user's location using the MapView's internal camera with the users' course /// /// This feature uses the MLNMapView's userTrackingMode to .followWithCourse which automatically /// follows the user from within the MLNMapView. case trackingUserLocationWithCourse - + /// Centered on a bounding box/rectangle. case rect(northeast: CLLocationCoordinate2D, southwest: CLLocationCoordinate2D) // TODO: make a bounding box? - + /// Showcasing GeoJSON, Polygons, etc. case showcase(shapeCollection: MLNShapeCollection) } @@ -35,19 +34,18 @@ public enum CameraState: Hashable { extension CameraState: CustomDebugStringConvertible { public var debugDescription: String { switch self { - - case .centered(onCoordinate: let onCoordinate): - return "CameraState.centered(onCoordinate: \(onCoordinate)" + case let .centered(onCoordinate: onCoordinate): + "CameraState.centered(onCoordinate: \(onCoordinate)" case .trackingUserLocation: - return "CameraState.trackingUserLocation" + "CameraState.trackingUserLocation" case .trackingUserLocationWithHeading: - return "CameraState.trackingUserLocationWithHeading" + "CameraState.trackingUserLocationWithHeading" case .trackingUserLocationWithCourse: - return "CameraState.trackingUserLocationWithCourse" - case .rect(northeast: let northeast, southwest: let southwest): - return "CameraState.rect(northeast: \(northeast), southwest: \(southwest))" - case .showcase(shapeCollection: let shapeCollection): - return "CameraState.showcase(shapeCollection: \(shapeCollection))" + "CameraState.trackingUserLocationWithCourse" + case let .rect(northeast: northeast, southwest: southwest): + "CameraState.rect(northeast: \(northeast), southwest: \(southwest))" + case let .showcase(shapeCollection: shapeCollection): + "CameraState.showcase(shapeCollection: \(shapeCollection))" } } } diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift index cdaa807..e28cb0f 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift @@ -1,43 +1,42 @@ -import Foundation import CoreLocation +import Foundation import MapLibre /// The SwiftUI MapViewCamera. /// /// This manages the camera state within the MapView. public struct MapViewCamera: Hashable { - - public struct Defaults { + public enum Defaults { public static let coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0) public static let zoom: Double = 10 public static let pitch: CameraPitch = .free public static let direction: CLLocationDirection = 0 } - + public var state: CameraState public var zoom: Double public var pitch: CameraPitch public var direction: CLLocationDirection - + /// The reason the camera was changed. /// /// This can be used to see if the camera programmatically moved, /// or manipulated through a user gesture. public var lastReasonForChange: CameraChangeReason? - + /// A camera centered at 0.0, 0.0. This is typically used as a backup, /// pre-load for an expected camera update (e.g. before a location provider produces /// it's first location). /// /// - Returns: The constructed MapViewCamera. public static func `default`() -> MapViewCamera { - return MapViewCamera(state: .centered(onCoordinate: Defaults.coordinate), - zoom: Defaults.zoom, - pitch: Defaults.pitch, - direction: Defaults.direction, - lastReasonForChange: .programmatic) + MapViewCamera(state: .centered(onCoordinate: Defaults.coordinate), + zoom: Defaults.zoom, + pitch: Defaults.pitch, + direction: Defaults.direction, + lastReasonForChange: .programmatic) } - + /// Center the map on a specific location. /// /// - Parameters: @@ -50,71 +49,74 @@ public struct MapViewCamera: Hashable { zoom: Double, pitch: CameraPitch = Defaults.pitch, direction: CLLocationDirection = Defaults.direction, - reason: CameraChangeReason? = nil) -> MapViewCamera { - - return MapViewCamera(state: .centered(onCoordinate: coordinate), - zoom: zoom, - pitch: pitch, - direction: direction, - lastReasonForChange: reason) + reason: CameraChangeReason? = nil) -> MapViewCamera + { + MapViewCamera(state: .centered(onCoordinate: coordinate), + zoom: zoom, + pitch: pitch, + direction: direction, + lastReasonForChange: reason) } - + /// Enables user location tracking within the MapView. /// /// This feature uses the MLNMapView's userTrackingMode = .follow /// /// - Parameters: - /// - zoom: Set the desired zoom. This is a one time event and the user can manipulate their zoom after unlike pitch. + /// - zoom: Set the desired zoom. This is a one time event and the user can manipulate their zoom after unlike + /// pitch. /// - pitch: Set the camera pitch method. /// - Returns: The MapViewCamera representing the scenario public static func trackUserLocation(zoom: Double = Defaults.zoom, - pitch: CameraPitch = Defaults.pitch) -> MapViewCamera { - + pitch: CameraPitch = Defaults.pitch) -> MapViewCamera + { // Coordinate is ignored when tracking user location. However, pitch and zoom are valid. - return MapViewCamera(state: .trackingUserLocation, - zoom: zoom, - pitch: pitch, - direction: Defaults.direction, - lastReasonForChange: .programmatic) + MapViewCamera(state: .trackingUserLocation, + zoom: zoom, + pitch: pitch, + direction: Defaults.direction, + lastReasonForChange: .programmatic) } - + /// Enables user location tracking within the MapView. /// /// This feature uses the MLNMapView's userTrackingMode = .followWithHeading /// /// - Parameters: - /// - zoom: Set the desired zoom. This is a one time event and the user can manipulate their zoom after unlike pitch. + /// - zoom: Set the desired zoom. This is a one time event and the user can manipulate their zoom after unlike + /// pitch. /// - pitch: Set the camera pitch method. /// - Returns: The MapViewCamera representing the scenario public static func trackUserLocationWithHeading(zoom: Double = Defaults.zoom, - pitch: CameraPitch = Defaults.pitch) -> MapViewCamera { - + pitch: CameraPitch = Defaults.pitch) -> MapViewCamera + { // Coordinate is ignored when tracking user location. However, pitch and zoom are valid. - return MapViewCamera(state: .trackingUserLocationWithHeading, - zoom: zoom, - pitch: pitch, - direction: Defaults.direction, - lastReasonForChange: .programmatic) + MapViewCamera(state: .trackingUserLocationWithHeading, + zoom: zoom, + pitch: pitch, + direction: Defaults.direction, + lastReasonForChange: .programmatic) } - + /// Enables user location tracking within the MapView. /// /// This feature uses the MLNMapView's userTrackingMode = .followWithCourse /// /// - Parameters: - /// - zoom: Set the desired zoom. This is a one time event and the user can manipulate their zoom after unlike pitch. + /// - zoom: Set the desired zoom. This is a one time event and the user can manipulate their zoom after unlike + /// pitch. /// - pitch: Set the camera pitch method. /// - Returns: The MapViewCamera representing the scenario public static func trackUserLocationWithCourse(zoom: Double = Defaults.zoom, - pitch: CameraPitch = Defaults.pitch) -> MapViewCamera { - + pitch: CameraPitch = Defaults.pitch) -> MapViewCamera + { // Coordinate is ignored when tracking user location. However, pitch and zoom are valid. - return MapViewCamera(state: .trackingUserLocationWithCourse, - zoom: zoom, - pitch: pitch, - direction: Defaults.direction, - lastReasonForChange: .programmatic) + MapViewCamera(state: .trackingUserLocationWithCourse, + zoom: zoom, + pitch: pitch, + direction: Defaults.direction, + lastReasonForChange: .programmatic) } - + // TODO: Create init methods for other camera states once supporting materials are understood (e.g. BoundingBox) } diff --git a/Tests/MapLibreSwiftDSLTests/PointFeatureTests.swift b/Tests/MapLibreSwiftDSLTests/PointFeatureTests.swift index a8b61d5..254e292 100644 --- a/Tests/MapLibreSwiftDSLTests/PointFeatureTests.swift +++ b/Tests/MapLibreSwiftDSLTests/PointFeatureTests.swift @@ -1,7 +1,7 @@ +import InternalUtils +import MapLibre import XCTest @testable import MapLibreSwiftDSL -import MapLibre -import InternalUtils final class PointFeatureTests: XCTestCase { private let primeMeridian = CLLocationCoordinate2D(latitude: 51.47778, longitude: -0.00139) diff --git a/Tests/MapLibreSwiftDSLTests/ShapeSourceTests.swift b/Tests/MapLibreSwiftDSLTests/ShapeSourceTests.swift index eaed93f..38d018a 100644 --- a/Tests/MapLibreSwiftDSLTests/ShapeSourceTests.swift +++ b/Tests/MapLibreSwiftDSLTests/ShapeSourceTests.swift @@ -1,7 +1,7 @@ +import InternalUtils +import MapLibre import XCTest @testable import MapLibreSwiftDSL -import MapLibre -import InternalUtils final class ShapeSourceTests: XCTestCase { func testShapeSourcePolylineShapeBuilder() throws { @@ -14,7 +14,7 @@ final class ShapeSourceTests: XCTestCase { XCTAssertEqual(shapeSource.identifier, "foo") switch shapeSource.data { - case .shapes(let shapes): + case let .shapes(shapes): XCTAssertEqual(shapes.count, 1) default: XCTFail("Expected a shape source") @@ -29,13 +29,13 @@ final class ShapeSourceTests: XCTestCase { XCTAssertEqual(shapeSource.identifier, "foo") switch shapeSource.data { - case .features(let features): + case let .features(features): XCTAssertEqual(features.count, 1) default: XCTFail("Expected a feature source") } } - + func testForInAndCombinationFeatureBuilder() throws { // ShapeSource now accepts 'for in' building, arrays, and combinations of them let shapeSource = ShapeSource(identifier: "foo") { @@ -44,15 +44,14 @@ final class ShapeSourceTests: XCTestCase { } MLNPointFeature(coordinate: CLLocationCoordinate2D(latitude: 48.2082, longitude: 16.3719)) } - + XCTAssertEqual(shapeSource.identifier, "foo") - + switch shapeSource.data { - case .features(let features): + case let .features(features): XCTAssertEqual(features.count, 48) default: XCTFail("Expected a feature source") } } - } diff --git a/Tests/MapLibreSwiftDSLTests/StyleLayerTest.swift b/Tests/MapLibreSwiftDSLTests/StyleLayerTest.swift index 728ddc6..db0b40b 100644 --- a/Tests/MapLibreSwiftDSLTests/StyleLayerTest.swift +++ b/Tests/MapLibreSwiftDSLTests/StyleLayerTest.swift @@ -1,6 +1,6 @@ -import XCTest import MapLibre import MapLibreSwiftDSL +import XCTest final class StyleLayerTest: XCTestCase { func testBackgroundStyleLayer() throws { diff --git a/Tests/MapLibreSwiftUITests/Examples/CameraPreviewTests.swift b/Tests/MapLibreSwiftUITests/Examples/CameraPreviewTests.swift index 5fafc7b..505dbfb 100644 --- a/Tests/MapLibreSwiftUITests/Examples/CameraPreviewTests.swift +++ b/Tests/MapLibreSwiftUITests/Examples/CameraPreviewTests.swift @@ -1,9 +1,8 @@ -import XCTest import SnapshotTesting +import XCTest @testable import MapLibreSwiftUI final class CameraPreviewTests: XCTestCase { - func testCameraPreview() { assertView(named: "CameraPreview") { CameraDirectManipulationPreview( diff --git a/Tests/MapLibreSwiftUITests/Examples/LayerPreviewTests.swift b/Tests/MapLibreSwiftUITests/Examples/LayerPreviewTests.swift index e22e159..d3eb36f 100644 --- a/Tests/MapLibreSwiftUITests/Examples/LayerPreviewTests.swift +++ b/Tests/MapLibreSwiftUITests/Examples/LayerPreviewTests.swift @@ -1,10 +1,9 @@ -import XCTest import MapLibre import MapLibreSwiftDSL +import XCTest @testable import MapLibreSwiftUI final class LayerPreviewTests: XCTestCase { - let demoTilesURL = URL(string: "https://demotiles.maplibre.org/style.json")! // A collection of points with various @@ -34,7 +33,7 @@ final class LayerPreviewTests: XCTestCase { } } } - + func testSimpleSymbol() { assertView { MapView(styleURL: demoTilesURL) { @@ -44,7 +43,7 @@ final class LayerPreviewTests: XCTestCase { } } } - + func testRotatedSymbolConst() { assertView { MapView(styleURL: demoTilesURL) { @@ -55,7 +54,7 @@ final class LayerPreviewTests: XCTestCase { } } } - + func testRotatedSymboleDynamic() { assertView { MapView(styleURL: demoTilesURL) { @@ -66,7 +65,7 @@ final class LayerPreviewTests: XCTestCase { } } } - + func testCirclesWithSymbols() { assertView { MapView(styleURL: demoTilesURL) { @@ -76,7 +75,7 @@ final class LayerPreviewTests: XCTestCase { .color(constant: .systemRed) .strokeWidth(constant: 2) .strokeColor(constant: .white) - + SymbolStyleLayer(identifier: "simple-symbols", source: pointSource) .iconImage(constant: UIImage(systemName: "mappin")!.withRenderingMode(.alwaysTemplate)) .iconColor(constant: .white) diff --git a/Tests/MapLibreSwiftUITests/Extensions/CoreLocation/CLLocationCoordinate2D.swift b/Tests/MapLibreSwiftUITests/Extensions/CoreLocation/CLLocationCoordinate2D.swift index 2a7e9a2..2b9c587 100644 --- a/Tests/MapLibreSwiftUITests/Extensions/CoreLocation/CLLocationCoordinate2D.swift +++ b/Tests/MapLibreSwiftUITests/Extensions/CoreLocation/CLLocationCoordinate2D.swift @@ -1,16 +1,15 @@ -import XCTest import CoreLocation +import XCTest @testable import MapLibreSwiftUI final class CLLocationCoordinate2DTests: XCTestCase { - func testHashable() { let coordinate = CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4) - + var hasher = Hasher() coordinate.hash(into: &hasher) let hashedValue = hasher.finalize() - + XCTAssertEqual(hashedValue, coordinate.hashValue) } } diff --git a/Tests/MapLibreSwiftUITests/MapView/MapViewGestureTests.swift b/Tests/MapLibreSwiftUITests/MapView/MapViewGestureTests.swift index e4b8e3a..f2f0593 100644 --- a/Tests/MapLibreSwiftUITests/MapView/MapViewGestureTests.swift +++ b/Tests/MapLibreSwiftUITests/MapView/MapViewGestureTests.swift @@ -1,76 +1,75 @@ -import XCTest -import MockableTest import MapLibre +import MockableTest +import XCTest @testable import MapLibreSwiftUI final class MapViewGestureTests: XCTestCase { - let maplibreMapView = MLNMapView() let mapView = MapView(styleURL: URL(string: "https://maplibre.org")!) - + // MARK: Gesture View Modifiers - + func testMapViewOnTapGestureModifier() { let newMapView = mapView.onTapMapGesture { _ in // Do nothing } - + XCTAssertEqual(newMapView.gestures.first?.method, .tap()) } - + func testMapViewOnLongPressGestureModifier() { let newMapView = mapView.onLongPressMapGesture { _ in // Do nothing } - + XCTAssertEqual(newMapView.gestures.first?.method, .longPress()) } - + // MARK: Gesture Processing - + func testTapGesture() { let gesture = MapGesture(method: .tap(numberOfTaps: 2)) { _ in // Do nothing } - + let mockTapGesture = MockUIGestureRecognizing() - + given(mockTapGesture) .state.willReturn(.ended) - + given(mockTapGesture) .location(ofTouch: .value(1), in: .any) .willReturn(CGPoint(x: 10, y: 10)) - + let result = mapView.processContextFromGesture(maplibreMapView, gesture: gesture, sender: mockTapGesture) - + XCTAssertEqual(result.gestureMethod, .tap(numberOfTaps: 2)) XCTAssertEqual(result.point, CGPoint(x: 10, y: 10)) // This is what the un-rendered map view returns. We're simply testing it returns something. XCTAssertEqual(result.coordinate.latitude, 15, accuracy: 1) XCTAssertEqual(result.coordinate.longitude, -15, accuracy: 1) } - + func testLongPressGesture() { let gesture = MapGesture(method: .longPress(minimumDuration: 1)) { _ in // Do nothing } - + let mockTapGesture = MockUIGestureRecognizing() - + given(mockTapGesture) .state.willReturn(.ended) - + given(mockTapGesture) .location(in: .any) .willReturn(CGPoint(x: 10, y: 10)) - + let result = mapView.processContextFromGesture(maplibreMapView, gesture: gesture, sender: mockTapGesture) - + XCTAssertEqual(result.gestureMethod, .longPress(minimumDuration: 1)) XCTAssertEqual(result.point, CGPoint(x: 10, y: 10)) // This is what the un-rendered map view returns. We're simply testing it returns something. diff --git a/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift b/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift index 39a2f64..411a7bc 100644 --- a/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift +++ b/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift @@ -1,14 +1,13 @@ -import XCTest -import MockableTest import CoreLocation +import MockableTest +import XCTest @testable import MapLibreSwiftUI final class MapViewCoordinatorCameraTests: XCTestCase { - var maplibreMapView: MockMLNMapViewCameraUpdating! var mapView: MapView! var coordinator: MapView.Coordinator! - + override func setUp() async throws { maplibreMapView = MockMLNMapViewCameraUpdating() mapView = MapView(styleURL: URL(string: "https://maplibre.org")!) @@ -16,158 +15,157 @@ final class MapViewCoordinatorCameraTests: XCTestCase { // No action } } - + func testUnchangedCamera() { let camera: MapViewCamera = .default() - + coordinator.updateCamera(mapView: maplibreMapView, camera: camera, animated: false) // Run a second update. We're testing that the snapshotCamera correctly exits the function // when nothing changed. coordinator.updateCamera(mapView: maplibreMapView, camera: camera, animated: false) - + // All of the actions only allow 1 count of set even though we've run the action twice. // This verifies the comment above. verify(maplibreMapView) .userTrackingMode(newValue: .value(.none)) .setterCalled(count: 1) - + verify(maplibreMapView) .setCenter(.value(MapViewCamera.Defaults.coordinate), zoomLevel: .value(10), direction: .value(0), animated: .value(false)) .called(count: 1) - + verify(maplibreMapView) .minimumPitch(newValue: .value(0)) .setterCalled(count: 1) - + verify(maplibreMapView) .maximumPitch(newValue: .value(60)) .setterCalled(count: 1) - + verify(maplibreMapView) .setZoomLevel(.any, animated: .any) .called(count: 0) } - + func testCenterCameraUpdate() { let coordinate = CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4) let newCamera: MapViewCamera = .center(coordinate, zoom: 13) - + coordinator.updateCamera(mapView: maplibreMapView, camera: newCamera, animated: false) - + verify(maplibreMapView) .userTrackingMode(newValue: .value(.none)) .setterCalled(count: 1) - + verify(maplibreMapView) .setCenter(.value(coordinate), zoomLevel: .value(13), direction: .value(0), animated: .value(false)) .called(count: 1) - + verify(maplibreMapView) .minimumPitch(newValue: .value(0)) .setterCalled(count: 1) - + verify(maplibreMapView) .maximumPitch(newValue: .value(60)) .setterCalled(count: 1) - + verify(maplibreMapView) .setZoomLevel(.any, animated: .any) .called(count: 0) } - + func testUserTrackingCameraUpdate() { let newCamera: MapViewCamera = .trackUserLocation() - + coordinator.updateCamera(mapView: maplibreMapView, camera: newCamera, animated: false) - + verify(maplibreMapView) .userTrackingMode(newValue: .value(.follow)) .setterCalled(count: 1) - + verify(maplibreMapView) .setCenter(.any, zoomLevel: .any, direction: .any, animated: .any) .called(count: 0) - + verify(maplibreMapView) .minimumPitch(newValue: .value(0)) .setterCalled(count: 1) - + verify(maplibreMapView) .maximumPitch(newValue: .value(60)) .setterCalled(count: 1) - + verify(maplibreMapView) .setZoomLevel(.value(10), animated: .value(false)) .called(count: 1) } - + func testUserTrackingWithCourseCameraUpdate() { let newCamera: MapViewCamera = .trackUserLocationWithCourse() - + coordinator.updateCamera(mapView: maplibreMapView, camera: newCamera, animated: false) - + verify(maplibreMapView) .userTrackingMode(newValue: .value(.followWithCourse)) .setterCalled(count: 1) - + verify(maplibreMapView) .setCenter(.any, zoomLevel: .any, direction: .any, animated: .any) .called(count: 0) - + verify(maplibreMapView) .minimumPitch(newValue: .value(0)) .setterCalled(count: 1) - + verify(maplibreMapView) .maximumPitch(newValue: .value(60)) .setterCalled(count: 1) - + verify(maplibreMapView) .setZoomLevel(.value(10), animated: .value(false)) .called(count: 1) } - + func testUserTrackingWithHeadingUpdate() { let newCamera: MapViewCamera = .trackUserLocationWithHeading() - + coordinator.updateCamera(mapView: maplibreMapView, camera: newCamera, animated: false) - + verify(maplibreMapView) .userTrackingMode(newValue: .value(.followWithHeading)) .setterCalled(count: 1) - + verify(maplibreMapView) .setCenter(.any, zoomLevel: .any, direction: .any, animated: .any) .called(count: 0) - + verify(maplibreMapView) .minimumPitch(newValue: .value(0)) .setterCalled(count: 1) - + verify(maplibreMapView) .maximumPitch(newValue: .value(60)) .setterCalled(count: 1) - + verify(maplibreMapView) .setZoomLevel(.value(10), animated: .value(false)) .called(count: 1) } - + // TODO: Test Rect & Showcase once we build it! - } diff --git a/Tests/MapLibreSwiftUITests/Models/Gesture/MapGestureTests.swift b/Tests/MapLibreSwiftUITests/Models/Gesture/MapGestureTests.swift index f105153..c699809 100644 --- a/Tests/MapLibreSwiftUITests/Models/Gesture/MapGestureTests.swift +++ b/Tests/MapLibreSwiftUITests/Models/Gesture/MapGestureTests.swift @@ -2,35 +2,34 @@ import XCTest @testable import MapLibreSwiftUI final class MapGestureTests: XCTestCase { - func testTapGestureDefaults() { let gesture = MapGesture(method: .tap(), onChange: { _ in }) - + XCTAssertEqual(gesture.method, .tap()) XCTAssertNil(gesture.gestureRecognizer) } - + func testTapGesture() { let gesture = MapGesture(method: .tap(numberOfTaps: 3), onChange: { _ in }) - + XCTAssertEqual(gesture.method, .tap(numberOfTaps: 3)) XCTAssertNil(gesture.gestureRecognizer) } - + func testLongPressGestureDefaults() { let gesture = MapGesture(method: .longPress(), onChange: { _ in }) - + XCTAssertEqual(gesture.method, .longPress()) XCTAssertNil(gesture.gestureRecognizer) } - + func testLongPressGesture() { let gesture = MapGesture(method: .longPress(minimumDuration: 3), onChange: { _ in }) - + XCTAssertEqual(gesture.method, .longPress(minimumDuration: 3)) XCTAssertNil(gesture.gestureRecognizer) } diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraChangeReasonTests.swift b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraChangeReasonTests.swift index 3cfaa8e..52eb1f1 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraChangeReasonTests.swift +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraChangeReasonTests.swift @@ -1,54 +1,53 @@ -import XCTest import MapLibre +import XCTest @testable import MapLibreSwiftUI final class CameraChangeReasonTests: XCTestCase { - func testProgrammatic() { let mlnReason: MLNCameraChangeReason = [.programmatic] XCTAssertEqual(CameraChangeReason(mlnReason), .programmatic) } - + func testTransitionCancelled() { let mlnReason: MLNCameraChangeReason = [.transitionCancelled] XCTAssertEqual(CameraChangeReason(mlnReason), .transitionCancelled) } - + func testResetNorth() { let mlnReason: MLNCameraChangeReason = [.programmatic, .resetNorth] XCTAssertEqual(CameraChangeReason(mlnReason), .resetNorth) } - + func testGesturePan() { let mlnReason: MLNCameraChangeReason = [.gesturePan] XCTAssertEqual(CameraChangeReason(mlnReason), .gesturePan) } - + func testGesturePinch() { let mlnReason: MLNCameraChangeReason = [.gesturePinch] XCTAssertEqual(CameraChangeReason(mlnReason), .gesturePinch) } - + func testGestureRotate() { let mlnReason: MLNCameraChangeReason = [.gestureRotate] XCTAssertEqual(CameraChangeReason(mlnReason), .gestureRotate) } - + func testGestureTilt() { let mlnReason: MLNCameraChangeReason = [.programmatic, .gestureTilt] XCTAssertEqual(CameraChangeReason(mlnReason), .gestureTilt) } - + func testGestureZoomIn() { - let mlnReason: MLNCameraChangeReason = [.gestureZoomIn, .programmatic, ] + let mlnReason: MLNCameraChangeReason = [.gestureZoomIn, .programmatic] XCTAssertEqual(CameraChangeReason(mlnReason), .gestureZoomIn) } - + func testGestureZoomOut() { let mlnReason: MLNCameraChangeReason = [.programmatic, .gestureZoomOut] XCTAssertEqual(CameraChangeReason(mlnReason), .gestureZoomOut) } - + func testGestureOneFingerZoom() { let mlnReason: MLNCameraChangeReason = [.programmatic, .gestureOneFingerZoom] XCTAssertEqual(CameraChangeReason(mlnReason), .gestureOneFingerZoom) diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraPitchTests.swift b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraPitchTests.swift index 36c5401..1cb717f 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraPitchTests.swift +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraPitchTests.swift @@ -2,19 +2,18 @@ import XCTest @testable import MapLibreSwiftUI final class CameraPitchTests: XCTestCase { - func testFreePitch() { let pitch: CameraPitch = .free XCTAssertEqual(pitch.rangeValue.lowerBound, 0) XCTAssertEqual(pitch.rangeValue.upperBound, 60) } - + func testRangePitch() { let pitch = CameraPitch.freeWithinRange(minimum: 9, maximum: 29) XCTAssertEqual(pitch.rangeValue.lowerBound, 9) XCTAssertEqual(pitch.rangeValue.upperBound, 29) } - + func testFixedPitch() { let pitch = CameraPitch.fixed(41) XCTAssertEqual(pitch.rangeValue.lowerBound, 41) diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift index b0f42ec..5365a8d 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift @@ -1,43 +1,45 @@ -import XCTest import CoreLocation +import XCTest @testable import MapLibreSwiftUI final class CameraStateTests: XCTestCase { - func testCenterCameraState() { let expectedCoordinate = CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4) let state: CameraState = .centered(onCoordinate: expectedCoordinate) XCTAssertEqual(state, .centered(onCoordinate: CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4))) - XCTAssertEqual(String(describing: state), "CameraState.centered(onCoordinate: CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4)") + XCTAssertEqual( + String(describing: state), + "CameraState.centered(onCoordinate: CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4)" + ) } - + func testTrackingUserLocation() { let state: CameraState = .trackingUserLocation XCTAssertEqual(state, .trackingUserLocation) XCTAssertEqual(String(describing: state), "CameraState.trackingUserLocation") } - + func testTrackingUserLocationWithHeading() { let state: CameraState = .trackingUserLocationWithHeading XCTAssertEqual(state, .trackingUserLocationWithHeading) XCTAssertEqual(String(describing: state), "CameraState.trackingUserLocationWithHeading") } - + func testTrackingUserLocationWithCourse() { let state: CameraState = .trackingUserLocationWithCourse XCTAssertEqual(state, .trackingUserLocationWithCourse) XCTAssertEqual(String(describing: state), "CameraState.trackingUserLocationWithCourse") } - + func testRect() { let northeast = CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4) let southwest = CLLocationCoordinate2D(latitude: 34.5, longitude: 45.6) - + let state: CameraState = .rect(northeast: northeast, southwest: southwest) XCTAssertEqual(state, .rect(northeast: northeast, southwest: southwest)) XCTAssertEqual( String(describing: state), - "CameraState.rect(northeast: CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4), southwest: CLLocationCoordinate2D(latitude: 34.5, longitude: 45.6))") + "CameraState.rect(northeast: CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4), southwest: CLLocationCoordinate2D(latitude: 34.5, longitude: 45.6))" + ) } - } diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift b/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift index 70eb264..1031264 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift @@ -1,51 +1,49 @@ -import XCTest import CoreLocation +import XCTest @testable import MapLibreSwiftUI final class MapViewCameraTests: XCTestCase { - func testCenterCamera() { let expectedCoordinate = CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4) let pitch: CameraPitch = .freeWithinRange(minimum: 12, maximum: 34) let direction: CLLocationDirection = 23 - + let camera = MapViewCamera.center(expectedCoordinate, zoom: 12, pitch: pitch, direction: direction) - + XCTAssertEqual(camera.state, .centered(onCoordinate: expectedCoordinate)) XCTAssertEqual(camera.zoom, 12) XCTAssertEqual(camera.pitch, pitch) XCTAssertEqual(camera.direction, direction) } - + func testTrackingUserLocation() { let pitch: CameraPitch = .freeWithinRange(minimum: 12, maximum: 34) let camera = MapViewCamera.trackUserLocation(pitch: pitch) - + XCTAssertEqual(camera.state, .trackingUserLocation) XCTAssertEqual(camera.zoom, 10) XCTAssertEqual(camera.pitch, pitch) XCTAssertEqual(camera.direction, 0) } - + func testTrackUserLocationWithCourse() { let pitch: CameraPitch = .freeWithinRange(minimum: 12, maximum: 34) let camera = MapViewCamera.trackUserLocationWithCourse(zoom: 18, pitch: pitch) - + XCTAssertEqual(camera.state, .trackingUserLocationWithCourse) XCTAssertEqual(camera.zoom, 18) XCTAssertEqual(camera.pitch, pitch) XCTAssertEqual(camera.direction, 0) } - + func testTrackUserLocationWithHeading() { let camera = MapViewCamera.trackUserLocationWithHeading() - + XCTAssertEqual(camera.state, .trackingUserLocationWithHeading) XCTAssertEqual(camera.zoom, 10) XCTAssertEqual(camera.pitch, .free) XCTAssertEqual(camera.direction, 0) } - + // TODO: Add additional camera tests once behaviors are added (e.g. rect) - } diff --git a/Tests/MapLibreSwiftUITests/Support/XCTestAssertView.swift b/Tests/MapLibreSwiftUITests/Support/XCTestAssertView.swift index 27a901b..7ea9ff5 100644 --- a/Tests/MapLibreSwiftUITests/Support/XCTestAssertView.swift +++ b/Tests/MapLibreSwiftUITests/Support/XCTestAssertView.swift @@ -1,25 +1,23 @@ +import MapLibreSwiftUI +import SnapshotTesting import SwiftUI import XCTest -import SnapshotTesting -import MapLibreSwiftUI // TODO: This is a WIP that needs some additional eyes extension XCTestCase { - - func assertView( + func assertView( named name: String? = nil, record: Bool = false, frame: CGSize = CGSize(width: 430, height: 932), - expectation: XCTestExpectation? = nil, - @ViewBuilder content: () -> Content, + expectation _: XCTestExpectation? = nil, + @ViewBuilder content: () -> some View, file: StaticString = #file, testName: String = #function, line: UInt = #line ) { - let view = content() .frame(width: frame.width, height: frame.height) - + assertSnapshot(matching: view, as: .image(precision: 0.9, perceptualPrecision: 0.95), named: name, @@ -49,4 +47,3 @@ extension Snapshotting { ) } } -