Skip to content

Commit ff2a830

Browse files
authored
[CP] filterable attributions (#2375)
1 parent b25c48d commit ff2a830

20 files changed

+532
-319
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
Mapbox welcomes participation and contributions from everyone.
44

5+
## 11.5.3 - 4 December, 2024
6+
7+
* Add a way to filter attribution menu items.
8+
9+
510
## 11.5.2 - 29 July, 2024
611

712
* Update CoreMaps to the 11.5.3 version to fix crash on iOS simulators when using symbols with occlusion.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import Foundation
2+
import UIKit
3+
@_implementationOnly import MapboxCommon_Private
4+
5+
/// API for attribution menu configuration
6+
/// Restricted API. Please contact Mapbox to discuss your use case if you intend to use this property.
7+
@_spi(Restricted)
8+
public class AttributionMenu {
9+
private let urlOpener: AttributionURLOpener
10+
private let feedbackURLRef: Ref<URL?>
11+
12+
/// Filters attribution menu items based on the provided closure.
13+
public var filter: ((AttributionMenuItem) -> Bool)?
14+
15+
init(
16+
urlOpener: AttributionURLOpener,
17+
feedbackURLRef: Ref<URL?>,
18+
filter: ((AttributionMenuItem) -> Bool)? = nil
19+
) {
20+
self.urlOpener = urlOpener
21+
self.filter = filter
22+
self.feedbackURLRef = feedbackURLRef
23+
}
24+
}
25+
26+
extension AttributionMenu {
27+
var isMetricsEnabled: Bool {
28+
get { UserDefaults.standard.MGLMapboxMetricsEnabled }
29+
set { UserDefaults.standard.MGLMapboxMetricsEnabled = newValue }
30+
}
31+
32+
internal func menu(from attributions: [Attribution]) -> AttributionMenuSection {
33+
var elements = [AttributionMenuElement]()
34+
let items = attributions.compactMap { attribution in
35+
switch attribution.kind {
36+
case .actionable(let url):
37+
return AttributionMenuItem(title: attribution.localizedTitle, id: .copyright, category: .main) { [weak self] in
38+
self?.urlOpener.openAttributionURL(url)
39+
}
40+
case .nonActionable:
41+
return AttributionMenuItem(title: attribution.localizedTitle, id: .copyright, category: .main)
42+
case .feedback:
43+
guard let feedbackURL = feedbackURLRef.value else { return nil }
44+
return AttributionMenuItem(title: attribution.localizedTitle, id: .contribute, category: .main) { [weak self] in
45+
self?.urlOpener.openAttributionURL(feedbackURL)
46+
}
47+
}
48+
}
49+
let menuSubtitle: String?
50+
if items.count == 1, let item = items.first, item.action == nil {
51+
menuSubtitle = item.title
52+
} else {
53+
menuSubtitle = nil
54+
elements.append(contentsOf: items.map(AttributionMenuElement.item))
55+
}
56+
57+
elements.append(.section(telemetryMenu))
58+
59+
elements.append(.item(privacyPolicyItem))
60+
elements.append(.item(cancelItem))
61+
62+
let mainTitle = Bundle.mapboxMaps.localizedString(
63+
forKey: "SDK_NAME",
64+
value: "Powered by Mapbox",
65+
table: Ornaments.localizableTableName
66+
)
67+
68+
return AttributionMenuSection(title: mainTitle, subtitle: menuSubtitle, category: .main, elements: elements)
69+
}
70+
71+
private var cancelItem: AttributionMenuItem {
72+
let cancelTitle = NSLocalizedString("ATTRIBUTION_CANCEL",
73+
tableName: Ornaments.localizableTableName,
74+
bundle: .mapboxMaps,
75+
value: "Cancel",
76+
comment: "Title of button for dismissing attribution action sheet")
77+
78+
return AttributionMenuItem(title: cancelTitle, style: .cancel, id: .cancel, category: .main) { }
79+
}
80+
81+
private var privacyPolicyItem: AttributionMenuItem {
82+
let privacyPolicyTitle = NSLocalizedString("ATTRIBUTION_PRIVACY_POLICY",
83+
tableName: Ornaments.localizableTableName,
84+
bundle: .mapboxMaps,
85+
value: "Mapbox Privacy Policy",
86+
comment: "Privacy policy action in attribution sheet")
87+
88+
return AttributionMenuItem(title: privacyPolicyTitle, id: .privacyPolicy, category: .main) { [weak self] in
89+
self?.urlOpener.openAttributionURL(Attribution.privacyPolicyURL)
90+
}
91+
}
92+
93+
private var telemetryMenu: AttributionMenuSection {
94+
let telemetryTitle = TelemetryStrings.telemetryTitle
95+
let telemetryURL = URL(string: Ornaments.telemetryURL)!
96+
let message: String
97+
let participateTitle: String
98+
let declineTitle: String
99+
100+
if isMetricsEnabled {
101+
message = TelemetryStrings.telemetryEnabledMessage
102+
participateTitle = TelemetryStrings.telemetryEnabledOnMessage
103+
declineTitle = TelemetryStrings.telemetryEnabledOffMessage
104+
} else {
105+
message = TelemetryStrings.telemetryDisabledMessage
106+
participateTitle = TelemetryStrings.telemetryDisabledOnMessage
107+
declineTitle = TelemetryStrings.telemetryDisabledOffMessage
108+
}
109+
110+
return AttributionMenuSection(title: telemetryTitle, actionTitle: TelemetryStrings.telemetryName, subtitle: message, category: .telemetry, elements: [
111+
AttributionMenuItem(title: TelemetryStrings.telemetryMore, id: .telemetryInfo, category: .telemetry) { [weak self] in
112+
self?.urlOpener.openAttributionURL(telemetryURL)
113+
},
114+
AttributionMenuItem(title: declineTitle, id: .disable, category: .telemetry) { [weak self] in
115+
self?.isMetricsEnabled = false
116+
},
117+
AttributionMenuItem(title: participateTitle, style: .cancel, id: .enable, category: .telemetry) { [weak self] in
118+
self?.isMetricsEnabled = true
119+
}
120+
].map(AttributionMenuElement.item))
121+
}
122+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import Foundation
2+
import UIKit
3+
4+
/// A menu item entry in the attribution list.
5+
@_spi(Restricted)
6+
public struct AttributionMenuItem {
7+
8+
/// Denotes a category(section) that item belongs to.
9+
public struct Category: RawRepresentable {
10+
public let rawValue: String
11+
12+
public init(rawValue: String) {
13+
self.rawValue = rawValue
14+
}
15+
16+
/// Main(root) category
17+
public static let main = Category(rawValue: "com.mapbox.maps.attribution.main")
18+
19+
/// Category for opting in/out of telemetry
20+
public static let telemetry = Category(rawValue: "com.mapbox.maps.attribution.telemetry")
21+
22+
/// Category for opting in/out of geofencing
23+
public static let geofencing = Category(rawValue: "com.mapbox.maps.attribution.geofencing")
24+
}
25+
26+
/// Denotes an identifier of an item
27+
public struct ID: RawRepresentable {
28+
public let rawValue: String
29+
30+
public init(rawValue: String) {
31+
self.rawValue = rawValue
32+
}
33+
34+
/// Item attributing a copyright
35+
public static let copyright = ID(rawValue: "com.mapbox.maps.attribution.copyright")
36+
37+
/// Represents an item opening a contribution form
38+
public static let contribute = ID(rawValue: "com.mapbox.maps.attribution.contribute")
39+
40+
/// Opens privacy policy page
41+
public static let privacyPolicy = ID(rawValue: "com.mapbox.maps.attribution.privacyPolicy")
42+
43+
/// Opens page with the info about Mapbox telemetry
44+
public static let telemetryInfo = ID(rawValue: "com.mapbox.maps.attribution.telemetryInfo")
45+
46+
/// Item that enables a certain option, typically associated with a category
47+
/// e.g. `category: .telemetry, id: .enable`
48+
public static let enable = ID(rawValue: "com.mapbox.maps.attribution.enable")
49+
50+
/// Item that disables a certain option, typically associated with a category
51+
/// e.g. `category: .telemetry, id: .disable`
52+
public static let disable = ID(rawValue: "com.mapbox.maps.attribution.disable")
53+
54+
/// Item that dismisses the attribution menu
55+
public static let cancel = ID(rawValue: "com.mapbox.maps.attribution.cancel")
56+
}
57+
58+
/// Title of the attribution menu item
59+
public let title: String
60+
61+
/// Identifier of the item
62+
public let id: ID
63+
64+
/// Category of the item
65+
public let category: Category
66+
67+
let action: (() -> Void)?
68+
let style: Style
69+
70+
init(title: String, style: Style = .default, id: ID, category: Category, action: (() -> Void)? = nil) {
71+
self.title = title
72+
self.id = id
73+
self.category = category
74+
self.action = action
75+
self.style = style
76+
}
77+
}
78+
79+
extension AttributionMenuItem {
80+
enum Style {
81+
case `default`
82+
case cancel
83+
84+
var uiActionStyle: UIAlertAction.Style {
85+
switch self {
86+
case .default: return .default
87+
case .cancel: return .cancel
88+
}
89+
}
90+
}
91+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import Foundation
2+
import UIKit
3+
4+
indirect enum AttributionMenuElement {
5+
case section(AttributionMenuSection)
6+
case item(AttributionMenuItem)
7+
}
8+
9+
internal struct AttributionMenuSection {
10+
var title: String
11+
var actionTitle: String?
12+
var subtitle: String?
13+
var category: AttributionMenuItem.Category
14+
var elements: [AttributionMenuElement]
15+
16+
init(title: String, actionTitle: String? = nil, subtitle: String? = nil, category: AttributionMenuItem.Category, elements: [AttributionMenuElement]) {
17+
self.title = title
18+
self.actionTitle = actionTitle
19+
self.subtitle = subtitle
20+
self.category = category
21+
self.elements = elements
22+
}
23+
24+
mutating func filter(_ filter: (AttributionMenuItem) -> Bool) {
25+
elements = elements.compactMap { element in
26+
switch element {
27+
case .item(let item):
28+
return filter(item) ? .item(item) : nil
29+
case .section(var section):
30+
section.filter(filter)
31+
return .section(section)
32+
}
33+
}
34+
}
35+
}

Sources/MapboxMaps/Foundation/MapView+Attribution.swift

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,11 @@ extension MapView: AttributionDialogManagerDelegate {
55
func viewControllerForPresenting(_ attributionDialogManager: AttributionDialogManager) -> UIViewController? {
66
parentViewController?.topmostPresentedViewController
77
}
8+
}
89

9-
func attributionDialogManager(_ attributionDialogManager: AttributionDialogManager, didTriggerActionFor attribution: Attribution) {
10-
switch attribution.kind {
11-
case .actionable(let url):
12-
Log.debug(forMessage: "Open url: \(url))", category: "Attribution")
13-
attributionUrlOpener.openAttributionURL(url)
14-
case .feedback:
15-
let url = mapboxFeedbackURL()
16-
Log.debug(forMessage: "Open url: \(url))", category: "Attribution")
17-
attributionUrlOpener.openAttributionURL(url)
18-
case .nonActionable:
19-
break
20-
}
21-
}
22-
23-
internal func mapboxFeedbackURL(accessToken: String = MapboxOptions.accessToken) -> URL {
24-
let cameraState = self.mapboxMap.cameraState
10+
internal extension MapboxMap {
11+
func mapboxFeedbackURL(accessToken: String = MapboxOptions.accessToken) -> URL {
12+
let cameraState = self.cameraState
2513

2614
var components = URLComponents(string: "https://apps.mapbox.com/feedback/")!
2715
components.fragment = String(format: "/%.5f/%.5f/%.2f/%.1f/%i",
@@ -38,7 +26,7 @@ extension MapView: AttributionDialogManagerDelegate {
3826

3927
let sdkVersion = Bundle.mapboxMapsMetadata.version
4028

41-
if let styleURIString = mapboxMap.styleURI?.rawValue,
29+
if let styleURIString = styleURI?.rawValue,
4230
let styleURL = URL(string: styleURIString),
4331
styleURL.scheme == "mapbox",
4432
styleURL.host == "styles" {

Sources/MapboxMaps/Foundation/MapView.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import MetalKit
77
// swiftlint:disable:next type_body_length
88
open class MapView: UIView, SizeTrackingLayerDelegate {
99

10+
/// Handles attribution menu customization
11+
/// Restricted API. Please contact Mapbox to discuss your use case if you intend to use this property.
12+
@_spi(Restricted)
13+
public private(set) var attributionMenu: AttributionMenu!
14+
1015
open override class var layerClass: AnyClass { SizeTrackingLayer.self }
1116

1217
// `mapboxMap` depends on `MapInitOptions`, which is not available until
@@ -379,10 +384,15 @@ open class MapView: UIView, SizeTrackingLayerDelegate {
379384
annotations: annotationsImpl,
380385
cameraAnimationsManager: internalCamera)
381386

382-
// Initialize the attribution manager
387+
// Initialize the attribution manager and menu
388+
attributionMenu = AttributionMenu(
389+
urlOpener: attributionUrlOpener,
390+
feedbackURLRef: Ref { [weak mapboxMap] in mapboxMap?.mapboxFeedbackURL() }
391+
)
383392
attributionDialogManager = AttributionDialogManager(
384393
dataSource: mapboxMap,
385-
delegate: self)
394+
delegate: self,
395+
attributionMenu: attributionMenu)
386396

387397
// Initialize/Configure ornaments manager
388398
ornaments = OrnamentsManager(

Sources/MapboxMaps/Ornaments/InfoButtonOrnament.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ internal class InfoButtonOrnament: UIView {
1515
}
1616
}
1717

18-
internal var isMetricsEnabled: Bool {
19-
return UserDefaults.standard.MGLMapboxMetricsEnabled
20-
}
21-
2218
internal weak var delegate: InfoButtonOrnamentDelegate?
2319

2420
internal init() {

Sources/MapboxMaps/Style/Attribution.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import WebKit
3+
@_implementationOnly import MapboxCommon_Private
34

45
struct Attribution: Hashable {
56

@@ -21,7 +22,7 @@ struct Attribution: Hashable {
2122
"https://www.mapbox.com/map-feedback/",
2223
"https://apps.mapbox.com/feedback/"
2324
]
24-
private static let privacyPolicyURL = URL(string: "https://www.mapbox.com/legal/privacy#product-privacy-policy")
25+
internal static let privacyPolicyURL = URL(string: "https://www.mapbox.com/legal/privacy#product-privacy-policy")!
2526

2627
var title: String
2728
var kind: Kind

0 commit comments

Comments
 (0)