(_ content: Content) -> Content {
+ content
+ }
+}
diff --git a/Demo/BetterSafariViewDemo/Models/WebAuthenticationSessionOptions.swift b/Demo/Shared/Options/WebAuthenticationSessionOptions.swift
similarity index 100%
rename from Demo/BetterSafariViewDemo/Models/WebAuthenticationSessionOptions.swift
rename to Demo/Shared/Options/WebAuthenticationSessionOptions.swift
diff --git a/Demo/BetterSafariViewDemo/Views/Form/WebAuthenticationSessionOptionsForm.swift b/Demo/Shared/Options/WebAuthenticationSessionOptionsForm.swift
similarity index 58%
rename from Demo/BetterSafariViewDemo/Views/Form/WebAuthenticationSessionOptionsForm.swift
rename to Demo/Shared/Options/WebAuthenticationSessionOptionsForm.swift
index 4e4d9dd..c75ca62 100644
--- a/Demo/BetterSafariViewDemo/Views/Form/WebAuthenticationSessionOptionsForm.swift
+++ b/Demo/Shared/Options/WebAuthenticationSessionOptionsForm.swift
@@ -22,24 +22,44 @@ struct WebAuthenticationSessionOptionsForm: View {
Form {
Section(header: Text("URL")) {
TextField(gitHubAuthorizationURLString, text: $temporaryOptions.urlString)
- .textContentType(.URL)
- .keyboardType(.URL)
- .autocapitalization(.none)
+ .modify {
+ #if os(iOS)
+ $0
+ .textContentType(.URL)
+ .keyboardType(.URL)
+ .autocapitalization(.none)
+ #endif
+ }
+ .modify {
+ #if os(watchOS)
+ $0
+ .textContentType(.URL)
+ #endif
+ }
}
Section(header: Text("Callback URL Scheme")) {
TextField(gitHubCallbackURLScheme, text: $temporaryOptions.callbackURLScheme)
- .textContentType(.URL)
- .keyboardType(.asciiCapable)
- .autocapitalization(.none)
+ .modify {
+ #if os(iOS)
+ $0
+ .textContentType(.URL)
+ .keyboardType(.asciiCapable)
+ .autocapitalization(.none)
+ #endif
+ }
+ .modify {
+ #if os(watchOS)
+ $0
+ .textContentType(.URL)
+ #endif
+ }
}
Section(header: Text("Modifiers")) {
Toggle("Ephemeral Session", isOn: $temporaryOptions.prefersEphemeralWebBrowserSession)
}
}
- .navigationTitle(Text("Session Options"))
- .navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
@@ -51,8 +71,16 @@ struct WebAuthenticationSessionOptionsForm: View {
options = temporaryOptions
presentationMode.wrappedValue.dismiss()
}
+ .disabled(urlIsInvalid)
}
}
+ .modify {
+ #if os(iOS)
+ $0
+ .navigationTitle(Text("Session Options"))
+ .navigationBarTitleDisplayMode(.inline)
+ #endif
+ }
}
}
}
diff --git a/Demo/BetterSafariViewDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Demo/Shared/macOS.entitlements
similarity index 61%
rename from Demo/BetterSafariViewDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
rename to Demo/Shared/macOS.entitlements
index 18d9810..1981043 100644
--- a/Demo/BetterSafariViewDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ b/Demo/Shared/macOS.entitlements
@@ -2,7 +2,9 @@
- IDEDidComputeMac32BitWarning
-
+ com.apple.security.app-sandbox
+
+ com.apple.security.network.client
+
diff --git a/Demo/BetterSafariViewDemo/Info.plist b/Demo/iOS/Info.plist
similarity index 98%
rename from Demo/BetterSafariViewDemo/Info.plist
rename to Demo/iOS/Info.plist
index a2c2ff4..d4d8d80 100644
--- a/Demo/BetterSafariViewDemo/Info.plist
+++ b/Demo/iOS/Info.plist
@@ -5,7 +5,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- SafariView
+ BetterSafari
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
diff --git a/Demo/BetterSafariViewDemo/Views/Components/DetailButton.swift b/Demo/iOS/Views/Components/DetailButton.swift
similarity index 100%
rename from Demo/BetterSafariViewDemo/Views/Components/DetailButton.swift
rename to Demo/iOS/Views/Components/DetailButton.swift
diff --git a/Demo/BetterSafariViewDemo/Views/Components/DisclosureIndicator.swift b/Demo/iOS/Views/Components/DisclosureIndicator.swift
similarity index 100%
rename from Demo/BetterSafariViewDemo/Views/Components/DisclosureIndicator.swift
rename to Demo/iOS/Views/Components/DisclosureIndicator.swift
diff --git a/Demo/BetterSafariViewDemo/Views/Components/TitleLabel.swift b/Demo/iOS/Views/Components/TitleLabel.swift
similarity index 100%
rename from Demo/BetterSafariViewDemo/Views/Components/TitleLabel.swift
rename to Demo/iOS/Views/Components/TitleLabel.swift
diff --git a/Demo/BetterSafariViewDemo/Views/NaiveSafariView.swift b/Demo/iOS/Views/NaiveSafariView.swift
similarity index 100%
rename from Demo/BetterSafariViewDemo/Views/NaiveSafariView.swift
rename to Demo/iOS/Views/NaiveSafariView.swift
diff --git a/Demo/BetterSafariViewDemo/Models/SafariViewOptions.swift b/Demo/iOS/Views/Options/SafariViewOptions.swift
similarity index 100%
rename from Demo/BetterSafariViewDemo/Models/SafariViewOptions.swift
rename to Demo/iOS/Views/Options/SafariViewOptions.swift
diff --git a/Demo/BetterSafariViewDemo/Views/Form/SafariViewOptionsForm.swift b/Demo/iOS/Views/Options/SafariViewOptionsForm.swift
similarity index 98%
rename from Demo/BetterSafariViewDemo/Views/Form/SafariViewOptionsForm.swift
rename to Demo/iOS/Views/Options/SafariViewOptionsForm.swift
index 21ccf46..2b2c0e9 100644
--- a/Demo/BetterSafariViewDemo/Views/Form/SafariViewOptionsForm.swift
+++ b/Demo/iOS/Views/Options/SafariViewOptionsForm.swift
@@ -80,6 +80,7 @@ struct SafariViewOptionsForm: View {
options = temporaryOptions
presentationMode.wrappedValue.dismiss()
}
+ .disabled(urlIsInvalid)
}
}
}
diff --git a/Demo/BetterSafariViewDemo/Views/RootView.swift b/Demo/iOS/Views/RootView.swift
similarity index 100%
rename from Demo/BetterSafariViewDemo/Views/RootView.swift
rename to Demo/iOS/Views/RootView.swift
diff --git a/Demo/macOS/HorizontalAlignment+PreferenceLabelAlignment.swift b/Demo/macOS/HorizontalAlignment+PreferenceLabelAlignment.swift
new file mode 100644
index 0000000..d3036d7
--- /dev/null
+++ b/Demo/macOS/HorizontalAlignment+PreferenceLabelAlignment.swift
@@ -0,0 +1,11 @@
+import SwiftUI
+
+extension HorizontalAlignment {
+ private enum PreferenceLabelAlignment: AlignmentID {
+ static func defaultValue(in context: ViewDimensions) -> CGFloat {
+ context[HorizontalAlignment.center]
+ }
+ }
+
+ static let preferenceLabel = HorizontalAlignment(PreferenceLabelAlignment.self)
+}
diff --git a/Demo/macOS/Info.plist b/Demo/macOS/Info.plist
new file mode 100644
index 0000000..f15d7ce
--- /dev/null
+++ b/Demo/macOS/Info.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIconFile
+
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ LSMinimumSystemVersion
+ $(MACOSX_DEPLOYMENT_TARGET)
+
+
diff --git a/Demo/macOS/RootView.swift b/Demo/macOS/RootView.swift
new file mode 100644
index 0000000..4669044
--- /dev/null
+++ b/Demo/macOS/RootView.swift
@@ -0,0 +1,81 @@
+import SwiftUI
+import BetterSafariView
+
+struct RootView: View {
+
+ @State private var webAuthenticationSessionOptions = WebAuthenticationSessionOptions()
+ @State private var showingWebAuthenticationSession = false
+ @State private var webAuthenticationSessionCallbackURL: URL? = nil
+
+ private var urlIsInvalid: Bool {
+ (webAuthenticationSessionOptions.url == nil) || !["http", "https"].contains(webAuthenticationSessionOptions.url?.scheme)
+ }
+
+ var body: some View {
+ VStack(alignment: .trailing) {
+ GroupBox(label: Text("WebAuthenticationSession")) {
+ VStack(alignment: .preferenceLabel) {
+ HStack {
+ Text("URL:")
+ TextField(gitHubAuthorizationURLString, text: $webAuthenticationSessionOptions.urlString)
+ .frame(maxWidth: 240)
+ .alignmentGuide(.preferenceLabel, computeValue: { $0[.leading] })
+ }
+ HStack {
+ Text("Callback URL Scheme:")
+ TextField(gitHubAuthorizationURLString, text: $webAuthenticationSessionOptions.callbackURLScheme)
+ .frame(maxWidth: 240)
+ .alignmentGuide(.preferenceLabel, computeValue: { $0[.leading] })
+ }
+ HStack {
+ Text("Modifiers:")
+ Toggle("Ephemeral Session", isOn: $webAuthenticationSessionOptions.prefersEphemeralWebBrowserSession)
+ .alignmentGuide(.preferenceLabel, computeValue: { $0[.leading] })
+ }
+ Spacer()
+ }
+ .padding()
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+ Button(action: { showingWebAuthenticationSession = true }) {
+ Text("Start Session")
+ }
+ .keyboardShortcut(.defaultAction)
+ .disabled(urlIsInvalid)
+ // Capture `webAuthenticationSessionOptions` to fix an issue
+ // where SwiftUI doesn't pass the latest value to the modifier.
+ // https://developer.apple.com/documentation/swiftui/view/onchange(of:perform:)
+ .webAuthenticationSession(
+ isPresented: $showingWebAuthenticationSession
+ ) { [webAuthenticationSessionOptions] in
+ WebAuthenticationSession(
+ url: webAuthenticationSessionOptions.url!,
+ callbackURLScheme: webAuthenticationSessionOptions.callbackURLScheme
+ ) { callbackURL, error in
+ webAuthenticationSessionCallbackURL = callbackURL
+ }
+ .prefersEphemeralWebBrowserSession(webAuthenticationSessionOptions.prefersEphemeralWebBrowserSession)
+ }
+ .alert(item: $webAuthenticationSessionCallbackURL) { callbackURL in
+ Alert(
+ title: Text("Session Completed with Callback URL"),
+ message: Text(callbackURL.absoluteString),
+ dismissButton: nil
+ )
+ }
+ }
+ .padding()
+ .frame(width: 480, height: 320)
+ .toolbar {
+ ToolbarItem(placement: .automatic) {
+ Spacer()
+ }
+ }
+ }
+}
+
+struct RootView_Previews: PreviewProvider {
+ static var previews: some View {
+ RootView()
+ }
+}
diff --git a/Demo/watchOS Extension/Info.plist b/Demo/watchOS Extension/Info.plist
new file mode 100644
index 0000000..a5c543b
--- /dev/null
+++ b/Demo/watchOS Extension/Info.plist
@@ -0,0 +1,36 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ BetterSafariViewDemo Extension
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ NSExtension
+
+ NSExtensionAttributes
+
+ WKAppBundleIdentifier
+ com.stleam.BetterSafariViewDemo
+
+ NSExtensionPointIdentifier
+ com.apple.watchkit
+
+ WKWatchOnly
+
+
+
diff --git a/Demo/watchOS Extension/RootView.swift b/Demo/watchOS Extension/RootView.swift
new file mode 100644
index 0000000..7259ed5
--- /dev/null
+++ b/Demo/watchOS Extension/RootView.swift
@@ -0,0 +1,52 @@
+import SwiftUI
+import BetterSafariView
+
+struct RootView: View {
+
+ @State private var webAuthenticationSessionOptions = WebAuthenticationSessionOptions()
+ @State private var showingWebAuthenticationSession = false
+ @State private var showingWebAuthenticationSessionOptionsForm = false
+ @State private var webAuthenticationSessionCallbackURL: URL? = nil
+
+ var body: some View {
+ NavigationView {
+ List {
+ Section(header: Text("WebAuthenticationSession").textCase(nil)) {
+ Button(action: { showingWebAuthenticationSession = true }) {
+ Text("Start Session")
+ }
+ .webAuthenticationSession(isPresented: $showingWebAuthenticationSession) {
+ WebAuthenticationSession(
+ url: webAuthenticationSessionOptions.url!,
+ callbackURLScheme: webAuthenticationSessionOptions.callbackURLScheme
+ ) { callbackURL, error in
+ webAuthenticationSessionCallbackURL = callbackURL
+ }
+ .prefersEphemeralWebBrowserSession(webAuthenticationSessionOptions.prefersEphemeralWebBrowserSession)
+ }
+ .alert(item: $webAuthenticationSessionCallbackURL) { callbackURL in
+ Alert(
+ title: Text("Session Completed with Callback URL"),
+ message: Text(callbackURL.absoluteString),
+ dismissButton: nil
+ )
+ }
+
+ Button(action: { showingWebAuthenticationSessionOptionsForm = true }) {
+ Text("Options")
+ }
+ .sheet(isPresented: $showingWebAuthenticationSessionOptionsForm) {
+ WebAuthenticationSessionOptionsForm(options: $webAuthenticationSessionOptions)
+ }
+ }
+ }
+ .navigationTitle(Text("BetterSafari"))
+ }
+ }
+}
+
+struct RootView_Previews: PreviewProvider {
+ static var previews: some View {
+ RootView()
+ }
+}
diff --git a/Demo/watchOS/Info.plist b/Demo/watchOS/Info.plist
new file mode 100644
index 0000000..9b81e1f
--- /dev/null
+++ b/Demo/watchOS/Info.plist
@@ -0,0 +1,31 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ BetterSafari
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+
+ WKWatchKitApp
+
+
+
diff --git a/Docs/Images/BetterSafariViewDemo-RootView.png b/Docs/Images/BetterSafariViewDemo-iOS.png
similarity index 100%
rename from Docs/Images/BetterSafariViewDemo-RootView.png
rename to Docs/Images/BetterSafariViewDemo-iOS.png
diff --git a/Docs/Images/BetterSafariViewDemo-macOS.png b/Docs/Images/BetterSafariViewDemo-macOS.png
new file mode 100644
index 0000000..a4a8105
Binary files /dev/null and b/Docs/Images/BetterSafariViewDemo-macOS.png differ
diff --git a/Docs/Images/BetterSafariViewDemo-watchOS.png b/Docs/Images/BetterSafariViewDemo-watchOS.png
new file mode 100644
index 0000000..6d0cd60
Binary files /dev/null and b/Docs/Images/BetterSafariViewDemo-watchOS.png differ
diff --git a/Package.swift b/Package.swift
index a97cca1..2ff047a 100644
--- a/Package.swift
+++ b/Package.swift
@@ -4,7 +4,7 @@ import PackageDescription
let package = Package(
name: "BetterSafariView",
- platforms: [.iOS(.v13)],
+ platforms: [.iOS(.v13), .macOS(.v10_15), .watchOS("6.2")],
products: [
.library(name: "BetterSafariView", targets: ["BetterSafariView"])
],
diff --git a/README.md b/README.md
index 2afa331..9a70950 100644
--- a/README.md
+++ b/README.md
@@ -5,22 +5,29 @@
-
+
-
+
-
+
+
+
+
+
+
+
+
-
+
-
+
-
+
@@ -31,11 +38,11 @@ A better way to present a SFSafariViewController or start a ASWebAuthenticationS
## Contents
- [Motivation](#motivation)
+- [Requirements](#requirements)
- [Usage](#usage)
- [SafariView](#safariview)
- [WebAuthenticationSession](#webauthenticationsession)
- [Known Issues](#known-issues)
-- [Requirements](#requirements)
- [Installation](#installation)
- [Swift Package Manager](#swift-package-manager)
- [Xcode](#xcode)
@@ -53,19 +60,24 @@ However, there’s a problem in this approach: it can’t present the `SFSafariV
`BetterSafariView` clearly achieves this goal by hosting a simple `UIViewController` to present a `SFSafariViewController` as a view’s background. In this way, a [`ASWebAuthenticationSession`](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) is also able to be started without any issue in SwiftUI.
+## Requirements
+- Xcode 11.0+
+- Swift 5.1+
+
+#### SafariView
+- iOS 13.0+
+- Mac Catalyst 13.0+
+
+#### WebAuthenticationSession
+- iOS 13.0+
+- Mac Catalyst 13.0+
+- macOS 10.15+
+- watchOS 6.2+
+
## Usage
With the following modifiers, you can use it in a similar way to present a sheet.
### SafariView
-#### Modifiers
-```swift
-.safariView(isPresented:onDismiss:content:)
-```
-
-```swift
-.safariView(item:onDismiss:content:)
-```
-
#### Example
```swift
import SwiftUI
@@ -97,16 +109,81 @@ struct ContentView: View {
}
```
-### WebAuthenticationSession
-#### Modifiers
+#### `View` Modifiers
+
+safariView(isPresented:onDismiss:content:)
+
+```swift
+/// Presents a Safari view when a given condition is true.
+func safariView(
+ isPresented: Binding,
+ onDismiss: (() -> Void)? = nil,
+ content: @escaping () -> SafariView
+) -> some View
+```
+
+
+
+safariView(item:onDismiss:content:)
+
+```swift
+/// Presents a Safari view using the given item as a data source for the `SafariView` to present.
+func safariView(
+ item: Binding- ,
+ onDismiss: (() -> Void)? = nil,
+ content: @escaping (Item) -> SafariView
+) -> some View
+```
+
+
+#### `SafariView` Initializers
+
+init(url:)
+
+```swift
+/// Creates a Safari view that loads the specified URL.
+init(url: URL)
+```
+
+
+
+init(url:configuration:)
+
+```swift
+/// Creates and configures a Safari view that loads the specified URL.
+init(url: URL, configuration: SafariView.Configuration)
+```
+
+
+#### `SafariView` Modifiers
+
+preferredBarAccentColor(_:)
+
```swift
-.webAuthenticationSession(isPresented:content:)
+/// Sets the accent color for the background of the navigation bar and the toolbar.
+func preferredBarAccentColor(_ color: Color?) -> SafariView
```
+
+
+
+preferredControlAccentColor(_:)
```swift
-.webAuthenticationSession(item:content:)
+/// Sets the accent color for the control buttons on the navigation bar and the toolbar.
+func preferredControlAccentColor(_ color: Color?) -> SafariView
```
+
+
+
+dismissButtonStyle(_:)
+```swift
+/// Sets the style of dismiss button to use in the navigation bar to close `SafariView`.
+func dismissButtonStyle(_ style: SafariView.DismissButtonStyle) -> SafariView
+```
+
+
+### WebAuthenticationSession
#### Example
```swift
import SwiftUI
@@ -135,19 +212,77 @@ struct ContentView: View {
}
```
+#### `View` Modifiers
+
+webAuthenticationSession(isPresented:content:)
+
+```swift
+/// Starts a web authentication session when a given condition is true.
+func webAuthenticationSession(
+ isPresented: Binding,
+ content: @escaping () -> WebAuthenticationSession
+) -> some View
+```
+
+
+
+webAuthenticationSession(item:content:)
+
+```swift
+/// Starts a web authentication session using the given item as a data source for the `WebAuthenticationSession` to start.
+func webAuthenticationSession(
+ item: Binding- ,
+ content: @escaping (Item) -> WebAuthenticationSession
+) -> some View
+```
+
+
+#### `WebAuthenticationSession` Initializers
+
+init(url:callbackURLScheme:completionHandler:)
+
+```swift
+/// Creates a web authentication session instance.
+init(
+ url: URL,
+ callbackURLScheme: String?,
+ completionHandler: @escaping (URL?, Error?) -> Void
+)
+```
+
+
+
+init(url:callbackURLScheme:onCompletion:)
+
+```swift
+/// Creates a web authentication session instance.
+init(
+ url: URL,
+ callbackURLScheme: String?,
+ onCompletion: @escaping (Result) -> Void
+)
+```
+
+
+#### `WebAuthenticationSession` Modifier
+
+prefersEphemeralWebBrowserSession(_:)
+
+```swift
+/// Configures whether the session should ask the browser for a private authentication session.
+func prefersEphemeralWebBrowserSession(_ prefersEphemeralWebBrowserSession: Bool) -> WebAuthenticationSession
+```
+
+
## Known Issues
- In `.webAuthenticationSession(item:content:)` modifier, the functionality that replaces a session on the `item`'s identity change is not implemented, as there is no non-hacky way to be notified when the session's dismissal animation is completed.
-## Requirements
-- Swift 5.1+
-- iOS 13.0+
-
## Installation
### Swift Package Manager
Add the following line to the `dependencies` in your [`Package.swift`](https://developer.apple.com/documentation/swift_packages/package) file:
```swift
-.package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.2.2"))
+.package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.3.0"))
```
Next, add `BetterSafariView` as a dependency for your targets:
@@ -166,7 +301,7 @@ import PackageDescription
let package = Package(
name: "MyPackage",
dependencies: [
- .package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.2.2"))
+ .package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.3.0"))
],
targets: [
.target(name: "MyTarget", dependencies: ["BetterSafariView"])
@@ -185,11 +320,15 @@ https://github.com/stleamist/BetterSafariView.git
For more details, see [Adding Package Dependencies to Your App](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app).
## Demo
-
+
+
+
+
+
-You can compare the behavior of BetterSafariView with the other ways above in the demo project. Check out the demo app by opening BetterSafariView.xcworkspace.
+You can see how it works on each platform and compare it with the other naive implementations in the demo project. Check out the demo app by opening BetterSafariView.xcworkspace.
-**NOTE:** This demo project is designed for iOS 14.0+, though the package is compatible with iOS 13.0+.
+**NOTE:** This demo project is available for iOS 14.0+, macOS 11.0+, and watchOS 7.0+, while the package is compatible with iOS 13.0+, macOS 10.15+, and watchOS 6.2+.
## License
BetterSafariView is released under the MIT license. See [LICENSE](/LICENSE) for details.
diff --git a/Sources/BetterSafariView/SafariView/SafariView+View.swift b/Sources/BetterSafariView/SafariView/SafariView+View.swift
index 747b8eb..20ea602 100644
--- a/Sources/BetterSafariView/SafariView/SafariView+View.swift
+++ b/Sources/BetterSafariView/SafariView/SafariView+View.swift
@@ -1,10 +1,13 @@
+#if os(iOS)
+
import SwiftUI
import SafariServices
// A `View` conformance for the advanced usage.
extension SafariView: View {
- #if compiler(>=5.3)
+ // There is a bug on Xcode 12.0 (Swift 5.3.0) where `ignoresSafeArea(_:edges:)` is missing for Mac Catalyst target.
+ #if compiler(>=5.3.1) || (compiler(>=5.3) && !targetEnvironment(macCatalyst))
// To apply `ignoresSafeArea(_:edges:)` modifier to the `UIViewRepresentable`,
// define nested `Representable` struct and wrap it with `View`.
@@ -76,3 +79,5 @@ extension SafariView {
}
}
}
+
+#endif
diff --git a/Sources/BetterSafariView/SafariView/SafariView.swift b/Sources/BetterSafariView/SafariView/SafariView.swift
index 912488b..10f9052 100644
--- a/Sources/BetterSafariView/SafariView/SafariView.swift
+++ b/Sources/BetterSafariView/SafariView/SafariView.swift
@@ -1,3 +1,5 @@
+#if os(iOS)
+
import SwiftUI
import SafariServices
@@ -27,6 +29,7 @@ import SafariServices
///
public struct SafariView {
+ /// A configuration object that defines how a Safari view controller should be initialized.
public typealias Configuration = SFSafariViewController.Configuration
public typealias DismissButtonStyle = SFSafariViewController.DismissButtonStyle
@@ -45,7 +48,7 @@ public struct SafariView {
/// - url: The URL to navigate to. The URL must use the http or https scheme.
/// - configuration: The configuration for the new view controller.
///
- public init(url: URL, configuration: SFSafariViewController.Configuration = .init()) {
+ public init(url: URL, configuration: Configuration = .init()) {
self.url = url
self.configuration = configuration
}
@@ -56,7 +59,8 @@ public struct SafariView {
var preferredControlTintColor: UIColor?
var dismissButtonStyle: DismissButtonStyle = .done
- #if compiler(>=5.3)
+ // There is a bug on Xcode 12.0 (Swift 5.3.0) where `UIColor.init(_ color: Color)` is missing for Mac Catalyst target.
+ #if compiler(>=5.3.1) || (compiler(>=5.3) && !targetEnvironment(macCatalyst))
/// Sets the accent color for the background of the navigation bar and the toolbar.
///
@@ -161,3 +165,5 @@ public extension SafariView.Configuration {
self.barCollapsingEnabled = barCollapsingEnabled
}
}
+
+#endif
diff --git a/Sources/BetterSafariView/SafariView/SafariViewPresentationModifier.swift b/Sources/BetterSafariView/SafariView/SafariViewPresentationModifier.swift
index ce2dff2..8adca32 100644
--- a/Sources/BetterSafariView/SafariView/SafariViewPresentationModifier.swift
+++ b/Sources/BetterSafariView/SafariView/SafariViewPresentationModifier.swift
@@ -1,3 +1,5 @@
+#if os(iOS)
+
import SwiftUI
struct SafariViewPresentationModifier: ViewModifier {
@@ -95,3 +97,5 @@ public extension View {
)
}
}
+
+#endif
diff --git a/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift b/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift
index 48e91cb..c024dfc 100644
--- a/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift
+++ b/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift
@@ -1,3 +1,5 @@
+#if os(iOS)
+
import SwiftUI
import SafariServices
@@ -126,3 +128,5 @@ extension SafariViewPresenter {
}
}
}
+
+#endif
diff --git a/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresentationModifier.swift b/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresentationModifier.swift
index 000fa56..e4f0023 100644
--- a/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresentationModifier.swift
+++ b/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresentationModifier.swift
@@ -1,3 +1,5 @@
+#if os(iOS) || os(macOS) || os(watchOS)
+
import SwiftUI
struct WebAuthenticationPresentationModifier: ViewModifier {
@@ -91,3 +93,5 @@ public extension View {
)
}
}
+
+#endif
diff --git a/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresenter.swift b/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresenter.swift
index 277eeb2..1975e33 100644
--- a/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresenter.swift
+++ b/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresenter.swift
@@ -1,31 +1,86 @@
+#if os(iOS) || os(macOS) || os(watchOS)
+
import SwiftUI
-import SafariServices
import AuthenticationServices
+#if os(iOS)
+import SafariServices
+#endif
-struct WebAuthenticationPresenter: UIViewControllerRepresentable {
+#if os(iOS)
+typealias ConcreteViewController = UIViewController
+typealias ViewController = UIViewController
+typealias ViewControllerRepresentable = UIViewControllerRepresentable
+#elseif os(macOS)
+typealias ConcreteViewController = NSTabViewController
+typealias ViewController = NSViewController
+typealias ViewControllerRepresentable = NSViewControllerRepresentable
+#elseif os(watchOS)
+// Use `WKInterfaceInlineMovie` as a concrete interface objct type,
+// since there is no public initializer for `WKInterfaceObject`.
+typealias ConcreteViewController = WKInterfaceInlineMovie
+typealias ViewController = WKInterfaceObject
+typealias ViewControllerRepresentable = WKInterfaceObjectRepresentable
+#endif
+
+struct WebAuthenticationPresenter: ViewControllerRepresentable {
// MARK: Representation
@Binding var item: Item?
var representationBuilder: (Item) -> WebAuthenticationSession
- // MARK: UIViewControllerRepresentable
+ // MARK: ViewControllerRepresentable
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
- func makeUIViewController(context: Context) -> UIViewController {
- return context.coordinator.uiViewController
+ #if os(iOS)
+
+ func makeUIViewController(context: Context) -> ViewController {
+ return makeViewController(context: context)
}
- func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
+ func updateUIViewController(_ uiViewController: ViewController, context: Context) {
+
+ updateViewController(uiViewController, context: context)
// To set a delegate for the presentation controller of an `SFAuthenticationViewController` as soon as possible,
// check the view controller presented by `uiViewController` then set it as a delegate on every view updates.
// INFO: `SFAuthenticationViewController` is a private subclass of `SFSafariViewController`.
- context.coordinator.setInteractiveDismissalDelegateIfPossible()
-
+ guard #available(iOS 14.0, *) else {
+ context.coordinator.setInteractiveDismissalDelegateIfPossible()
+ return
+ }
+ }
+
+ #elseif os(macOS)
+
+ func makeNSViewController(context: Context) -> ViewController {
+ return makeViewController(context: context)
+ }
+
+ func updateNSViewController(_ nsViewController: ViewController, context: Context) {
+ updateViewController(nsViewController, context: context)
+ }
+
+ #elseif os(watchOS)
+
+ func makeWKInterfaceObject(context: Context) -> ViewController {
+ return makeViewController(context: context)
+ }
+
+ func updateWKInterfaceObject(_ wkInterfaceObject: ViewController, context: Context) {
+ updateViewController(wkInterfaceObject, context: context)
+ }
+
+ #endif
+
+ private func makeViewController(context: Context) -> ViewController {
+ return context.coordinator.viewController
+ }
+
+ private func updateViewController(_ viewController: ViewController, context: Context) {
// Keep the coordinator updated with a new presenter struct.
context.coordinator.parent = self
context.coordinator.item = item
@@ -34,7 +89,7 @@ struct WebAuthenticationPresenter: UIViewControllerRepresent
extension WebAuthenticationPresenter {
- class Coordinator: NSObject, ASWebAuthenticationPresentationContextProviding, UIAdaptivePresentationControllerDelegate {
+ class Coordinator: NSObject {
// MARK: Parent Copying
@@ -46,7 +101,7 @@ extension WebAuthenticationPresenter {
// MARK: View Controller Holding
- let uiViewController = UIViewController()
+ let viewController = ConcreteViewController()
private var session: ASWebAuthenticationSession?
// MARK: Item Handling
@@ -84,7 +139,11 @@ extension WebAuthenticationPresenter {
representation.completionHandler(callbackURL, error)
}
)
- session.presentationContextProvider = self
+
+ #if os(iOS) || os(macOS)
+ session.presentationContextProvider = presentationContextProvider
+ #endif
+
representation.applyModification(to: session)
self.session = session
@@ -102,32 +161,71 @@ extension WebAuthenticationPresenter {
parent.item = nil
}
- // MARK: ASWebAuthenticationPresentationContextProviding
+ #if os(iOS) || os(macOS)
+
+ // MARK: PresentationContextProvider
// INFO: `ASWebAuthenticationPresentationContextProviding` provides an window
// to present an `SFAuthenticationViewController`, and usually presents the `SFAuthenticationViewController`
// by calling `present(_:animated:completion:)` method from a root view controller of the window.
- func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
- return uiViewController.view.window!
+ private lazy var presentationContextProvider = PresentationContextProvider(coordinator: self)
+
+ class PresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
+
+ weak var coordinator: WebAuthenticationPresenter.Coordinator?
+
+ init(coordinator: WebAuthenticationPresenter.Coordinator) {
+ self.coordinator = coordinator
+ }
+
+ // MARK: ASWebAuthenticationPresentationContextProviding
+
+ func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
+ return coordinator!.viewController.view.window!
+ }
}
- // MARK: UIAdaptivePresentationControllerDelegate
+ #endif
+
+ #if os(iOS)
+
+ // MARK: InteractiveDismissalDelegate
// There is a problem that `item` is not set to `nil` after the sheet is dismissed with pulling down
- // because the completion handler is not called on this case due to a system bug.
- // To resolve this issue, set `Coordinator` as a presentation controller delegate of `SFAuthenticationViewController`
- // so that ensures the completion handler is always called.
+ // because the completion handler is not called on this case due to a system bug on iOS 13.
+ // To resolve this issue, set `interactiveDismissalDelegate` as a presentation controller delegate of
+ // `SFAuthenticationViewController` so that ensures the completion handler is always called.
+
+ @available(iOS, introduced: 13.0, deprecated: 14.0)
+ private lazy var interactiveDismissalDelegate = InteractiveDismissalDelegate(coordinator: self)
+ @available(iOS, introduced: 13.0, deprecated: 14.0)
func setInteractiveDismissalDelegateIfPossible() {
- guard let safariViewController = uiViewController.presentedViewController as? SFSafariViewController else {
+ guard let safariViewController = viewController.presentedViewController as? SFSafariViewController else {
return
}
- safariViewController.presentationController?.delegate = self
+ safariViewController.presentationController?.delegate = interactiveDismissalDelegate
}
- func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
- resetItemBinding()
+ @available(iOS, introduced: 13.0, deprecated: 14.0)
+ class InteractiveDismissalDelegate: NSObject, UIAdaptivePresentationControllerDelegate {
+
+ weak var coordinator: WebAuthenticationPresenter.Coordinator?
+
+ init(coordinator: WebAuthenticationPresenter.Coordinator) {
+ self.coordinator = coordinator
+ }
+
+ // MARK: UIAdaptivePresentationControllerDelegate
+
+ func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
+ coordinator?.resetItemBinding()
+ }
}
+
+ #endif
}
}
+
+#endif
diff --git a/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationSession.swift b/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationSession.swift
index d0e7d84..4c8b9d0 100644
--- a/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationSession.swift
+++ b/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationSession.swift
@@ -1,5 +1,6 @@
+#if os(iOS) || os(macOS) || os(watchOS)
+
import SwiftUI
-import SafariServices
import AuthenticationServices
// Used for getting a public completion handler to inject an assignment that sets `item` to `nil`.
@@ -97,5 +98,10 @@ public struct WebAuthenticationSession {
}
}
+/// Errors that a web authentication session can generate.
public typealias WebAuthenticationSessionError = ASWebAuthenticationSessionError
+
+/// The error domain for a web authentication session.
public let WebAuthenticationSessionErrorDomain = ASWebAuthenticationSessionErrorDomain
+
+#endif