Skip to content

Commit 7539ddc

Browse files
evil159github-actions[bot]
authored andcommitted
Expose screen culling shape for SwiftUI (#3529)
Address https://mapbox.atlassian.net/browse/MAPSIOS-1819 cc @mapbox/sdk-ci GitOrigin-RevId: af6d1331f68aef6740dcf5e3f385b7052264e8d7
1 parent 6589a14 commit 7539ddc

File tree

7 files changed

+83
-1
lines changed

7 files changed

+83
-1
lines changed

Sources/Examples/SwiftUI Examples/Testing Examples/MapSettingsExample.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ struct Settings {
1010
var ornamentSettings = OrnamentSettings()
1111
var debugOptions: MapViewDebugOptions = [.camera]
1212
var performance = PerformanceSettings()
13+
var isCullingShapeEnabled = true
1314

1415
struct OrnamentSettings {
1516
var isScaleBarVisible = true
@@ -25,6 +26,18 @@ struct Settings {
2526
PerformanceStatisticsOptions(samplerOptions, samplingDurationMillis: Double(samplingDurationMillis))
2627
}
2728
}
29+
30+
fileprivate var cullingShape: [CGPoint] {
31+
guard isCullingShapeEnabled else { return [] }
32+
return [
33+
CGPoint(x: 0.35, y: 0.37), // top-left
34+
CGPoint(x: 0.65, y: 0.37), // top-right
35+
CGPoint(x: 0.85, y: 0.50), // right
36+
CGPoint(x: 0.65, y: 0.63), // bottom-right
37+
CGPoint(x: 0.35, y: 0.63), // bottom-left
38+
CGPoint(x: 0.15, y: 0.50) // left
39+
]
40+
}
2841
}
2942

3043
struct MapSettingsExample: View {
@@ -49,6 +62,7 @@ struct MapSettingsExample: View {
4962
compass: CompassViewOptions(visibility: settings.ornamentSettings.isCompassVisible ? .visible : .hidden)
5063
))
5164
.debugOptions(settings.debugOptions)
65+
.screenCullingShape(settings.cullingShape)
5266
.ignoresSafeArea()
5367
.sheet(isPresented: $settingsOpened) {
5468
SettingsView(settings: $settings)
@@ -70,6 +84,30 @@ struct MapSettingsExample: View {
7084
}
7185
.floating()
7286
}
87+
88+
if settings.isCullingShapeEnabled {
89+
ZStack {
90+
Color.black.opacity(0.7)
91+
.compositingGroup()
92+
.overlay {
93+
ZStack {
94+
// cutout
95+
HexagonShape(points: settings.cullingShape)
96+
.fill()
97+
.blendMode(.destinationOut)
98+
99+
// border
100+
HexagonShape(points: settings.cullingShape)
101+
.stroke(.white, lineWidth: 4)
102+
.shadow(color: .white, radius: 5)
103+
}
104+
105+
}
106+
}
107+
.ignoresSafeArea()
108+
.allowsHitTesting(false)
109+
.compositingGroup()
110+
}
73111
}
74112
}
75113

