Skip to content

Commit

Permalink
[CP] filterable attributions (#2375)
Browse files Browse the repository at this point in the history
  • Loading branch information
evil159 authored Dec 4, 2024
1 parent b25c48d commit ff2a830
Show file tree
Hide file tree
Showing 20 changed files with 532 additions and 319 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Mapbox welcomes participation and contributions from everyone.

## 11.5.3 - 4 December, 2024

* Add a way to filter attribution menu items.


## 11.5.2 - 29 July, 2024

* Update CoreMaps to the 11.5.3 version to fix crash on iOS simulators when using symbols with occlusion.
Expand Down
122 changes: 122 additions & 0 deletions Sources/MapboxMaps/Attribution/AttributionMenu.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import Foundation
import UIKit
@_implementationOnly import MapboxCommon_Private

/// API for attribution menu configuration
/// Restricted API. Please contact Mapbox to discuss your use case if you intend to use this property.
@_spi(Restricted)
public class AttributionMenu {
private let urlOpener: AttributionURLOpener
private let feedbackURLRef: Ref<URL?>

/// Filters attribution menu items based on the provided closure.
public var filter: ((AttributionMenuItem) -> Bool)?

init(
urlOpener: AttributionURLOpener,
feedbackURLRef: Ref<URL?>,
filter: ((AttributionMenuItem) -> Bool)? = nil
) {
self.urlOpener = urlOpener
self.filter = filter
self.feedbackURLRef = feedbackURLRef
}
}

extension AttributionMenu {
var isMetricsEnabled: Bool {
get { UserDefaults.standard.MGLMapboxMetricsEnabled }
set { UserDefaults.standard.MGLMapboxMetricsEnabled = newValue }
}

internal func menu(from attributions: [Attribution]) -> AttributionMenuSection {
var elements = [AttributionMenuElement]()
let items = attributions.compactMap { attribution in
switch attribution.kind {
case .actionable(let url):
return AttributionMenuItem(title: attribution.localizedTitle, id: .copyright, category: .main) { [weak self] in
self?.urlOpener.openAttributionURL(url)
}
case .nonActionable:
return AttributionMenuItem(title: attribution.localizedTitle, id: .copyright, category: .main)
case .feedback:
guard let feedbackURL = feedbackURLRef.value else { return nil }
return AttributionMenuItem(title: attribution.localizedTitle, id: .contribute, category: .main) { [weak self] in
self?.urlOpener.openAttributionURL(feedbackURL)
}
}
}
let menuSubtitle: String?
if items.count == 1, let item = items.first, item.action == nil {
menuSubtitle = item.title
} else {
menuSubtitle = nil
elements.append(contentsOf: items.map(AttributionMenuElement.item))
}

elements.append(.section(telemetryMenu))

elements.append(.item(privacyPolicyItem))
elements.append(.item(cancelItem))

let mainTitle = Bundle.mapboxMaps.localizedString(
forKey: "SDK_NAME",
value: "Powered by Mapbox",
table: Ornaments.localizableTableName
)

return AttributionMenuSection(title: mainTitle, subtitle: menuSubtitle, category: .main, elements: elements)
}

private var cancelItem: AttributionMenuItem {
let cancelTitle = NSLocalizedString("ATTRIBUTION_CANCEL",
tableName: Ornaments.localizableTableName,
bundle: .mapboxMaps,
value: "Cancel",
comment: "Title of button for dismissing attribution action sheet")

return AttributionMenuItem(title: cancelTitle, style: .cancel, id: .cancel, category: .main) { }
}

private var privacyPolicyItem: AttributionMenuItem {
let privacyPolicyTitle = NSLocalizedString("ATTRIBUTION_PRIVACY_POLICY",
tableName: Ornaments.localizableTableName,
bundle: .mapboxMaps,
value: "Mapbox Privacy Policy",
comment: "Privacy policy action in attribution sheet")

return AttributionMenuItem(title: privacyPolicyTitle, id: .privacyPolicy, category: .main) { [weak self] in
self?.urlOpener.openAttributionURL(Attribution.privacyPolicyURL)
}
}

private var telemetryMenu: AttributionMenuSection {
let telemetryTitle = TelemetryStrings.telemetryTitle
let telemetryURL = URL(string: Ornaments.telemetryURL)!
let message: String
let participateTitle: String
let declineTitle: String

if isMetricsEnabled {
message = TelemetryStrings.telemetryEnabledMessage
participateTitle = TelemetryStrings.telemetryEnabledOnMessage
declineTitle = TelemetryStrings.telemetryEnabledOffMessage
} else {
message = TelemetryStrings.telemetryDisabledMessage
participateTitle = TelemetryStrings.telemetryDisabledOnMessage
declineTitle = TelemetryStrings.telemetryDisabledOffMessage
}

return AttributionMenuSection(title: telemetryTitle, actionTitle: TelemetryStrings.telemetryName, subtitle: message, category: .telemetry, elements: [
AttributionMenuItem(title: TelemetryStrings.telemetryMore, id: .telemetryInfo, category: .telemetry) { [weak self] in
self?.urlOpener.openAttributionURL(telemetryURL)
},
AttributionMenuItem(title: declineTitle, id: .disable, category: .telemetry) { [weak self] in
self?.isMetricsEnabled = false
},
AttributionMenuItem(title: participateTitle, style: .cancel, id: .enable, category: .telemetry) { [weak self] in
self?.isMetricsEnabled = true
}
].map(AttributionMenuElement.item))
}
}
91 changes: 91 additions & 0 deletions Sources/MapboxMaps/Attribution/AttributionMenuItem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import Foundation
import UIKit

/// A menu item entry in the attribution list.
@_spi(Restricted)
public struct AttributionMenuItem {

/// Denotes a category(section) that item belongs to.
public struct Category: RawRepresentable {
public let rawValue: String

public init(rawValue: String) {
self.rawValue = rawValue
}

/// Main(root) category
public static let main = Category(rawValue: "com.mapbox.maps.attribution.main")

/// Category for opting in/out of telemetry
public static let telemetry = Category(rawValue: "com.mapbox.maps.attribution.telemetry")

/// Category for opting in/out of geofencing
public static let geofencing = Category(rawValue: "com.mapbox.maps.attribution.geofencing")
}

/// Denotes an identifier of an item
public struct ID: RawRepresentable {
public let rawValue: String

public init(rawValue: String) {
self.rawValue = rawValue
}

/// Item attributing a copyright
public static let copyright = ID(rawValue: "com.mapbox.maps.attribution.copyright")

/// Represents an item opening a contribution form
public static let contribute = ID(rawValue: "com.mapbox.maps.attribution.contribute")

/// Opens privacy policy page
public static let privacyPolicy = ID(rawValue: "com.mapbox.maps.attribution.privacyPolicy")

/// Opens page with the info about Mapbox telemetry
public static let telemetryInfo = ID(rawValue: "com.mapbox.maps.attribution.telemetryInfo")

/// Item that enables a certain option, typically associated with a category
/// e.g. `category: .telemetry, id: .enable`
public static let enable = ID(rawValue: "com.mapbox.maps.attribution.enable")

/// Item that disables a certain option, typically associated with a category
/// e.g. `category: .telemetry, id: .disable`
public static let disable = ID(rawValue: "com.mapbox.maps.attribution.disable")

/// Item that dismisses the attribution menu
public static let cancel = ID(rawValue: "com.mapbox.maps.attribution.cancel")
}

/// Title of the attribution menu item
public let title: String

/// Identifier of the item
public let id: ID

/// Category of the item
public let category: Category

let action: (() -> Void)?
let style: Style

init(title: String, style: Style = .default, id: ID, category: Category, action: (() -> Void)? = nil) {
self.title = title
self.id = id
self.category = category
self.action = action
self.style = style
}
}

extension AttributionMenuItem {
enum Style {
case `default`
case cancel

var uiActionStyle: UIAlertAction.Style {
switch self {
case .default: return .default
case .cancel: return .cancel
}
}
}
}
35 changes: 35 additions & 0 deletions Sources/MapboxMaps/Attribution/AttributionMenuSection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation
import UIKit

indirect enum AttributionMenuElement {
case section(AttributionMenuSection)
case item(AttributionMenuItem)
}

internal struct AttributionMenuSection {
var title: String
var actionTitle: String?
var subtitle: String?
var category: AttributionMenuItem.Category
var elements: [AttributionMenuElement]

init(title: String, actionTitle: String? = nil, subtitle: String? = nil, category: AttributionMenuItem.Category, elements: [AttributionMenuElement]) {
self.title = title
self.actionTitle = actionTitle
self.subtitle = subtitle
self.category = category
self.elements = elements
}

mutating func filter(_ filter: (AttributionMenuItem) -> Bool) {
elements = elements.compactMap { element in
switch element {
case .item(let item):
return filter(item) ? .item(item) : nil
case .section(var section):
section.filter(filter)
return .section(section)
}
}
}
}
22 changes: 5 additions & 17 deletions Sources/MapboxMaps/Foundation/MapView+Attribution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,11 @@ extension MapView: AttributionDialogManagerDelegate {
func viewControllerForPresenting(_ attributionDialogManager: AttributionDialogManager) -> UIViewController? {
parentViewController?.topmostPresentedViewController
}
}

func attributionDialogManager(_ attributionDialogManager: AttributionDialogManager, didTriggerActionFor attribution: Attribution) {
switch attribution.kind {
case .actionable(let url):
Log.debug(forMessage: "Open url: \(url))", category: "Attribution")
attributionUrlOpener.openAttributionURL(url)
case .feedback:
let url = mapboxFeedbackURL()
Log.debug(forMessage: "Open url: \(url))", category: "Attribution")
attributionUrlOpener.openAttributionURL(url)
case .nonActionable:
break
}
}

internal func mapboxFeedbackURL(accessToken: String = MapboxOptions.accessToken) -> URL {
let cameraState = self.mapboxMap.cameraState
internal extension MapboxMap {
func mapboxFeedbackURL(accessToken: String = MapboxOptions.accessToken) -> URL {
let cameraState = self.cameraState

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

let sdkVersion = Bundle.mapboxMapsMetadata.version

if let styleURIString = mapboxMap.styleURI?.rawValue,
if let styleURIString = styleURI?.rawValue,
let styleURL = URL(string: styleURIString),
styleURL.scheme == "mapbox",
styleURL.host == "styles" {
Expand Down
14 changes: 12 additions & 2 deletions Sources/MapboxMaps/Foundation/MapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import MetalKit
// swiftlint:disable:next type_body_length
open class MapView: UIView, SizeTrackingLayerDelegate {

/// Handles attribution menu customization
/// Restricted API. Please contact Mapbox to discuss your use case if you intend to use this property.
@_spi(Restricted)
public private(set) var attributionMenu: AttributionMenu!

open override class var layerClass: AnyClass { SizeTrackingLayer.self }

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

// Initialize the attribution manager
// Initialize the attribution manager and menu
attributionMenu = AttributionMenu(
urlOpener: attributionUrlOpener,
feedbackURLRef: Ref { [weak mapboxMap] in mapboxMap?.mapboxFeedbackURL() }
)
attributionDialogManager = AttributionDialogManager(
dataSource: mapboxMap,
delegate: self)
delegate: self,
attributionMenu: attributionMenu)

// Initialize/Configure ornaments manager
ornaments = OrnamentsManager(
Expand Down
4 changes: 0 additions & 4 deletions Sources/MapboxMaps/Ornaments/InfoButtonOrnament.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ internal class InfoButtonOrnament: UIView {
}
}

internal var isMetricsEnabled: Bool {
return UserDefaults.standard.MGLMapboxMetricsEnabled
}

internal weak var delegate: InfoButtonOrnamentDelegate?

internal init() {
Expand Down
3 changes: 2 additions & 1 deletion Sources/MapboxMaps/Style/Attribution.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import WebKit
@_implementationOnly import MapboxCommon_Private

struct Attribution: Hashable {

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

var title: String
var kind: Kind
Expand Down
Loading

0 comments on commit ff2a830

Please sign in to comment.