diff --git a/CHANGELOG.md b/CHANGELOG.md index a487d49feee3..659f3804aa5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Sources/MapboxMaps/Attribution/AttributionMenu.swift b/Sources/MapboxMaps/Attribution/AttributionMenu.swift new file mode 100644 index 000000000000..d27c6c3d5d5b --- /dev/null +++ b/Sources/MapboxMaps/Attribution/AttributionMenu.swift @@ -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 + + /// Filters attribution menu items based on the provided closure. + public var filter: ((AttributionMenuItem) -> Bool)? + + init( + urlOpener: AttributionURLOpener, + feedbackURLRef: Ref, + 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)) + } +} diff --git a/Sources/MapboxMaps/Attribution/AttributionMenuItem.swift b/Sources/MapboxMaps/Attribution/AttributionMenuItem.swift new file mode 100644 index 000000000000..e438ca35ce4a --- /dev/null +++ b/Sources/MapboxMaps/Attribution/AttributionMenuItem.swift @@ -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 + } + } + } +} diff --git a/Sources/MapboxMaps/Attribution/AttributionMenuSection.swift b/Sources/MapboxMaps/Attribution/AttributionMenuSection.swift new file mode 100644 index 000000000000..06548b09e569 --- /dev/null +++ b/Sources/MapboxMaps/Attribution/AttributionMenuSection.swift @@ -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) + } + } + } +} diff --git a/Sources/MapboxMaps/Foundation/MapView+Attribution.swift b/Sources/MapboxMaps/Foundation/MapView+Attribution.swift index 3ab80a43e159..6766db5e5b52 100644 --- a/Sources/MapboxMaps/Foundation/MapView+Attribution.swift +++ b/Sources/MapboxMaps/Foundation/MapView+Attribution.swift @@ -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", @@ -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" { diff --git a/Sources/MapboxMaps/Foundation/MapView.swift b/Sources/MapboxMaps/Foundation/MapView.swift index ba80ac9344c3..771bb86c0f74 100644 --- a/Sources/MapboxMaps/Foundation/MapView.swift +++ b/Sources/MapboxMaps/Foundation/MapView.swift @@ -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 @@ -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( diff --git a/Sources/MapboxMaps/Ornaments/InfoButtonOrnament.swift b/Sources/MapboxMaps/Ornaments/InfoButtonOrnament.swift index 729d4c4de41d..114bf5d34f49 100644 --- a/Sources/MapboxMaps/Ornaments/InfoButtonOrnament.swift +++ b/Sources/MapboxMaps/Ornaments/InfoButtonOrnament.swift @@ -15,10 +15,6 @@ internal class InfoButtonOrnament: UIView { } } - internal var isMetricsEnabled: Bool { - return UserDefaults.standard.MGLMapboxMetricsEnabled - } - internal weak var delegate: InfoButtonOrnamentDelegate? internal init() { diff --git a/Sources/MapboxMaps/Style/Attribution.swift b/Sources/MapboxMaps/Style/Attribution.swift index 3e6c8de371b5..6aa80c2beadd 100644 --- a/Sources/MapboxMaps/Style/Attribution.swift +++ b/Sources/MapboxMaps/Style/Attribution.swift @@ -1,5 +1,6 @@ import Foundation import WebKit +@_implementationOnly import MapboxCommon_Private struct Attribution: Hashable { @@ -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 diff --git a/Sources/MapboxMaps/Style/AttributionDialogManager.swift b/Sources/MapboxMaps/Style/AttributionDialogManager.swift index 3342e6ba6d2f..76f7b6d56b43 100644 --- a/Sources/MapboxMaps/Style/AttributionDialogManager.swift +++ b/Sources/MapboxMaps/Style/AttributionDialogManager.swift @@ -5,7 +5,6 @@ internal protocol AttributionDataSource: AnyObject { internal protocol AttributionDialogManagerDelegate: AnyObject { func viewControllerForPresenting(_ attributionDialogManager: AttributionDialogManager) -> UIViewController? - func attributionDialogManager(_ attributionDialogManager: AttributionDialogManager, didTriggerActionFor attribution: Attribution) } internal class AttributionDialogManager { @@ -14,94 +13,30 @@ internal class AttributionDialogManager { private weak var delegate: AttributionDialogManagerDelegate? private var inProcessOfParsingAttributions: Bool = false - internal init(dataSource: AttributionDataSource, delegate: AttributionDialogManagerDelegate?) { + private let attributionMenu: AttributionMenu + + init( + dataSource: AttributionDataSource, + delegate: AttributionDialogManagerDelegate?, + attributionMenu: AttributionMenu + ) { self.dataSource = dataSource self.delegate = delegate + self.attributionMenu = attributionMenu } - internal var isMetricsEnabled: Bool { - get { - UserDefaults.standard.MGLMapboxMetricsEnabled - } - set { - UserDefaults.standard.MGLMapboxMetricsEnabled = newValue - } - } - - //swiftlint:disable:next function_body_length - internal func showTelemetryAlertController(from viewController: UIViewController) { - let alert: UIAlertController - let bundle = Bundle.mapboxMaps - let telemetryTitle = NSLocalizedString("TELEMETRY_TITLE", - tableName: Ornaments.localizableTableName, - bundle: bundle, - value: "Make Mapbox Maps Better", - comment: "Telemetry prompt title") - - let message: String - let participateTitle: String - let declineTitle: String - - if isMetricsEnabled { - message = NSLocalizedString("TELEMETRY_ENABLED_MSG", - tableName: Ornaments.localizableTableName, - bundle: bundle, - value: """ - You are helping to make OpenStreetMap and - Mapbox maps better by contributing anonymous usage data. - """, - comment: "Telemetry prompt message") - participateTitle = NSLocalizedString("TELEMETRY_ENABLED_ON", - tableName: Ornaments.localizableTableName, - bundle: bundle, - value: "Keep Participating", - comment: "Telemetry prompt button") - declineTitle = NSLocalizedString("TELEMETRY_ENABLED_OFF", - tableName: Ornaments.localizableTableName, - bundle: bundle, - value: "Stop Participating", - comment: "Telemetry prompt button") - } else { - message = NSLocalizedString("TELEMETRY_DISABLED_MSG", - tableName: Ornaments.localizableTableName, - bundle: bundle, value: """ - You can help make OpenStreetMap and Mapbox maps better - by contributing anonymous usage data. - """, - comment: "Telemetry prompt message") - participateTitle = NSLocalizedString("TELEMETRY_DISABLED_ON", - tableName: Ornaments.localizableTableName, - bundle: bundle, value: "Participate", - comment: "Telemetry prompt button") - declineTitle = NSLocalizedString("TELEMETRY_DISABLED_OFF", - tableName: Ornaments.localizableTableName, - bundle: bundle, value: "Don’t Participate", - comment: "Telemetry prompt button") - } - - if UIDevice.current.userInterfaceIdiom == .pad { - alert = UIAlertController(title: telemetryTitle, message: message, preferredStyle: .alert) - } else { - alert = UIAlertController(title: telemetryTitle, message: message, preferredStyle: .actionSheet) - } - - let moreTitle = NSLocalizedString("TELEMETRY_MORE", - tableName: Ornaments.localizableTableName, - bundle: bundle, value: "Tell Me More", - comment: "Telemetry prompt button") - let moreAction = UIAlertAction(title: moreTitle, style: .default) { _ in - guard let url = URL(string: Ornaments.telemetryURL) else { return } - self.delegate?.attributionDialogManager(self, didTriggerActionFor: Attribution(title: "", url: url)) - } - alert.addAction(moreAction) - - alert.addAction(UIAlertAction(title: declineTitle, style: .default) { _ in - self.isMetricsEnabled = false - }) - - alert.addAction(UIAlertAction(title: participateTitle, style: .cancel) { _ in - self.isMetricsEnabled = true - }) + func showAlertController( + from viewController: UIViewController, + title: String? = nil, + message: String? = nil, + actions: [UIAlertAction] = [] + ) { + let alert = UIAlertController( + title: title, + message: message, + preferredStyle: UIDevice.current.userInterfaceIdiom == .pad ? .alert : .actionSheet + ) + actions.forEach(alert.addAction) viewController.present(alert, animated: true) } @@ -113,74 +48,47 @@ extension AttributionDialogManager: InfoButtonOrnamentDelegate { guard inProcessOfParsingAttributions == false else { return } inProcessOfParsingAttributions = true + dataSource?.loadAttributions { [weak self] attributions in - self?.showAttributionDialog(for: attributions) - self?.inProcessOfParsingAttributions = false + guard let self else { return } + var menu = self.attributionMenu.menu(from: attributions) + if let filter = self.attributionMenu.filter { + menu.filter(filter) + } + showAttributionDialog(for: menu) + self.inProcessOfParsingAttributions = false } } - private func showAttributionDialog(for attributions: [Attribution]) { + private func showAttributionDialog(for menu: AttributionMenuSection) { guard let viewController = delegate?.viewControllerForPresenting(self) else { Log.error(forMessage: "Failed to present an attribution dialogue: no presenting view controller found.") return } - let title = Bundle.mapboxMaps.localizedString(forKey: "SDK_NAME", value: "Powered by Mapbox", table: Ornaments.localizableTableName) - - let alert: UIAlertController - - if UIDevice.current.userInterfaceIdiom == .pad { - alert = UIAlertController(title: title, message: nil, preferredStyle: .alert) - } else { - alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) - } - - let bundle = Bundle.mapboxMaps - - // Non actionable single item gets displayed as alert's message - if attributions.count == 1, let attribution = attributions.first, attribution.kind == .nonActionable { - alert.message = attribution.localizedTitle - } else { - for attribution in attributions { - let action = UIAlertAction(title: attribution.localizedTitle, style: .default) { _ in - self.delegate?.attributionDialogManager(self, didTriggerActionFor: attribution) + let actions = menu.elements.compactMap { element in + switch element { + case .item(let item): + let action = UIAlertAction(title: item.title, style: item.style.uiActionStyle) { _ in + item.action?() + } + action.isEnabled = item.action != nil + return action + case .section(let section): + if section.elements.isEmpty { + return nil + } + return UIAlertAction(title: section.actionTitle, style: .default) { _ in + self.showAttributionDialog(for: section) } - action.isEnabled = attribution.kind != .nonActionable - alert.addAction(action) } } - let telemetryTitle = NSLocalizedString("TELEMETRY_NAME", - tableName: Ornaments.localizableTableName, - bundle: bundle, - value: "Mapbox Telemetry", - comment: "Action in attribution sheet") - let telemetryAction = UIAlertAction(title: telemetryTitle, style: .default) { _ in - self.showTelemetryAlertController(from: viewController) - } - - alert.addAction(telemetryAction) - - let privacyPolicyAttribution = Attribution.makePrivacyPolicyAttribution() - let privacyPolicyAction = UIAlertAction(title: privacyPolicyAttribution.title, style: .default) { _ in - self.delegate?.attributionDialogManager(self, didTriggerActionFor: privacyPolicyAttribution) - } - - alert.addAction(privacyPolicyAction) - - let cancelTitle = NSLocalizedString("ATTRIBUTION_CANCEL", - tableName: Ornaments.localizableTableName, - bundle: bundle, - value: "Cancel", - comment: "Title of button for dismissing attribution action sheet") - - alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel)) - - viewController.present(alert, animated: true, completion: nil) + showAlertController(from: viewController, title: menu.title, message: menu.subtitle, actions: actions) } } -private extension Attribution { +internal extension Attribution { var localizedTitle: String { NSLocalizedString( title, @@ -191,3 +99,83 @@ private extension Attribution { ) } } + +enum TelemetryStrings { + static let telemetryName = NSLocalizedString( + "TELEMETRY_NAME", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Mapbox Telemetry", + comment: "Action in attribution sheet" + ) + + static let telemetryTitle = NSLocalizedString( + "TELEMETRY_TITLE", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Make Mapbox Maps Better", + comment: "Telemetry prompt title" + ) + + static let telemetryEnabledMessage = NSLocalizedString( + "TELEMETRY_ENABLED_MSG", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: """ + You are helping to make OpenStreetMap and + Mapbox maps better by contributing anonymous usage data. + """, + comment: "Telemetry prompt message" + ) + + static let telemetryDisabledMessage = NSLocalizedString( + "TELEMETRY_DISABLED_MSG", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: """ + You can help make OpenStreetMap and Mapbox maps better + by contributing anonymous usage data. + """, + comment: "Telemetry prompt message" + ) + + static let telemetryEnabledOnMessage = NSLocalizedString( + "TELEMETRY_ENABLED_ON", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Keep Participating", + comment: "Telemetry prompt button" + ) + + static let telemetryEnabledOffMessage = NSLocalizedString( + "TELEMETRY_ENABLED_OFF", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Stop Participating", + comment: "Telemetry prompt button" + ) + + static let telemetryDisabledOnMessage = NSLocalizedString( + "TELEMETRY_DISABLED_ON", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Participate", + comment: "Telemetry prompt button" + ) + + static let telemetryDisabledOffMessage = NSLocalizedString( + "TELEMETRY_DISABLED_OFF", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Don’t Participate", + comment: "Telemetry prompt button" + ) + + static let telemetryMore = NSLocalizedString( + "TELEMETRY_MORE", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Tell Me More", + comment: "Telemetry prompt button" + ) +} diff --git a/Sources/MapboxMaps/SwiftUI/Deps.swift b/Sources/MapboxMaps/SwiftUI/Deps.swift index 560e15a064d1..17000d26519c 100644 --- a/Sources/MapboxMaps/SwiftUI/Deps.swift +++ b/Sources/MapboxMaps/SwiftUI/Deps.swift @@ -21,6 +21,7 @@ struct MapDependencies { var additionalSafeArea = SwiftUI.EdgeInsets() var viewportOptions = ViewportOptions(transitionsToIdleUponUserInteraction: true, usesSafeAreaInsetsAsPadding: true) var performanceStatisticsParameters: Map.PerformanceStatisticsParameters? + var attributionMenuFilter: ((AttributionMenuItem) -> Bool)? var onMapTap: ((MapContentGestureContext) -> Void)? var onMapLongPress: ((MapContentGestureContext) -> Void)? diff --git a/Sources/MapboxMaps/SwiftUI/Map.swift b/Sources/MapboxMaps/SwiftUI/Map.swift index d1e95fbd9fd8..cbace3cc635b 100644 --- a/Sources/MapboxMaps/SwiftUI/Map.swift +++ b/Sources/MapboxMaps/SwiftUI/Map.swift @@ -231,6 +231,14 @@ extension Map { @_documentation(visibility: public) @available(iOS 13.0, *) public extension Map { + + /// Filters attribution menu items + /// Restricted API. Please contact Mapbox to discuss your use case if you intend to use this property. + @_spi(Restricted) + func attributionMenuFilter(_ filter: @escaping (AttributionMenuItem) -> Bool) -> Self { + copyAssigned(self, \.mapDependencies.attributionMenuFilter, filter) + } + /// Sets camera bounds. @_documentation(visibility: public) func cameraBounds(_ cameraBounds: CameraBoundsOptions) -> Self { diff --git a/Sources/MapboxMaps/SwiftUI/MapBasicCoordinator.swift b/Sources/MapboxMaps/SwiftUI/MapBasicCoordinator.swift index 6025d7ac7fe6..a46dfbe4ad07 100644 --- a/Sources/MapboxMaps/SwiftUI/MapBasicCoordinator.swift +++ b/Sources/MapboxMaps/SwiftUI/MapBasicCoordinator.swift @@ -99,6 +99,7 @@ final class MapBasicCoordinator { cameraChangeHandlers = deps.cameraChangeHandlers mapView.gestureManager.gestureHandlers = deps.gestureHandlers + mapView.attributionMenu.filter = deps.attributionMenuFilter shortLivedSubscriptions.removeAll() diff --git a/Sources/MapboxMaps/SwiftUI/MapViewFacade.swift b/Sources/MapboxMaps/SwiftUI/MapViewFacade.swift index 42cb17701e48..58375ffdc2fa 100644 --- a/Sources/MapboxMaps/SwiftUI/MapViewFacade.swift +++ b/Sources/MapboxMaps/SwiftUI/MapViewFacade.swift @@ -17,6 +17,7 @@ struct MapViewFacade { var presentsWithTransaction: Bool @MutableRef var frameRate: Map.FrameRate + var attributionMenu: AttributionMenu var makeViewportTransition: (ViewportAnimation) -> ViewportTransition var makeViewportState: (Viewport, LayoutDirection) -> ViewportState? @@ -34,7 +35,7 @@ extension MapViewFacade { _isOpaque = MutableRef(root: mapView, keyPath: \.isOpaque) _presentsWithTransaction = MutableRef(root: mapView, keyPath: \.presentsWithTransaction) _frameRate = MutableRef(get: mapView.getFrameRate, set: mapView.set(frameRate:)) - + attributionMenu = mapView.attributionMenu makeViewportTransition = { animation in animation.makeViewportTransition(mapView) } diff --git a/Tests/MapboxMapsTests/Foundation/MapViewTests.swift b/Tests/MapboxMapsTests/Foundation/MapViewTests.swift index 0e8c7bd38f8d..5cc12f4e70a3 100644 --- a/Tests/MapboxMapsTests/Foundation/MapViewTests.swift +++ b/Tests/MapboxMapsTests/Foundation/MapViewTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable @_spi(Metrics) import MapboxMaps +@testable @_spi(Metrics) @_spi(Restricted) import MapboxMaps final class MapViewTests: XCTestCase { @@ -352,12 +352,17 @@ final class MapViewTests: XCTestCase { XCTAssertEqual(notificationCenter.addObserverStub.invocations[5].parameters.name, UIApplication.didReceiveMemoryWarningNotification) } - func testURLOpener() { - let manager = AttributionDialogManager(dataSource: MockAttributionDataSource(), delegate: MockAttributionDialogManagerDelegate()) + func testURLOpener() throws { + let attributionMenu = AttributionMenu(urlOpener: attributionURLOpener, feedbackURLRef: Ref { nil }) let url = URL(string: "http://example.com")! let attribution = Attribution(title: .randomASCII(withLength: 10), url: url) - mapView.attributionDialogManager(manager, didTriggerActionFor: attribution) + let menu = attributionMenu.menu(from: [attribution]) + guard let item = menu.elements.first, case let AttributionMenuElement.item(menuItem) = item else { + XCTFail("Failed to unwrap AttributionMenuElement.item") + return + } + menuItem.action?() XCTAssertEqual(attributionURLOpener.openAttributionURLStub.invocations.count, 1) XCTAssertEqual(attributionURLOpener.openAttributionURLStub.invocations.first?.parameters, url) diff --git a/Tests/MapboxMapsTests/Ornaments/InfoButton/InfoButtonOrnamentTests.swift b/Tests/MapboxMapsTests/Ornaments/InfoButton/InfoButtonOrnamentTests.swift index 911fbc57f5d8..24d97cb27ec3 100644 --- a/Tests/MapboxMapsTests/Ornaments/InfoButton/InfoButtonOrnamentTests.swift +++ b/Tests/MapboxMapsTests/Ornaments/InfoButton/InfoButtonOrnamentTests.swift @@ -1,16 +1,32 @@ import XCTest -@testable import MapboxMaps +@_spi(Restricted) @testable import MapboxMaps class InfoButtonOrnamentTests: XCTestCase { var parentViewController: MockParentViewController! var attributionDialogManager: AttributionDialogManager! + var urlOpener: AttributionURLOpener! + var attributionMenu: AttributionMenu! var tapCompletion: (() -> Void)? override func setUp() { super.setUp() parentViewController = MockParentViewController() - attributionDialogManager = AttributionDialogManager(dataSource: self, delegate: self) + urlOpener = MockAttributionURLOpener() + attributionMenu = AttributionMenu(urlOpener: urlOpener, feedbackURLRef: Ref { nil }) + attributionDialogManager = AttributionDialogManager( + dataSource: self, + delegate: self, + attributionMenu: attributionMenu + ) + } + + override func tearDown() { + urlOpener = nil + attributionMenu = nil + parentViewController = nil + attributionDialogManager = nil + super.tearDown() } func testInfoButtonTapped() throws { @@ -52,13 +68,13 @@ class InfoButtonOrnamentTests: XCTestCase { let participatingTitle = NSLocalizedString("Keep Participating", comment: "Telemetry prompt button") XCTAssertEqual(participatingTitle, telemetryAlert.actions[2].title, "The third action should be a 'Keep Participating' button.") - XCTAssertTrue(infoButton.isMetricsEnabled) + XCTAssertTrue(attributionMenu.isMetricsEnabled) let stopParticipatingTitle = NSLocalizedString("Stop Participating", comment: "Telemetry prompt button") XCTAssertEqual(stopParticipatingTitle, telemetryAlert.actions[1].title, "The second action should be a 'Stop Participating' button.") telemetryAlert.tapButton(atIndex: 1) - XCTAssertFalse(infoButton.isMetricsEnabled, "Metrics should not be enabled after selecting 'Stop participating'.") + XCTAssertFalse(attributionMenu.isMetricsEnabled, "Metrics should not be enabled after selecting 'Stop participating'.") infoButton.infoTapped() infoAlert = try XCTUnwrap(parentViewController.currentAlert, "The info alert controller could not be found.") @@ -68,7 +84,7 @@ class InfoButtonOrnamentTests: XCTestCase { let dontParticipateTitle = NSLocalizedString("Don’t Participate", comment: "Telemetry prompt button") XCTAssertEqual(dontParticipateTitle, telemetryAlert.actions[1].title, "The second action should be a 'Don't Participate' button.") telemetryAlert.tapButton(atIndex: 1) - XCTAssertFalse(infoButton.isMetricsEnabled, "Metrics should not be enabled after selecting 'Don't Participate'.") + XCTAssertFalse(attributionMenu.isMetricsEnabled, "Metrics should not be enabled after selecting 'Don't Participate'.") } func testTelemetryOptIn() throws { @@ -90,13 +106,13 @@ class InfoButtonOrnamentTests: XCTestCase { let participatingTitle = NSLocalizedString("Participate", comment: "Telemetry prompt button") XCTAssertEqual(participatingTitle, telemetryAlert.actions[2].title, "The third action should be a 'Participate' button.") - XCTAssertFalse(infoButton.isMetricsEnabled) + XCTAssertFalse(attributionMenu.isMetricsEnabled) let dontParticipateTitle = NSLocalizedString("Don’t Participate", comment: "Telemetry prompt button") XCTAssertEqual(dontParticipateTitle, telemetryAlert.actions[1].title, "The second action should be a 'Don't Participate' button.") telemetryAlert.tapButton(atIndex: 2) - XCTAssertTrue(infoButton.isMetricsEnabled, "Metrics should be enabled after selecting 'Participate'.") + XCTAssertTrue(attributionMenu.isMetricsEnabled, "Metrics should be enabled after selecting 'Participate'.") infoButton.infoTapped() infoAlert = try XCTUnwrap(parentViewController.currentAlert, "The info alert controller could not be found.") @@ -105,7 +121,7 @@ class InfoButtonOrnamentTests: XCTestCase { let keepParticipatingTitle = NSLocalizedString("Keep Participating", comment: "Telemetry prompt button") XCTAssertEqual(keepParticipatingTitle, telemetryAlert.actions[2].title, "The third action should be a 'Keep Participating' button.") telemetryAlert.tapButton(atIndex: 2) - XCTAssertTrue(infoButton.isMetricsEnabled, "Metrics should be enabled after selecting 'Keep Participating'.") + XCTAssertTrue(attributionMenu.isMetricsEnabled, "Metrics should be enabled after selecting 'Keep Participating'.") } } diff --git a/Tests/MapboxMapsTests/Style/AttributionDialogTests.swift b/Tests/MapboxMapsTests/Style/AttributionDialogTests.swift index 103a29beaf0f..2dd85c095591 100644 --- a/Tests/MapboxMapsTests/Style/AttributionDialogTests.swift +++ b/Tests/MapboxMapsTests/Style/AttributionDialogTests.swift @@ -1,134 +1,46 @@ import XCTest -@testable import MapboxMaps +@_spi(Restricted) @testable import MapboxMaps import Foundation import UIKit class AttributionDialogTests: XCTestCase { - + var parentViewController: MockParentViewController! var attributionDialogManager: AttributionDialogManager! + var attributionMenu: AttributionMenu! + var urlOpener: AttributionURLOpener! var mockDataSource: MockAttributionDataSource! - var mockDelegate: MockAttributionDialogManagerDelegate! override func setUp() { super.setUp() + + parentViewController = MockParentViewController() mockDataSource = MockAttributionDataSource() - mockDelegate = MockAttributionDialogManagerDelegate() - attributionDialogManager = AttributionDialogManager(dataSource: mockDataSource, delegate: mockDelegate) + urlOpener = MockAttributionURLOpener() + attributionMenu = AttributionMenu( + urlOpener: urlOpener, + feedbackURLRef: Ref { nil } + ) + attributionDialogManager = AttributionDialogManager( + dataSource: mockDataSource, + delegate: self, + attributionMenu: attributionMenu + ) } override func tearDown() { super.tearDown() + parentViewController = nil + attributionMenu = nil + urlOpener = nil attributionDialogManager = nil mockDataSource = nil - mockDelegate = nil - } - - func testShowTelemetryDialogMetricsEnabled() throws { - let viewController = UIViewController() - let bundle = Bundle.mapboxMaps - let window = UIWindow() - window.rootViewController = viewController - window.makeKeyAndVisible() - attributionDialogManager.isMetricsEnabled = true - - attributionDialogManager.showTelemetryAlertController(from: viewController) - - let alert = try XCTUnwrap(viewController.presentedViewController as? UIAlertController) - let telemetryTitle = NSLocalizedString("TELEMETRY_TITLE", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") - XCTAssertEqual(alert.title, telemetryTitle) - - let message = NSLocalizedString("TELEMETRY_ENABLED_MSG", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") - XCTAssertEqual(alert.message, message) - - guard alert.actions.count == 3 else { - XCTFail("Telemetry alert should have 3 actions") - return - } - - let moreTitle = NSLocalizedString("TELEMETRY_MORE", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") - XCTAssertEqual(alert.actions[0].title, moreTitle) - - let declineTitle = NSLocalizedString("TELEMETRY_ENABLED_OFF", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") - XCTAssertEqual(alert.actions[1].title, declineTitle) - - let participateTitle = NSLocalizedString("TELEMETRY_ENABLED_ON", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") - XCTAssertEqual(alert.actions[2].title, participateTitle) - } - - func testShowTelemetryDialogMetricsDisabled() throws { - let viewController = UIViewController() - let bundle = Bundle.mapboxMaps - let window = UIWindow() - window.rootViewController = viewController - window.makeKeyAndVisible() - attributionDialogManager.isMetricsEnabled = false - - attributionDialogManager.showTelemetryAlertController(from: viewController) - - let alert = try XCTUnwrap(viewController.presentedViewController as? UIAlertController) - let telemetryTitle = NSLocalizedString("TELEMETRY_TITLE", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") - XCTAssertEqual(alert.title, telemetryTitle) - - let message = NSLocalizedString("TELEMETRY_DISABLED_MSG", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") - XCTAssertEqual(alert.message, message) - - guard alert.actions.count == 3 else { - XCTFail("Telemetry alert should have 3 actions") - return - } - - let moreTitle = NSLocalizedString("TELEMETRY_MORE", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") - XCTAssertEqual(alert.actions[0].title, moreTitle) - - let declineTitle = NSLocalizedString("TELEMETRY_DISABLED_OFF", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") - XCTAssertEqual(alert.actions[1].title, declineTitle) - - let participateTitle = NSLocalizedString("TELEMETRY_DISABLED_ON", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") - XCTAssertEqual(alert.actions[2].title, participateTitle) } func testShowAttributionDialogNoAttributions() throws { - let viewController = UIViewController() - let bundle = Bundle.mapboxMaps - let window = UIWindow() - window.rootViewController = viewController - window.makeKeyAndVisible() - mockDelegate.viewControllerForPresentingStub.defaultReturnValue = viewController - attributionDialogManager.didTap(InfoButtonOrnament()) - let alert = try XCTUnwrap(viewController.presentedViewController as? UIAlertController) + let alert = try XCTUnwrap(parentViewController.currentAlert, "The info alert controller could not be found.") let alertTitle = NSLocalizedString("SDK_NAME", tableName: nil, value: "Powered by Mapbox Maps", @@ -143,7 +55,7 @@ class AttributionDialogTests: XCTestCase { let telemetryTitle = NSLocalizedString("TELEMETRY_NAME", tableName: Ornaments.localizableTableName, - bundle: bundle, + bundle: .mapboxMaps, comment: "") XCTAssertEqual(alert.actions[0].title, telemetryTitle) @@ -151,26 +63,20 @@ class AttributionDialogTests: XCTestCase { let cancelTitle = NSLocalizedString("CANCEL", tableName: Ornaments.localizableTableName, - bundle: bundle, + bundle: .mapboxMaps, value: "Cancel", comment: "") XCTAssertEqual(alert.actions[2].title, cancelTitle) } func testShowAttributionDialogSingleNonActionableAttribution() throws { - let viewController = UIViewController() - let window = UIWindow() let attribution = Attribution(title: String.randomASCII(withLength: 10), url: nil) - window.rootViewController = viewController - window.makeKeyAndVisible() - mockDataSource.loadAttributionsStub.defaultSideEffect = { invocation in - invocation.parameters([attribution]) - } - mockDelegate.viewControllerForPresentingStub.defaultReturnValue = viewController + + mockDataSource.attributions = [attribution] attributionDialogManager.didTap(InfoButtonOrnament()) - let alert = try XCTUnwrap(viewController.presentedViewController as? UIAlertController) + let alert = try XCTUnwrap(parentViewController.currentAlert, "The info alert controller could not be found.") let alertTitle = NSLocalizedString("SDK_NAME", tableName: nil, value: "Powered by Mapbox Maps", @@ -188,21 +94,14 @@ class AttributionDialogTests: XCTestCase { } func testShowAttributionDialogTwoAttributions() throws { - let viewController = UIViewController() - let window = UIWindow() let attribution0 = Attribution(title: String.randomASCII(withLength: 10), url: nil) let attribution1 = Attribution(title: String.randomASCII(withLength: 10), url: URL(string: "http://example.com")!) - window.rootViewController = viewController - window.makeKeyAndVisible() - mockDataSource.loadAttributionsStub.defaultSideEffect = { invocation in - invocation.parameters([attribution0, attribution1]) - } - mockDelegate.viewControllerForPresentingStub.defaultReturnValue = viewController + mockDataSource.attributions = [attribution0, attribution1] attributionDialogManager.didTap(InfoButtonOrnament()) - let alert = try XCTUnwrap(viewController.presentedViewController as? UIAlertController) + let alert = try XCTUnwrap(parentViewController.currentAlert, "The info alert controller could not be found.") let alertTitle = NSLocalizedString("SDK_NAME", tableName: nil, value: "Powered by Mapbox Maps", @@ -220,4 +119,61 @@ class AttributionDialogTests: XCTestCase { XCTAssertEqual(alert.actions[0].title, attribution0.title) XCTAssertEqual(alert.actions[1].title, attribution1.title) } + + func testAttributionFilteringID() throws { + let attribution0 = Attribution(title: String.randomASCII(withLength: 10), url: nil) + let attribution1 = Attribution(title: String.randomASCII(withLength: 10), url: URL(string: "http://example.com")!) + + mockDataSource.attributions = [attribution0, attribution1] + + attributionMenu.filter = { $0.id == .copyright || $0.id == .privacyPolicy } + + attributionDialogManager.didTap(.init()) + + let alert = try XCTUnwrap(parentViewController.currentAlert, "The info alert controller could not be found.") + + // Single, non-actionable attributions should be displayed as alert's actions along the telemetry and cancel actions + guard alert.actions.count == 3 else { + XCTFail("Telemetry alert should have 3 actions") + return + } + + let privacyPolicyTitle = NSLocalizedString("ATTRIBUTION_PRIVACY_POLICY", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Mapbox Privacy Policy", + comment: "Privacy policy action in attribution sheet") + + XCTAssertEqual(alert.actions[0].title, attribution0.title) + XCTAssertEqual(alert.actions[1].title, attribution1.title) + XCTAssertEqual(alert.actions[2].title, privacyPolicyTitle) + } + + func testAttributionFilteringCategory() throws { + let attribution0 = Attribution(title: String.randomASCII(withLength: 10), url: nil) + let attribution1 = Attribution(title: String.randomASCII(withLength: 10), url: URL(string: "http://example.com")!) + + mockDataSource.attributions = [attribution0, attribution1] + + attributionMenu.filter = { $0.category == .telemetry || $0.category == .geofencing } + + attributionDialogManager.didTap(.init()) + + let alert = try XCTUnwrap(parentViewController.currentAlert, "The info alert controller could not be found.") + + // Single, non-actionable attributions should be displayed as alert's actions along the telemetry and cancel actions + guard alert.actions.count == 1 else { + XCTFail("Telemetry alert should have 1 action") + return + } + + XCTAssertEqual(alert.actions[0].title, TelemetryStrings.telemetryName) + } + +} + +extension AttributionDialogTests: AttributionDialogManagerDelegate { + func viewControllerForPresenting(_ attributionDialogManager: AttributionDialogManager) -> UIViewController? { + return parentViewController + } } diff --git a/Tests/MapboxMapsTests/Style/AttributionTests.swift b/Tests/MapboxMapsTests/Style/AttributionTests.swift index 493232816065..92eaacf5e310 100644 --- a/Tests/MapboxMapsTests/Style/AttributionTests.swift +++ b/Tests/MapboxMapsTests/Style/AttributionTests.swift @@ -179,7 +179,7 @@ class AttributionTests: XCTestCase { let expectedURL = try XCTUnwrap(URL(string: "https://apps.mapbox.com/feedback/?referrer=\(Bundle.main.bundleIdentifier!)&owner=mapbox&id=standard&access_token=test-token&map_sdk_version=\(metadata.version)#/2.00000/1.00000/3.00/4.0/5")) let mapView = MapView(frame: .zero, mapInitOptions: mapInitOptions) - let url = mapView.mapboxFeedbackURL(accessToken: "test-token") + let url = mapView.mapboxMap.mapboxFeedbackURL(accessToken: "test-token") XCTAssertEqual(expectedURL, url) } diff --git a/Tests/MapboxMapsTests/Style/Mocks/MockAttributionDataSource.swift b/Tests/MapboxMapsTests/Style/Mocks/MockAttributionDataSource.swift index 0c9d88deb6e9..9f265e8ebbc1 100644 --- a/Tests/MapboxMapsTests/Style/Mocks/MockAttributionDataSource.swift +++ b/Tests/MapboxMapsTests/Style/Mocks/MockAttributionDataSource.swift @@ -3,9 +3,9 @@ import XCTest import Foundation final class MockAttributionDataSource: AttributionDataSource { + var attributions: [MapboxMaps.Attribution] = [] let loadAttributionsStub = Stub<(([Attribution]) -> Void), Void>() func loadAttributions(completion: @escaping ([MapboxMaps.Attribution]) -> Void) { - loadAttributionsStub.call(with: completion) - completion([]) + completion(attributions) } } diff --git a/Tests/MapboxMapsTests/Style/Mocks/MockAttributionDialogManagerDelegate.swift b/Tests/MapboxMapsTests/Style/Mocks/MockAttributionDialogManagerDelegate.swift index 2dd8a898dda5..0d72cdb8c6df 100644 --- a/Tests/MapboxMapsTests/Style/Mocks/MockAttributionDialogManagerDelegate.swift +++ b/Tests/MapboxMapsTests/Style/Mocks/MockAttributionDialogManagerDelegate.swift @@ -8,16 +8,4 @@ final class MockAttributionDialogManagerDelegate: AttributionDialogManagerDelega func viewControllerForPresenting(_ attributionDialogManager: AttributionDialogManager) -> UIViewController? { viewControllerForPresentingStub.call(with: attributionDialogManager) } - - struct TriggerActionForParameters { - let attributionDialogManager: AttributionDialogManager - let attribution: Attribution - } - - let attributionDialogManagerStub = Stub() - func attributionDialogManager(_ attributionDialogManager: AttributionDialogManager, didTriggerActionFor attribution: Attribution) { - attributionDialogManagerStub.call(with: - TriggerActionForParameters(attributionDialogManager: attributionDialogManager, - attribution: attribution)) - } } diff --git a/Tests/MapboxMapsTests/SwiftUI/Mocks/MockMapView.swift b/Tests/MapboxMapsTests/SwiftUI/Mocks/MockMapView.swift index a93ff24169c0..9b33a03a04ba 100644 --- a/Tests/MapboxMapsTests/SwiftUI/Mocks/MockMapView.swift +++ b/Tests/MapboxMapsTests/SwiftUI/Mocks/MockMapView.swift @@ -1,6 +1,6 @@ import UIKit import SwiftUI -@_spi(Experimental) @testable import MapboxMaps +@_spi(Experimental) @_spi(Restricted) @testable import MapboxMaps @available(iOS 13.0, *) struct MockMapView { @@ -9,7 +9,7 @@ struct MockMapView { var gestures = MockGestureManager() var viewportManager = MockViewportManager() var ornaments = MockOrnamentsManager() - + var attributionMenu = AttributionMenu(urlOpener: MockAttributionURLOpener(), feedbackURLRef: Ref { nil }) var makeViewportTransitionStub = Stub(defaultReturnValue: MockViewportTransition()) struct MakeViewportParameters { var viewport: Viewport @@ -29,6 +29,7 @@ struct MockMapView { isOpaque: false, presentsWithTransaction: false, frameRate: Map.FrameRate(), + attributionMenu: attributionMenu, makeViewportTransition: makeViewportTransitionStub.call(with:), makeViewportState: { [makeViewportStateStub] viewport, layoutDirection in makeViewportStateStub.call(with: MakeViewportParameters(viewport: viewport, layoutDirection: layoutDirection))