@@ -148,6 +186,13 @@ struct SettingsView: View {
148186
} header: {
149187
Text("Debug Options")
150188
}
189+
190+
Section {
191+
Toggle("Simulate hex screen shape", isOn: $settings.isCullingShapeEnabled)
192+
} header: {
193+
Text("Screen culling shape")
194+
}
195+
151196
Section {
152197
let samplerOptions = [
153198
("Per Frame", PerformanceStatisticsOptions.SamplerOptions.perFrame),
@@ -173,6 +218,23 @@ struct SettingsView: View {
173218
}
174219
}
175220

221+
struct HexagonShape: Shape {
222+
let points: [CGPoint]
223+
224+
func path(in rect: CGRect) -> Path {
225+
let scaledPoints = points.map { CGPoint(x: $0.x * rect.width, y: $0.y * rect.height) }
226+
227+
var path = Path()
228+
path.move(to: scaledPoints[0])
229+
for point in scaledPoints.dropFirst() {
230+
path.addLine(to: point)
231+
}
232+
path.closeSubpath()
233+
234+
return path
235+
}
236+
}
237+
176238
private extension Binding where Value: OptionSet {
177239
func contains(option: Value.Element) -> Binding<Bool> {
178240
Binding<Bool> {

Sources/MapboxMaps/Foundation/MapboxMap.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ protocol MapboxMapProtocol: AnyObject {
7878
stateKey: T.StateKey?,
7979
callback: ((Error?) -> Void)?
8080
) -> Cancelable
81+
var screenCullingShape: [CGPoint] { get set }
8182
}
8283

8384
// swiftlint:disable type_body_length

Sources/MapboxMaps/SwiftUI/Deps.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ struct MapDependencies {
2121
var viewportOptions = ViewportOptions(transitionsToIdleUponUserInteraction: true, usesSafeAreaInsetsAsPadding: true)
2222
var performanceStatisticsParameters: Map.PerformanceStatisticsParameters?
2323
var attributionMenuFilter: ((AttributionMenuItem) -> Bool)?
24+
var screenCullingShape = [CGPoint]()
2425

2526
var onMapTap: ((InteractionContext) -> Void)?
2627
var onMapLongPress: ((InteractionContext) -> Void)?

Sources/MapboxMaps/SwiftUI/Map.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,16 @@ public extension Map {
369369
copy.mapDependencies.additionalSafeArea.updateEdges(edges, length)
370370
return copy
371371
}
372+
373+
/// A convex polygon that describes the shape of the screen in case it is non-rectangular.
374+
/// Every coordinate is in 0 to 1 range, with (0, 0) being the map view top-left and (1, 1) the bottom-right.
375+
/// The points have to be given in clockwise winding order. The polygon will be closed automatically, so for a rectangular shape, pass in 4 points.
376+
/// Use this if the visible screen area is obscured enough that using a custom shape improves performance.
377+
@_documentation(visibility: public)
378+
@_spi(Experimental)
379+
func screenCullingShape(_ shape: [CGPoint]) -> Self {
380+
copyAssigned(self, \.mapDependencies.screenCullingShape, shape)
381+
}
372382
}
373383

374384
extension Map {

Sources/MapboxMaps/SwiftUI/MapBasicCoordinator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ final class MapBasicCoordinator {
9595
assign(&mapView, \.frameRate, value: deps.frameRate)
9696
assign(&mapView, \.presentationTransactionMode, value: deps.presentationTransactionMode)
9797
assign(&mapView, \.viewportManager.options, value: deps.viewportOptions)
98+
assign(mapboxMap.screenCullingShape, { mapboxMap.screenCullingShape = $0 }, value: deps.screenCullingShape)
9899

99100
cameraChangeHandlers = deps.cameraChangeHandlers
100101
mapView.gestureManager.gestureHandlers = deps.gestureHandlers

Tests/MapboxMapsTests/Foundation/Mocks/MockMapboxMap.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ final class MockMapboxMap: MapboxMapProtocol {
4444

4545
var anchor = CGPoint.zero
4646

47+
@Stubbed
48+
var screenCullingShape: [CGPoint] = []
49+
4750
let setCameraStub = Stub<MapboxMaps.CameraOptions, Void>()
4851
func setCamera(to cameraOptions: MapboxMaps.CameraOptions) {
4952
setCameraStub.call(with: cameraOptions)

Tests/MapboxMapsTests/SwiftUI/MapBasicCoordinatorTests.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,22 @@ final class MapBasicCoordinatorTests: XCTestCase {
5454
XCTAssertEqual(mapboxMap.northOrientationStub.invocations.count, 0)
5555
XCTAssertEqual(mapboxMap.setConstraintModeStub.invocations.count, 0)
5656
XCTAssertEqual(mapboxMap.setViewportModeStub.invocations.count, 0)
57+
XCTAssertEqual(mapboxMap.screenCullingShape, [])
5758

5859
update(with: MapDependencies(
5960
constrainMode: .none,
6061
viewportMode: .flippedY,
61-
orientation: .downwards))
62+
orientation: .downwards,
63+
screenCullingShape: [.init(x: 1, y: 2)]))
6264
XCTAssertEqual(mapboxMap.setConstraintModeStub.invocations.count, 1)
6365
XCTAssertEqual(mapboxMap.setViewportModeStub.invocations.count, 1)
6466
XCTAssertEqual(mapboxMap.northOrientationStub.invocations.count, 1)
67+
XCTAssertEqual(mapboxMap.$screenCullingShape.setStub.invocations.count, 1)
6568

6669
XCTAssertEqual(mapboxMap.setConstraintModeStub.invocations.first?.parameters, ConstrainMode.none)
6770
XCTAssertEqual(mapboxMap.setViewportModeStub.invocations.first?.parameters, .flippedY)
6871
XCTAssertEqual(mapboxMap.northOrientationStub.invocations.first?.parameters, .downwards)
72+
XCTAssertEqual(mapboxMap.$screenCullingShape.setStub.invocations.first?.parameters, [CGPoint(x: 1, y: 2)])
6973
}
7074

7175
func testOrnamentOptions() {

0 commit comments

Comments
 (0)