From ad169e99e01890af3660c183640895c4736df2b9 Mon Sep 17 00:00:00 2001 From: Varun Santhanam Date: Sun, 2 Jul 2023 21:35:02 -0700 Subject: [PATCH] Move configuration and styles to swiftui environment --- Package.swift | 2 +- Sources/SafariView/Environment.swift | 58 ++ Sources/SafariView/Modifiers.swift | 84 ++ ...iewExtensions.swift => Presentation.swift} | 17 +- Sources/SafariView/SafariView.swift | 794 ++++++------------ 5 files changed, 435 insertions(+), 520 deletions(-) create mode 100644 Sources/SafariView/Environment.swift create mode 100644 Sources/SafariView/Modifiers.swift rename Sources/SafariView/{ViewExtensions.swift => Presentation.swift} (96%) diff --git a/Package.swift b/Package.swift index e4f4607a1..32afec833 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "SafariView", platforms: [ - .iOS(.v15) + .iOS(.v16) ], products: [ .library( diff --git a/Sources/SafariView/Environment.swift b/Sources/SafariView/Environment.swift new file mode 100644 index 000000000..1e4c461b6 --- /dev/null +++ b/Sources/SafariView/Environment.swift @@ -0,0 +1,58 @@ +// SafariView +// Environment.swift +// +// MIT License +// +// Copyright (c) 2021 Varun Santhanam +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI + +public extension EnvironmentValues { + + var safariViewConfiguration: SafariView.Configuration { + get { self[SafariViewConfigurationEnvironmentKey.self] } + set { self[SafariViewConfigurationEnvironmentKey.self] = newValue } + } + + var safariViewStyle: SafariView.Style { + get { self[SafariViewStyleEvironmentKey.self] } + set { self[SafariViewStyleEvironmentKey.self] = newValue } + } + +} + +struct SafariViewConfigurationEnvironmentKey: EnvironmentKey { + + // MARK: - EnvironmentKey + + typealias Value = SafariView.Configuration + + static var defaultValue: Value { .init() } + +} + +private struct SafariViewStyleEvironmentKey: EnvironmentKey { + + typealias Value = SafariView.Style + + static var defaultValue: Value { .init() } + +} diff --git a/Sources/SafariView/Modifiers.swift b/Sources/SafariView/Modifiers.swift new file mode 100644 index 000000000..282b8ff94 --- /dev/null +++ b/Sources/SafariView/Modifiers.swift @@ -0,0 +1,84 @@ +// SafariView +// Modifiers.swift +// +// MIT License +// +// Copyright (c) 2021 Varun Santhanam +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI + +public extension View { + + func safariConfiguration(_ configuration: SafariView.Configuration) -> some View { + let modifier = SafariViewConfigurationModifier(configuration: configuration) + return ModifiedContent(content: self, modifier: modifier) + } + + func safariStyle(_ style: SafariView.Style) -> some View { + let modifier = SafariViewStyleModifier(style: style) + return ModifiedContent(content: self, modifier: modifier) + } + +} + +private struct SafariViewConfigurationModifier: ViewModifier { + + // MARK: - Initializers + + init(configuration: SafariView.Configuration) { + self.configuration = configuration + } + + // MARK: - ViewModifier + + @ViewBuilder + func body(content: Content) -> some View { + content + .environment(\.safariViewConfiguration, configuration) + } + + // MARK: - Private + + private let configuration: SafariView.Configuration + +} + +private struct SafariViewStyleModifier: ViewModifier { + + // MARK: - Initializers + + init(style: SafariView.Style) { + self.style = style + } + + // MARK: - ViewModifier + + @ViewBuilder + func body(content: Content) -> some View { + content + .environment(\.safariViewStyle, style) + } + + // MARK: - Private + + private let style: SafariView.Style + +} diff --git a/Sources/SafariView/ViewExtensions.swift b/Sources/SafariView/Presentation.swift similarity index 96% rename from Sources/SafariView/ViewExtensions.swift rename to Sources/SafariView/Presentation.swift index 62cf7225e..508f2e6a8 100644 --- a/Sources/SafariView/ViewExtensions.swift +++ b/Sources/SafariView/Presentation.swift @@ -1,5 +1,5 @@ // SafariView -// ViewExtensions.swift +// Presentation.swift // // MIT License // @@ -134,9 +134,11 @@ public extension View { onDismiss: (() -> Void)? = nil, @ViewBuilder safariView: @escaping (Item) -> SafariView ) -> some View where Item: Identifiable { - let modifier = SafariView.ItemModitifer(item: item, - build: safariView, - onDismiss: onDismiss ?? {}) + let modifier = SafariView.IdentifiableItemModitifer( + item: item, + build: safariView, + onDismiss: onDismiss ?? {} + ) return ModifiedContent(content: self, modifier: modifier) } @@ -199,7 +201,12 @@ public extension View { onDismiss: (() -> Void)? = nil, @ViewBuilder safariView: @escaping (Item) -> SafariView ) -> some View { - let modifier = SafariView.GenericItemModifier(item: item, id: id, onDismiss: onDismiss, safariView: safariView) + let modifier = SafariView.ItemModifier( + item: item, + id: id, + onDismiss: onDismiss, + safariView: safariView + ) return ModifiedContent(content: self, modifier: modifier) } diff --git a/Sources/SafariView/SafariView.swift b/Sources/SafariView/SafariView.swift index 27f3035c7..2bd34ae7e 100644 --- a/Sources/SafariView/SafariView.swift +++ b/Sources/SafariView/SafariView.swift @@ -57,395 +57,133 @@ import UIKit /// .safari(isPresented: $isShowingSafari, /// onDismiss: didDismiss) { /// SafariView(url: licenseAgreementURL) -/// .preferredBarTintColor(.red) -/// .accentColor(.white) -/// .dismissButtonStyle(.done) -/// .onInitialLoad { successful in -/// if !successful { -/// // Handle initial load failure -/// } -/// } -/// } -/// } -/// -/// func didDismiss() { -/// // Handle the dismissing action. +/// } /// } /// /// } /// ``` /// /// You can also use sheet presentation, or any other presentation mechanism of your choice. -/// -/// ## Topics -/// -/// ### Initializers -/// -/// - ``init(url:)`` -/// - ``init(url:configuration:)`` -/// -/// ### Appearance Modifiers -/// -/// - ``accentColor(_:)`` -/// - ``preferredBarTintColor(_:)`` -/// - ``preferredControlTintColor(_:)`` -/// - ``dismissButtonStyle(_:)`` -/// - ``enableBarCollapsing(_:)`` -/// -/// ### Behavior Modifiers -/// -/// - ``activityButton(_:)`` -/// - ``entersReaderIfAvailable(_:)`` -/// - ``eventAttribution(_:)`` -/// -/// ### Lifecycle Modifiers -/// -/// - ``onInitialLoad(_:)`` -/// - ``onInitialRedirect(_:)`` -/// - ``onOpenInBrowser(_:)`` -/// -/// ### Activity Item Modifiers -/// -/// - ``activityItems(_:)-3mpe3`` -/// - ``activityItems(_:)-2yuvl`` -/// - ``excludingActivityItems(_:)-5m53s`` -/// - ``excludingActivityItems(_:)-6whri`` public struct SafariView: View { // MARK: - Initializers /// Create a `SafariView` /// - Parameter url: The URL to load in the view - public init(url: URL) { - self.init(url: url, - configuration: .init()) - } - - /// Create a `SafariView` - /// - /// - Parameters: - /// - url: The URL to load in the view - /// - configuration: The ``Configuration`` to use - public init(url: URL, configuration: Configuration) { + public init( + url: URL, + onInitialLoad: ((_ didLoadSuccessfully: Bool) -> Void)? = nil, + onInitialRedirect: ((_ url: URL) -> Void)? = nil, + onOpenInBrowser: (() -> Void)? = nil + ) { self.url = url - self.configuration = configuration + self.onInitialLoad = onInitialLoad + self.onInitialRedirect = onInitialRedirect + self.onOpenInBrowser = onOpenInBrowser } // MARK: - API - /// A convenience typealias for [`SFSafariViewController.Configuration`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller/configuration) - public typealias Configuration = SFSafariViewController.Configuration - - /// A convenience typealias for [`SFSafariViewController.DismissButtonStyle`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller/dismissbuttonstyle) - public typealias DismissButtonStyle = SFSafariViewController.DismissButtonStyle - - /// A convenience typealias for [`SFSafariViewController.ActivityButton`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller/activitybutton) - public typealias ActivityButton = SFSafariViewController.ActivityButton - - /// A convenience typealias for [`SFSafariViewController.PrewarmingToken`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller/prewarmingtoken) - public typealias PrewarmingToken = SFSafariViewController.PrewarmingToken - - /// Apply an accent color to the view - /// - /// Use this modifier to set the view's accent color - /// - /// ```swift - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .accentColor(.blue) - /// ``` - /// - /// - Parameter color: The color to use - /// - Returns: The safari view - public func accentColor(_ accentColor: Color?) -> Self { - preferredControlTintColor(accentColor) - } - - /// Apply an bar tint color to the view - /// - /// This modifier is equivelent to `SFSafariViewController`'s `.preferredBarTintColor` property - /// - /// ```swift - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .preferredBarTintColor(.blue) - /// ``` - /// - /// - Parameter color: The color to use - /// - Returns: The safari view - public func preferredBarTintColor(_ color: Color?) -> Self { - var copy = self - copy.barTintColor = color - return copy - } - - /// Apply a control tint color to the view - /// - /// This modifier is equivelent to `SFSafariViewController`'s `.preferredControlTintColor` property - /// - /// ```swift - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .preferredControlTintColor(.blue) - /// ``` - /// - /// - Parameter color: The color to use - /// - Returns: The safari view - public func preferredControlTintColor(_ color: Color?) -> Self { - var copy = self - copy.controlTintColor = color - return copy - } - - /// Set the safari view's dismiss button style - /// - /// Use this modifier to set the view's dismiss button style. - /// - /// ```swift - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .dismissButtonStyle(.cancel) - /// ``` - /// - /// - Parameter style: The dismiss button style of the safari view - /// - Returns: The safari view - public func dismissButtonStyle(_ style: DismissButtonStyle) -> Self { - var copy = self - copy.dismissButtonStyle = style - return copy - } - - /// Set the safari view's automatic reader mode setting - /// - /// Set the value to `true` if Reader mode should be entered automatically when it is available for the webpage; otherwise, `false`. The default value is `false`. - /// - /// ```swift - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .entersReaderIfAvailable(true) - /// ``` - /// - /// This value that specifies whether Safari should enter Reader mode, if it is available. - /// - Parameter entersReaderIfAvailable: `true` to automatically enter reader mode when available, otherwise `false` - /// - Returns: The safari view - public func entersReaderIfAvailable(_ entersReaderIfAvailable: Bool) -> Self { - configuration.entersReaderIfAvailable = entersReaderIfAvailable - return self - } - - /// Set the safari view's bar collapsing setting - /// - /// Use this modifier to set the bar collapsing behavior of the view - /// - /// ```swift - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .enableBarCollapsing(true) - /// ``` - /// - /// - Parameter barCollapsingEnabled: `true` to enable bar collapsing, otherwise `false` - /// - Returns: The safari view - public func enableBarCollapsing(_ barCollapsingEnabled: Bool) -> Self { - configuration.barCollapsingEnabled = barCollapsingEnabled - return self - } + /// A struct used to configure the behavior of a`SafariView` + /// + /// A `SafariView`'s configuration isherited from its parent using SwiftUI environment values. + /// You can change the value using the `.safariConfiguration(_ configuration: SafariView.Configuration)` view modifier. + public struct Configuration { + + public init( + entersReaderIfAvailable: Bool = false, + barCollapsingEnabled: Bool = false, + activityButton: ActivityButton? = nil, + eventAttribution: UIEventAttribution? = nil, + includedActivities: ((_ url: URL, _ pageTitle: String?) -> [UIActivity])? = nil, + excludedActivityTypes: ((_ url: URL, _ pageTitle: String?) -> [UIActivity.ActivityType])? = nil + ) { + self.barCollapsingEnabled = barCollapsingEnabled + self.entersReaderIfAvailable = entersReaderIfAvailable + self.activityButton = activityButton + self.eventAttribution = eventAttribution + self.includedActivities = includedActivities + self.excludedActivityTypes = excludedActivityTypes + } - /// Set the safari view's activity button - /// - /// Use this modifier to set the view's activity button. See ``ActivityButton`` for more information, - /// - /// ```swift - /// let activityButton = SafarView.ActivityButton( ... ) - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .activityButton(activityButton) - /// ``` - /// - /// - Parameter activityButton: The activity button - /// - Returns: The safari view - public func activityButton(_ activityButton: ActivityButton?) -> Self { - configuration.activityButton = activityButton - return self + /// Set the safari view's automatic reader mode setting + /// + /// Set the value to `true` if Reader mode should be entered automatically when it is available for the webpage; otherwise, `false`. The default value is `false`. + public var entersReaderIfAvailable: Bool + + /// Set the safari view's bar collapsing setting + /// + /// Use this property to set the bar collapsing behavior of the view + public var barCollapsingEnabled: Bool + + /// Set the safari view's activity button + /// + /// Use this property to set the view's activity button. See ``SafariView/ActivityButton`` for more information + public var activityButton: ActivityButton? + + /// Set the safari view's event attribution + /// + /// Use this property to set the view's event attribution. + /// For more information about preparing event attribution data, see [`UIEventAttribution`](https://developer.apple.com/documentation/uikit/uieventattribution). + public var eventAttribution: UIEventAttribution? + + /// Add [`UIActivity`](https://developer.apple.com/documentation/uikit/uiactivity) items to the Safari View + /// + /// This closure should return any `UIActivity`(s) to show in the share sheet of the safari view, based on the currently visible URL and page title. + public var includedActivities: ((_ url: URL, _ pageTitle: String?) -> [UIActivity])? + + /// Exclude [`UIActivity.ActivityType`](https://developer.apple.com/documentation/uikit/uiactivity/activitytype)s from the Safari View + /// + /// This closure should return an `UIActivity.ActivityType`(s) to exclude from the share sheet of the safari view, based on the currently visible URL and page title. + public var excludedActivityTypes: ((_ url: URL, _ title: String?) -> [UIActivity.ActivityType])? } - /// Set the safari view's event attribution - /// - /// Use this modifier to set the view's event attribution. - /// For more information about preparing event attribution data, see [`UIEventAttribution`](https://developer.apple.com/documentation/uikit/uieventattribution). - /// - /// ```swift - /// let attribution = UIEventAttribution( ... ) - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .eventAttribution(attribution) - /// ``` - /// - /// - Parameter eventAttribution: The event attribution to use - /// - Returns: The safari view - @available(iOS 15.2, *) - public func eventAttribution(_ eventAttribution: UIEventAttribution?) -> Self { - configuration.eventAttribution = eventAttribution - return self - } + /// A struct used to configure the appearance of a `SafariView` + /// + /// A `SafariView`'s style is inherited from its parent using SwiftUI environment values + /// You can cange the value using the `.safariStyle(_ style: SafariView.Style)` view modifier. + public struct Style { + + /// Create a style for a `SafariView` + /// - Parameters: + /// - preferredControlTintColor: The preferred control tint color. + /// - preferredBarTintColor: The preferred bar tint color. + /// - dismissButtonStyle: The preferred dismiss button style. + public init( + preferredControlTintColor: Color = .accentColor, + preferredBarTintColor: Color? = nil, + dismissButtonStyle: DismissButtonStyle = .close + ) { + self.preferredControlTintColor = preferredControlTintColor + self.preferredBarTintColor = preferredBarTintColor + self.dismissButtonStyle = dismissButtonStyle + } - /// Set a function to call when the page first loads - /// - /// This closure is invoked when `SafariView` completes the loading of the URL that you pass to its initializer. The closure is not invoked for any subsequent page loads in the same `SafariView` instance. - /// - /// This method behaves similarly to [`SFSafariViewControllerDelegate`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontrollerdelegate) method [`safariViewController(_:didCompleteInitialLoad:)`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontrollerdelegate/1621215-safariviewcontroller) - /// - /// ```swift - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .onInitialLoad { didLoadSuccessfully in - /// if didLoadSuccessfully { - /// print("Success!") - /// } else { - /// print("Failre!") - /// } - /// } - /// ``` - /// - /// - Parameter onInitialLoad: The function to execute when page first loads - /// - Returns: The safari view - public func onInitialLoad(_ onInitialLoad: ((_ didLoadSuccessfully: Bool) -> Void)?) -> Self { - var copy = self - copy.onInitialLoad = onInitialLoad ?? { _ in } - return copy - } + /// Apply a control tint color to the view + /// + /// This property is equivelent to `SFSafariViewController`'s `.preferredControlTintColor` property + public var preferredControlTintColor: Color - /// Set a function to call if the first page load causes a redirection - /// - /// This closure is invoked when `SafariView`'s initial URL results in a redirection. The closure is not invoked for any subsequent page loads in the same `SafariView` instance. - /// - /// This method behaves similarly to [`SFSafariViewControllerDelegate`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontrollerdelegate) method [`safariViewController(_:initialLoadDidRedirectTo:)`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontrollerdelegate/2923545-safariviewcontroller) - /// - /// ```swift - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .onInitialRedirect { newURL in - /// print("Redirected to URL \(newURL.description)") - /// } - /// ``` - /// - /// - Parameter onInitialRedirect: The function to execute when the initial page load causes a redirection - /// - Returns: The safari view - public func onInitialRedirect(_ onInitialRedirect: ((_ url: URL) -> Void)?) -> Self { - var copy = self - copy.onInitialRedirect = onInitialRedirect ?? { _ in } - return copy - } + /// Apply an bar tint color to the view + /// + /// This property is equivelent to `SFSafariViewController`'s `.preferredBarTintColor` property + public var preferredBarTintColor: Color? - /// Set a function to call if the user open's a loaded page in `Safari.app` - /// - /// This method behaves similarly to [`SFSafariViewControllerDelegate`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontrollerdelegate) method [`safariViewControllerWillOpenInBrowser(_:)`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontrollerdelegate/3650426-safariviewcontrollerwillopeninbr) - /// - /// ```swift - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .onOpenInBrowser { - /// print("Opened in Safari!") - /// } - /// ``` - /// - /// - Parameter onOpenInBrowser: The function to execute when the user opens a loaded page in their safari app - /// - Returns: The safari view - public func onOpenInBrowser(_ onOpenInBrowser: (() -> Void)?) -> Self { - var copy = self - copy.willOpenInBrowser = onOpenInBrowser ?? {} - return copy - } + /// Set the safari view's dismiss button style + /// + /// This property is equivelent to `SFSafariViewController`'s `.dismissButtonStyle` property + public var dismissButtonStyle: DismissButtonStyle - /// Add [`UIActivity`](https://developer.apple.com/documentation/uikit/uiactivity) items to the Safari View - /// - /// Use this modifier to conditionally add activity items to the view based on the user's current URL or page title. - /// If you wish to show activity items that persist regardless of the user's activity, use the ``activityItems(_:)-3mpe3`` modifier instead - /// - /// This method behaves similarly to [`SFSafariViewControllerDelegate`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontrollerdelegate) method [`safariViewController(_:activityItemsFor:title:))`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontrollerdelegate/1621216-safariviewcontroller) - /// - /// ```swift - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .activtyItems { url, title in - /// if title == "MyTitle" { - /// return [item1, item2] - /// } else { - /// return [item3, item4] - /// } - /// } - /// ``` - /// - /// - Parameter itemProvider: Closure used to build an array of application-specific services you have chosen to include in the `SafariView`, based on the user's current URL and page title. - /// - Returns: The safari view - public func activityItems(_ itemProvider: ((_ url: URL, _ pageTitle: String?) -> [UIActivity])?) -> Self { - var copy = self - copy.itemProvider = itemProvider ?? { _, _ in [] } - return copy } - /// Add [`UIActivity`](https://developer.apple.com/documentation/uikit/uiactivity) items to the Safari View - /// - /// The items you provide are shown with every page the user might load in the `SafariView`. - /// If you wish to conditionally show items based on the current URL or page title, use the ``activityItems(_:)-2yuvl`` modifier instead. - /// - /// This method behaves similarly to [`SFSafariViewControllerDelegate`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontrollerdelegate) method [`safariViewController(_:activityItemsFor:title:))`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontrollerdelegate/1621216-safariviewcontroller) - /// - /// ```swift - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .activtyItems([item1, item1]) - /// ``` - /// - /// - Parameter items: An array of application-specific services you have chosen to include in the `SafariView` - /// - Returns: The safari view - public func activityItems(_ items: [UIActivity]) -> Self { - var copy = self - copy.activityItems = items - return copy - } + /// A convenience typealias for [`SFSafariViewController.DismissButtonStyle`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller/dismissbuttonstyle) + public typealias DismissButtonStyle = SFSafariViewController.DismissButtonStyle - /// Exclude [`UIActivity.ActivityType`](https://developer.apple.com/documentation/uikit/uiactivity/activitytype)s from the Safari View - /// - /// Use this modifier to conditionally exclude activity types based on the user's current URL or page title. - /// If you wish to exclude activity types from every page visited by the user, use the ``excludingActivityItems(_:)-5m53s`` modifier instead - /// - /// ```swift - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .excludingActivityItems { url, title in - /// if title == "MyTitle" { - /// return [type1, type2] - /// } else { - /// return [type3, type4] - /// } - /// } - /// ``` - /// - Parameter excludedActivityProvider: Closure used to build a list of activity types you wish to exclude from the `SafariView`, based on the current URL and page title. - /// - Returns: The safari view - public func excludingActivityItems(_ excludedActivityProvider: ((_ url: URL, _ title: String?) -> [UIActivity.ActivityType])?) -> Self { - var copy = self - copy.excludedActivityProvider = excludedActivityProvider ?? { _, _ in [] } - return copy - } + /// A convenience typealias for [`SFSafariViewController.ActivityButton`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller/activitybutton) + public typealias ActivityButton = SFSafariViewController.ActivityButton - /// Exclude [`UIActivity.ActivityType`](https://developer.apple.com/documentation/uikit/uiactivity/activitytype)s from the Safari View - /// - /// The activity types you provide are exlouded from every page the user might load in the `SafariView` - /// If you wish to conditionally exclude activity types based on the current URL or page title, use the ``excludingActivityItems(_:)-6whri`` modifier instead. - /// - /// ```swift - /// let url = URL(string: "https://www.apple.com")! - /// let view = SafariView(url: url) - /// .excludingActivityItems: [type1, type2]) - /// ``` - /// - /// - Parameter activityTypes: A list of activity types you wish to exclude from every page loaded by the `SafariView` - /// - Returns: The Safari View - public func excludingActivityItems(_ activityTypes: [UIActivity.ActivityType]) -> Self { - var copy = self - copy.excludedActivities = activityTypes - return copy - } + /// A convenience typealias for [`SFSafariViewController.PrewarmingToken`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller/prewarmingtoken) + public typealias PrewarmingToken = SFSafariViewController.PrewarmingToken /// Prewarm the connection to a list of provided URLs /// @@ -473,6 +211,117 @@ public struct SafariView: View { // MARK: - Private + @Environment(\.safariViewConfiguration) + private var configuration: SafariView.Configuration + + @Environment(\.safariViewStyle) + private var style: SafariView.Style + + private let url: URL + private let onInitialLoad: ((Bool) -> Void)? + private let onInitialRedirect: ((URL) -> Void)? + private let onOpenInBrowser: (() -> Void)? + + private func apply(to controller: SFSafariViewController) { + controller.preferredBarTintColor = style.preferredBarTintColor.map(UIColor.init) + controller.preferredControlTintColor = UIColor(style.preferredControlTintColor) + controller.dismissButtonStyle = style.dismissButtonStyle + } + + private struct Safari: UIViewControllerRepresentable { + + // MARK: - Initializers + + init(parent: SafariView) { + self.parent = parent + delegate = Delegate( + onInitialLoad: parent.onInitialLoad, + onInitialRedirect: parent.onInitialRedirect, + onOpenInBrowser: parent.onOpenInBrowser, + withActivityItems: parent.configuration.includedActivities, + withoutActivityItems: parent.configuration.excludedActivityTypes + ) + } + + // MARK: - UIViewControllerRepresentable + + func makeUIViewController(context: Context) -> SFSafariViewController { + let safari = SFSafariViewController(url: parent.url, + configuration: parent.configuration.buildUIKitConfiguration()) + safari.modalPresentationStyle = .none + safari.delegate = delegate + parent.apply(to: safari) + return safari + } + + func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) { + parent.apply(to: uiViewController) + uiViewController.delegate = delegate + } + + // MARK: - Private + + private var parent: SafariView + private let delegate: Delegate + + private final class Delegate: NSObject, SFSafariViewControllerDelegate { + + init( + onInitialLoad: ((Bool) -> Void)?, + onInitialRedirect: ((URL) -> Void)?, + onOpenInBrowser: (() -> Void)?, + withActivityItems: ((URL, String?) -> [UIActivity])?, + withoutActivityItems: ((URL, String?) -> [UIActivity.ActivityType])? + ) { + self.onInitialLoad = onInitialLoad + self.onInitialRedirect = onInitialRedirect + self.onOpenInBrowser = onOpenInBrowser + self.withActivityItems = withActivityItems + self.withoutActivityItems = withoutActivityItems + } + + // MARK: - SFSafariViewControllerDelegate + + func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) { + onInitialLoad?(didLoadSuccessfully) + } + + func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo URL: URL) { + onInitialRedirect?(URL) + } + + func safariViewController( + _ controller: SFSafariViewController, + activityItemsFor URL: URL, + title: String? + ) -> [UIActivity] { + withActivityItems?(URL, title) ?? [] + } + + func safariViewController( + _ controller: SFSafariViewController, + excludedActivityTypesFor URL: URL, + title: String? + ) -> [UIActivity.ActivityType] { + withoutActivityItems?(URL, title) ?? [] + } + + func safariViewControllerWillOpenInBrowser(_ controller: SFSafariViewController) { + onOpenInBrowser?() + } + + // MARK: - Private + + private let onInitialLoad: ((Bool) -> Void)? + private let onInitialRedirect: ((URL) -> Void)? + private let onOpenInBrowser: (() -> Void)? + private let withActivityItems: ((URL, String?) -> [UIActivity])? + private let withoutActivityItems: ((URL, String?) -> [UIActivity.ActivityType])? + + } + + } + struct BoolModifier: ViewModifier { // MARK: - API @@ -540,11 +389,11 @@ public struct SafariView: View { // MARK: - SFSafariViewControllerDelegate func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) { - onInitialLoad(didLoadSuccessfully) + onInitialLoad?(didLoadSuccessfully) } func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo URL: URL) { - onInitialRedirect(URL) + onInitialRedirect?(URL) } func safariViewControllerDidFinish(_ controller: SFSafariViewController) { @@ -552,36 +401,44 @@ public struct SafariView: View { parent.onDismiss() } - func safariViewController(_ controller: SFSafariViewController, activityItemsFor URL: URL, title: String?) -> [UIActivity] { - withActivityItems(URL, title) + func safariViewController( + _ controller: SFSafariViewController, + activityItemsFor URL: URL, + title: String? + ) -> [UIActivity] { + withActivityItems?(URL, title) ?? [] } - func safariViewController(_ controller: SFSafariViewController, excludedActivityTypesFor URL: URL, title: String?) -> [UIActivity.ActivityType] { - withoutActivityItems(URL, title) + func safariViewController( + _ controller: SFSafariViewController, + excludedActivityTypesFor URL: URL, + title: String? + ) -> [UIActivity.ActivityType] { + withoutActivityItems?(URL, title) ?? [] } func safariViewControllerWillOpenInBrowser(_ controller: SFSafariViewController) { - willOpenInBrowser() + onOpenInBrowser?() } // MARK: - Private private weak var safari: SFSafariViewController? - private var onInitialLoad: (Bool) -> Void = { _ in } - private var onInitialRedirect: (URL) -> Void = { _ in } - private var withActivityItems: (URL, String?) -> [UIActivity] = { _, _ in [] } - private var withoutActivityItems: (URL, String?) -> [UIActivity.ActivityType] = { _, _ in [] } - private var willOpenInBrowser: () -> Void = {} + private var onInitialLoad: ((Bool) -> Void)? + private var onInitialRedirect: ((URL) -> Void)? + private var onOpenInBrowser: (() -> Void)? + private var withActivityItems: ((URL, String?) -> [UIActivity])? + private var withoutActivityItems: ((URL, String?) -> [UIActivity.ActivityType])? private func presentSafari() { let rep = parent.build() onInitialLoad = rep.onInitialLoad onInitialRedirect = rep.onInitialRedirect - withActivityItems = rep.withActivityItems - withoutActivityItems = rep.withoutActivityItems - willOpenInBrowser = rep.willOpenInBrowser - let vc = SFSafariViewController(url: rep.url, configuration: rep.configuration) + onOpenInBrowser = rep.onOpenInBrowser + withActivityItems = rep.configuration.includedActivities + withoutActivityItems = rep.configuration.excludedActivityTypes + let vc = SFSafariViewController(url: rep.url, configuration: rep.configuration.buildUIKitConfiguration()) vc.delegate = self rep.apply(to: vc) @@ -600,9 +457,9 @@ public struct SafariView: View { let rep = parent.build() onInitialLoad = rep.onInitialLoad onInitialRedirect = rep.onInitialRedirect - withActivityItems = rep.withActivityItems - withoutActivityItems = rep.withoutActivityItems - willOpenInBrowser = rep.willOpenInBrowser + onOpenInBrowser = rep.onOpenInBrowser + withActivityItems = rep.configuration.includedActivities + withoutActivityItems = rep.configuration.excludedActivityTypes rep.apply(to: safari) } @@ -610,11 +467,9 @@ public struct SafariView: View { guard let safari else { return } - safari.dismiss(animated: true) { self.parent.onDismiss() } - } } @@ -636,7 +491,7 @@ public struct SafariView: View { } - struct ItemModitifer: ViewModifier where Item: Identifiable { + struct IdentifiableItemModitifer: ViewModifier where Item: Identifiable { // MARK: - API @@ -711,43 +566,51 @@ public struct SafariView: View { } func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo URL: URL) { - onInitialRedirect(URL) + onInitialRedirect?(URL) } func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) { - onInitialLoad(didLoadSuccessfully) + onInitialLoad?(didLoadSuccessfully) } - func safariViewController(_ controller: SFSafariViewController, activityItemsFor URL: URL, title: String?) -> [UIActivity] { - withActivityItems(URL, title) + func safariViewController( + _ controller: SFSafariViewController, + activityItemsFor URL: URL, + title: String? + ) -> [UIActivity] { + withActivityItems?(URL, title) ?? [] } - func safariViewController(_ controller: SFSafariViewController, excludedActivityTypesFor URL: URL, title: String?) -> [UIActivity.ActivityType] { - withoutActivityItems(URL, title) + func safariViewController( + _ controller: SFSafariViewController, + excludedActivityTypesFor URL: URL, + title: String? + ) -> [UIActivity.ActivityType] { + withoutActivityItems?(URL, title) ?? [] } func safariViewControllerWillOpenInBrowser(_ controller: SFSafariViewController) { - willOpenInBrowser() + onOpenInBrowser?() } // MARK: - Private private weak var safari: SFSafariViewController? - private var onInitialLoad: (Bool) -> Void = { _ in } - private var onInitialRedirect: (URL) -> Void = { _ in } - private var withActivityItems: (URL, String?) -> [UIActivity] = { _, _ in [] } - private var withoutActivityItems: (URL, String?) -> [UIActivity.ActivityType] = { _, _ in [] } - private var willOpenInBrowser: () -> Void = {} + private var onInitialLoad: ((Bool) -> Void)? + private var onInitialRedirect: ((URL) -> Void)? + private var onOpenInBrowser: (() -> Void)? + private var withActivityItems: ((URL, String?) -> [UIActivity])? + private var withoutActivityItems: ((URL, String?) -> [UIActivity.ActivityType])? private func presentSafari(with item: Item) { let rep = parent.build(item) onInitialLoad = rep.onInitialLoad onInitialRedirect = rep.onInitialRedirect - withActivityItems = rep.withActivityItems - withoutActivityItems = rep.withoutActivityItems - willOpenInBrowser = rep.willOpenInBrowser - let vc = SFSafariViewController(url: rep.url, configuration: rep.configuration) + onOpenInBrowser = rep.onOpenInBrowser + withActivityItems = rep.configuration.includedActivities + withoutActivityItems = rep.configuration.excludedActivityTypes + let vc = SFSafariViewController(url: rep.url, configuration: rep.configuration.buildUIKitConfiguration()) vc.delegate = self rep.apply(to: vc) guard let presenting = view.controller else { @@ -767,9 +630,9 @@ public struct SafariView: View { let rep = parent.build(item) onInitialLoad = rep.onInitialLoad onInitialRedirect = rep.onInitialRedirect - withActivityItems = rep.withActivityItems - withoutActivityItems = rep.withoutActivityItems - willOpenInBrowser = rep.willOpenInBrowser + onOpenInBrowser = rep.onOpenInBrowser + withActivityItems = rep.configuration.includedActivities + withoutActivityItems = rep.configuration.excludedActivityTypes rep.apply(to: safari) } @@ -800,7 +663,7 @@ public struct SafariView: View { } } - struct GenericItemModifier: ViewModifier where Identifier: Hashable { + struct ItemModifier: ViewModifier where Identifier: Hashable { // MARK: - Initializers @@ -809,8 +672,7 @@ public struct SafariView: View { id: KeyPath, onDismiss: (() -> Void)? = nil, @ViewBuilder safariView: @escaping (Item) -> SafariView - ) - { + ) { self.item = item self.id = id self.onDismiss = onDismiss @@ -865,115 +727,6 @@ public struct SafariView: View { } } - private struct Safari: UIViewControllerRepresentable { - - // MARK: - Initializers - - init(parent: SafariView) { - self.parent = parent - delegate = Delegate(onInitialLoad: parent.onInitialLoad, - onInitialRedirect: parent.onInitialRedirect, - withActivityItems: parent.withActivityItems, - withoutActivityItems: parent.withoutActivityItems, - willOpenInBrowser: parent.willOpenInBrowser) - } - - // MARK: - UIViewControllerRepresentable - - func makeUIViewController(context: Context) -> SFSafariViewController { - let safari = SFSafariViewController(url: parent.url, - configuration: parent.configuration) - - safari.modalPresentationStyle = .none - safari.delegate = delegate - parent.apply(to: safari) - return safari - } - - func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) { - parent.apply(to: uiViewController) - uiViewController.delegate = delegate - } - - // MARK: - Private - - private var parent: SafariView - private let delegate: Delegate - - private final class Delegate: NSObject, SFSafariViewControllerDelegate { - - init(onInitialLoad: @escaping (Bool) -> Void, - onInitialRedirect: @escaping (URL) -> Void, - withActivityItems: @escaping (URL, String?) -> [UIActivity], - withoutActivityItems: @escaping (URL, String?) -> [UIActivity.ActivityType], - willOpenInBrowser: @escaping () -> Void) { - self.onInitialLoad = onInitialLoad - self.onInitialRedirect = onInitialRedirect - self.withActivityItems = withActivityItems - self.withoutActivityItems = withoutActivityItems - self.willOpenInBrowser = willOpenInBrowser - } - - // MARK: - SFSafariViewDelegate - - func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) { - onInitialLoad(didLoadSuccessfully) - } - - func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo URL: URL) { - onInitialRedirect(URL) - } - - func safariViewController(_ controller: SFSafariViewController, activityItemsFor URL: URL, title: String?) -> [UIActivity] { - withActivityItems(URL, title) - } - - func safariViewController(_ controller: SFSafariViewController, excludedActivityTypesFor URL: URL, title: String?) -> [UIActivity.ActivityType] { - withoutActivityItems(URL, title) - } - - func safariViewControllerWillOpenInBrowser(_ controller: SFSafariViewController) { - willOpenInBrowser() - } - - // MARK: - PRivate - - private let onInitialLoad: (Bool) -> Void - private let onInitialRedirect: (URL) -> Void - private let withActivityItems: (URL, String?) -> [UIActivity] - private let withoutActivityItems: (URL, String?) -> [UIActivity.ActivityType] - private let willOpenInBrowser: () -> Void - } - - } - - private let url: URL - private var configuration: Configuration - private var barTintColor: Color? - private var controlTintColor: Color? - private var dismissButtonStyle: DismissButtonStyle = .done - private var onInitialLoad: (Bool) -> Void = { _ in } - private var onInitialRedirect: (URL) -> Void = { _ in } - private var itemProvider: (URL, String?) -> [UIActivity] = { _, _ in [] } - private var activityItems: [UIActivity] = [] - private var excludedActivities: [UIActivity.ActivityType] = [] - private var excludedActivityProvider: (URL, String?) -> [UIActivity.ActivityType] = { _, _ in [] } - private var willOpenInBrowser: () -> Void = {} - - private func withActivityItems(_ url: URL, _ title: String?) -> [UIActivity] { - itemProvider(url, title) + activityItems - } - - private func withoutActivityItems(_ url: URL, _ title: String?) -> [UIActivity.ActivityType] { - excludedActivityProvider(url, title) + excludedActivities - } - - private func apply(to controller: SFSafariViewController) { - controller.preferredBarTintColor = barTintColor.map(UIColor.init) - controller.preferredControlTintColor = controlTintColor.map(UIColor.init) - controller.dismissButtonStyle = dismissButtonStyle - } - } private extension UIView { @@ -987,3 +740,16 @@ private extension UIView { } } } + +private extension SafariView.Configuration { + + func buildUIKitConfiguration() -> SFSafariViewController.Configuration { + let config = SFSafariViewController.Configuration() + config.entersReaderIfAvailable = entersReaderIfAvailable + config.barCollapsingEnabled = barCollapsingEnabled + config.activityButton = activityButton + config.eventAttribution = eventAttribution + return config + } + +}