diff --git a/Package.resolved b/Package.resolved index b0bba1e94..51ceeeb48 100644 --- a/Package.resolved +++ b/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nicklockwood/SwiftFormat", "state" : { - "revision" : "507d63f6e890621f055397c9503bfc9468e8bcb6", - "version" : "0.51.15" + "revision" : "9e5d0d588ab6e271fe9887ec3dde21d544d4b080", + "version" : "0.53.5" } } ], diff --git a/Sources/SafariUI/SafariUI.docc/SafariView.md b/Sources/SafariUI/SafariUI.docc/SafariView.md index 08cea8e6c..5a3ca8ad6 100644 --- a/Sources/SafariUI/SafariUI.docc/SafariView.md +++ b/Sources/SafariUI/SafariUI.docc/SafariView.md @@ -23,6 +23,7 @@ You can present a `SafariView` using the built-in presentation view modifiers: - ``SwiftUI/View/safari(isPresented:url:onDismiss:)`` - ``SwiftUI/View/safari(item:onDismiss:safariView:)`` - ``SwiftUI/View/safari(item:id:onDismiss:safariView:)`` +- ``SwiftUI/View/safari(url:onDismiss:)`` You can also use sheet presentation, or any other presentation mechanism of your choice. diff --git a/Sources/SafariUI/SafariUI.docc/View.md b/Sources/SafariUI/SafariUI.docc/View.md index d7c810845..113f9e732 100644 --- a/Sources/SafariUI/SafariUI.docc/View.md +++ b/Sources/SafariUI/SafariUI.docc/View.md @@ -21,6 +21,7 @@ SwiftUI view modifiers used to configure a ``SafariView`` or a ``WebAuthenticati - ``SwiftUI/View/safari(isPresented:url:onDismiss:)`` - ``SwiftUI/View/safari(item:onDismiss:safariView:)`` - ``SwiftUI/View/safari(item:id:onDismiss:safariView:)`` +- ``SwiftUI/View/safari(url:onDismiss:)`` ### SafarView Custom Activities diff --git a/Sources/SafariView/Modifiers.swift b/Sources/SafariView/Modifiers.swift index 85ec7b008..8547da5e2 100644 --- a/Sources/SafariView/Modifiers.swift +++ b/Sources/SafariView/Modifiers.swift @@ -35,8 +35,10 @@ public extension View { /// - Parameter entersReaderIfAvailable: Whether or not the safari view should automatically enter reader mode if available. /// - Returns: The modified view func safariEntersReaderIfAvailable(_ entersReaderIfAvailable: Bool = true) -> some View { - let modifier = SafariViewEntersReaderIfAvailableModifier(entersReaderIfAvailable: entersReaderIfAvailable) - return ModifiedContent(content: self, modifier: modifier) + ModifiedContent( + content: self, + modifier: SafariViewEntersReaderIfAvailableModifier(entersReaderIfAvailable: entersReaderIfAvailable) + ) } /// Set the bar collapsing behavior of safari views within this view @@ -46,8 +48,10 @@ public extension View { /// - Parameter barCollapsingEnabled: Whether or not bar collpasing should be enabled. /// - Returns: The modified view func safariBarCollapsingEnabled(_ barCollapsingEnabled: Bool = true) -> some View { - let modifier = SafariViewBarCollapsingEnabledModifier(barCollapsingEnabled: barCollapsingEnabled) - return ModifiedContent(content: self, modifier: modifier) + ModifiedContent( + content: self, + modifier: SafariViewBarCollapsingEnabledModifier(barCollapsingEnabled: barCollapsingEnabled) + ) } /// Set the bar tint color of safari views within this view @@ -57,8 +61,10 @@ public extension View { /// - Parameter color: The color to use, or `nil` for the system default /// - Returns: The modified view func safariBarTintColor(_ color: Color?) -> some View { - let modifier = SafariViewBarTintColorModifier(safariViewBarTintColor: color) - return ModifiedContent(content: self, modifier: modifier) + ModifiedContent( + content: self, + modifier: SafariViewBarTintColorModifier(safariViewBarTintColor: color) + ) } /// Set the control tint color of safari views within this view @@ -68,8 +74,10 @@ public extension View { /// - Parameter color: The color to use /// - Returns: The modified view func safariControlTintColor(_ color: Color) -> some View { - let modifier = SafariViewControlTintColorModifier(safariViewControlTintColor: color) - return ModifiedContent(content: self, modifier: modifier) + ModifiedContent( + content: self, + modifier: SafariViewControlTintColorModifier(safariViewControlTintColor: color) + ) } /// Set the safari view's dismiss button style @@ -107,8 +115,10 @@ public extension View { /// - Parameter activities: The activities to include. You may use an array literal of `UIActivity` types. /// - Returns: The modified content func includedSafariActivities(_ activities: SafariView.IncludedActivities) -> some View { - let modifier = SafariViewIncludedActivitiesModifier(activities: activities) - return ModifiedContent(content: self, modifier: modifier) + ModifiedContent( + content: self, + modifier: SafariViewIncludedActivitiesModifier(activities: activities) + ) } /// Conditionally include activities in the share sheet of safari views within this view. @@ -138,9 +148,12 @@ public extension View { /// - Parameter activities: Closure used to conditionally include activities /// - Returns: The modified content func includedSafariActivities(_ activities: @escaping (_ url: URL, _ pageTitle: String?) -> [UIActivity]) -> some View { - let activities = SafariView.IncludedActivities(activities) - let modifier = SafariViewIncludedActivitiesModifier(activities: activities) - return ModifiedContent(content: self, modifier: modifier) + ModifiedContent( + content: self, + modifier: SafariViewIncludedActivitiesModifier( + activities: .init(activities) + ) + ) } /// Exclude activity types from the share sheet of safari views within this view. @@ -167,8 +180,10 @@ public extension View { /// - Parameter activityTypes: The activity types to exclude. You may use an array literal of `UIActivity.ActivityType` values. /// - Returns: The modified content func excludedSafariActivityTypes(_ activityTypes: SafariView.ExcludedActivityTypes) -> some View { - let modifier = SafariViewExcludedActivityTypesModifier(activityTypes: activityTypes) - return ModifiedContent(content: self, modifier: modifier) + ModifiedContent( + content: self, + modifier: SafariViewExcludedActivityTypesModifier(activityTypes: activityTypes) + ) } /// Conditionally exclude activity types from the share sheet of safari views within this view. @@ -196,10 +211,15 @@ public extension View { /// /// - Parameter activityTypes: Closure used to conditionally exclude activities /// - Returns: The modified content - func excludedSafariActivityTypes(_ activityTypes: @escaping (_ url: URL, _ pageTitle: String?) -> [UIActivity.ActivityType]) -> some View { - let activityTypes = SafariView.ExcludedActivityTypes(activityTypes) - let modifier = SafariViewExcludedActivityTypesModifier(activityTypes: activityTypes) - return ModifiedContent(content: self, modifier: modifier) + func excludedSafariActivityTypes( + _ activityTypes: @escaping (_ url: URL, _ pageTitle: String?) -> [UIActivity.ActivityType] + ) -> some View { + ModifiedContent( + content: self, + modifier: SafariViewExcludedActivityTypesModifier( + activityTypes: .init(activityTypes) + ) + ) } } diff --git a/Sources/SafariView/BoolPresentation.swift b/Sources/SafariView/Presentation/BoolPresentation.swift similarity index 87% rename from Sources/SafariView/BoolPresentation.swift rename to Sources/SafariView/Presentation/BoolPresentation.swift index 3f1f476f4..e96d7b2af 100644 --- a/Sources/SafariView/BoolPresentation.swift +++ b/Sources/SafariView/Presentation/BoolPresentation.swift @@ -86,62 +86,6 @@ public extension View { ) } - /// Presents a ``SafariView`` when a binding to a Boolean value that you provide is `true`. - /// - /// Use this method when you want to present a ``SafariView`` to the user when a Boolean value you provide is true. - /// The example below displays a modal view of the mockup for a software license agreement when the user toggles the `isShowingSafari` variable by clicking or tapping on the “Show License Agreement” button: - /// - /// ```swift - /// import Foundation - /// import SafariView - /// import SwiftUI - /// - /// struct ShowLicenseAgreement: View { - /// - /// let licenseAgreementURL: URL - /// - /// @State private var isShowingSafari = false - /// - /// var body: some View { - /// Button { - /// isShowingSafari.toggle() - /// } label: { - /// Text("Show License Agreement") - /// } - /// .safari(isPresented: $isShowingSafari, - /// url: licenseAgreementURL - /// onDismiss: didDismiss) - /// } - /// - /// func didDismiss() { - /// // Handle the dismissing action. - /// } - /// - /// } - /// ``` - /// - /// - Parameters: - /// - isPresented: A binding to a Boolean value that determines whether to present the ``SafariView`` that you create in the modifier’s content closure. - /// - url: The URL to load in the presented ``SafariView`` - /// - onDismiss: The closure to execute when dismissing the ``SafariView`` - /// - Returns: The modified view - func safari( - isPresented: Binding, - url: URL, - onDismiss: (() -> Void)? = nil - ) -> some View { - ModifiedContent( - content: self, - modifier: IsPresentedModifier( - isPresented: isPresented, - safariView: SafariView( - url: url - ), - onDismiss: onDismiss - ) - ) - } - } @available(iOS 14.0, macCatalyst 14.0, *) diff --git a/Sources/SafariView/Presentation/BoolURLPresentation.swift b/Sources/SafariView/Presentation/BoolURLPresentation.swift new file mode 100644 index 000000000..ec3ca4b84 --- /dev/null +++ b/Sources/SafariView/Presentation/BoolURLPresentation.swift @@ -0,0 +1,118 @@ +// SafariUI +// BoolURLPresentation.swift +// +// MIT License +// +// Copyright (c) 2023 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 Foundation +import SwiftUI + +@available(iOS 14.0, macCatalyst 14.0, *) +public extension View { + + /// Presents a ``SafariView`` when a binding to a Boolean value that you provide is `true`. + /// + /// Use this method when you want to present a ``SafariView`` to the user when a Boolean value you provide is true. + /// The example below displays a modal view of the mockup for a software license agreement when the user toggles the `isShowingSafari` variable by clicking or tapping on the “Show License Agreement” button: + /// + /// ```swift + /// import Foundation + /// import SafariView + /// import SwiftUI + /// + /// struct ShowLicenseAgreement: View { + /// + /// let licenseAgreementURL: URL + /// + /// @State private var isShowingSafari = false + /// + /// var body: some View { + /// Button { + /// isShowingSafari.toggle() + /// } label: { + /// Text("Show License Agreement") + /// } + /// .safari(isPresented: $isShowingSafari, + /// url: licenseAgreementURL + /// onDismiss: didDismiss) + /// } + /// + /// func didDismiss() { + /// // Handle the dismissing action. + /// } + /// + /// } + /// ``` + /// + /// - Parameters: + /// - isPresented: A binding to a Boolean value that determines whether to present the ``SafariView`` that you create in the modifier’s content closure. + /// - url: The URL to load in the presented ``SafariView`` + /// - onDismiss: The closure to execute when dismissing the ``SafariView`` + /// - Returns: The modified view + func safari( + isPresented: Binding, + url: URL, + onDismiss: (() -> Void)? = nil + ) -> some View { + ModifiedContent( + content: self, + modifier: BoolURLPresentation( + isPresented: isPresented, + onDismiss: onDismiss, + url: url + ) + ) + } + +} + +@available(iOS 14.0, macCatalyst 14.0, *) +private struct BoolURLPresentation: ViewModifier { + + init( + isPresented: Binding, + onDismiss: (() -> Void)?, + url: URL + ) { + _isPresented = isPresented + self.onDismiss = onDismiss + self.url = url + } + + @MainActor + @ViewBuilder + func body(content: Content) -> some View { + content + .safari( + isPresented: $isPresented, + onDismiss: onDismiss + ) { + SafariView(url: url) + } + } + + @Binding + private var isPresented: Bool + private let onDismiss: (() -> Void)? + private let url: URL + +} diff --git a/Sources/SafariView/ItemPresentation.swift b/Sources/SafariView/Presentation/ItemPresentation.swift similarity index 100% rename from Sources/SafariView/ItemPresentation.swift rename to Sources/SafariView/Presentation/ItemPresentation.swift diff --git a/Sources/SafariView/Presentation/URLPresentation.swift b/Sources/SafariView/Presentation/URLPresentation.swift new file mode 100644 index 000000000..b37dd4950 --- /dev/null +++ b/Sources/SafariView/Presentation/URLPresentation.swift @@ -0,0 +1,109 @@ +// SafariUI +// URLPresentation.swift +// +// MIT License +// +// Copyright (c) 2023 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 Foundation +import SwiftUI + +@available(iOS 14.0, macCatalyst 14.0, *) +public extension View { + + /// Presents a ``SafariView`` using the given URL. + /// + /// Use this method when you need to present a ``SafariView`` with content from URL. + /// The example below shows a list of buttons, each of which provide a different URL to the view. + /// + /// ```swift + /// struct InformationView: View { + /// + /// @State var url: URL? + /// + /// var body: some View { + /// VStack { + /// Button("Terms of service") { + /// url = URL(string: "https://www.myservice.com/terms-of-service")! + /// } + /// Button("Warranty") { + /// url = URL(string: "https://www.myservice.com/warranty")! + /// } + /// Button("Support") { + /// ` url = URL(string: "https://www.myservice.com/help")! + /// } + /// } + /// .safari(url: $url, onDismiss: dismissAction) + /// } + /// + /// func didDismiss() { + /// // Handle the dismissing action. + /// } + /// } + /// ``` + /// - Parameters: + /// - url: The URL used to load the view + /// - onDismiss: The closure to execute when dismissing the ``SafariView`` + /// - Returns: The modified view + func safari( + url: Binding, + onDismiss: (() -> Void)? = nil + ) -> some View { + ModifiedContent( + content: self, + modifier: URLPresentation( + url: url, + onDismiss: onDismiss + ) + ) + } + +} + +@available(iOS 14.0, macCatalyst 14.0, *) +private struct URLPresentation: ViewModifier { + + init( + url: Binding, + onDismiss: (() -> Void)? + ) { + _url = url + self.onDismiss = onDismiss + } + + @MainActor + @ViewBuilder + func body(content: Content) -> some View { + content + .safari( + item: $url, + id: \.hashValue, + onDismiss: onDismiss + ) { url in + SafariView(url: url) + } + } + + @Binding + private var url: URL? + private let onDismiss: (() -> Void)? + +} diff --git a/Sources/SafariView/WrappedItemPresentation.swift b/Sources/SafariView/Presentation/WrappedItemPresentation.swift similarity index 92% rename from Sources/SafariView/WrappedItemPresentation.swift rename to Sources/SafariView/Presentation/WrappedItemPresentation.swift index 3cd45c349..59871a4d4 100644 --- a/Sources/SafariView/WrappedItemPresentation.swift +++ b/Sources/SafariView/Presentation/WrappedItemPresentation.swift @@ -38,8 +38,7 @@ public extension View { /// import SafariView /// import SwiftUI /// - /// struct InventoryItem: Identifiable { - /// let id: Int + /// struct InventoryItem { /// let title: String /// let url: URL /// } @@ -56,7 +55,7 @@ public extension View { /// @State private var selectedItem: InventoryItem? /// /// var body: some View { - /// List(inventory) { inventoryItem in + /// List(inventory, id: \.title) { inventoryItem in /// Button(action: { /// self.selectedItem = inventoryItem /// }) { @@ -64,6 +63,7 @@ public extension View { /// } /// } /// .safari(item: $selectedItem, + /// id: \.title /// onDismiss: dismissAction) { item in /// SafariView(url: item.url) /// } @@ -138,7 +138,7 @@ private struct WrappedItemPresentation: ViewModifier where ID: Hashabl private let onDismiss: (() -> Void)? private let safariView: (Item) -> SafariView - private var wrappedItem: Binding?> { + private var wrappedItem: Binding { .init { if let item { .init(value: item, path: id) @@ -150,18 +150,18 @@ private struct WrappedItemPresentation: ViewModifier where ID: Hashabl } } - struct WrappedIdentifiable: Identifiable where ID: Hashable { + struct WrappedIdentifiable: Identifiable { init( - value: Wrapped, - path: KeyPath + value: Item, + path: KeyPath ) { self.value = value self.path = path } - let value: Wrapped - let path: KeyPath + let value: Item + let path: KeyPath var id: ID { value[keyPath: path]