A better way to present a SFSafariViewController or start a ASWebAuthenticationSession in SwiftUI.

Overview

version Swift: 5.1+ iOS: 13.0+ macOS: 10.15+ watchOS: 6.2+
Build on Xcode SwiftPM: compatible license contact: @stleamist

BetterSafariView

A better way to present a SFSafariViewController or start a ASWebAuthenticationSession in SwiftUI.

Contents

Motivation

SwiftUI is a strong, intuitive way to build user interfaces, but was released with some part of existing elements missing. One example of those missing elements is the SFSafariViewController.

Fortunately, Apple provides a way to wrap UIKit elements into SwiftUI views. A common approach to place the SFSafariViewController inside SwiftUI is to create a simple view representing a SFSafariViewController, then present it with a sheet(isPresented:onDismiss:content:) modifier or a NavigationLink button (See RootView.swift in the demo project).

However, there’s a problem in this approach: it can’t present the SFSafariViewController with its default presentation style — a push transition covers full screen. A sheet modifier can present the view only in a modal sheet, and a navigation link shows the two navigation bars at the top so we have to deal with them. This comes down to the conclusion that there’s no option to present it the right way except for using present(_:animated:completion:) method of a UIViewController instance, but it is prohibited and not a good design to access the UIHostingController directly from the SwiftUI view.

BetterSafariView clearly achieves this goal by hosting a simple UIViewController to present a SFSafariViewController as a view’s background. In this way, a 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

Example

import SwiftUI
import BetterSafariView

struct ContentView: View {
    
    @State private var presentingSafariView = false
    
    var body: some View {
        Button(action: {
            self.presentingSafariView = true
        }) {
            Text("Present SafariView")
        }
        .safariView(isPresented: $presentingSafariView) {
            SafariView(
                url: URL(string: "https://github.com/")!,
                configuration: SafariView.Configuration(
                    entersReaderIfAvailable: false,
                    barCollapsingEnabled: true
                )
            )
            .preferredBarAccentColor(.clear)
            .preferredControlAccentColor(.accentColor)
            .dismissButtonStyle(.done)
        }
    }
}

View Modifiers

safariView(isPresented:onDismiss:content:)
/// Presents a Safari view when a given condition is true.
func safariView(
    isPresented: Binding<Bool>,
    onDismiss: (() -> Void)? = nil,
    content: @escaping () -> SafariView
) -> some View
safariView(item:onDismiss:content:)
/// Presents a Safari view using the given item as a data source for the `SafariView` to present.
func safariView<Item: Identifiable>(
    item: Binding<Item?>,
    onDismiss: (() -> Void)? = nil,
    content: @escaping (Item) -> SafariView
) -> some View

SafariView Initializers

init(url:)
/// Creates a Safari view that loads the specified URL.
init(url: URL)
init(url:configuration:)
/// Creates and configures a Safari view that loads the specified URL.
init(url: URL, configuration: SafariView.Configuration)

SafariView Modifiers

preferredBarAccentColor(_:)
/// Sets the accent color for the background of the navigation bar and the toolbar.
func preferredBarAccentColor(_ color: Color?) -> SafariView
preferredControlAccentColor(_:)
/// Sets the accent color for the control buttons on the navigation bar and the toolbar.
func preferredControlAccentColor(_ color: Color?) -> SafariView
dismissButtonStyle(_:)
/// Sets the style of dismiss button to use in the navigation bar to close `SafariView`.
func dismissButtonStyle(_ style: SafariView.DismissButtonStyle) -> SafariView

WebAuthenticationSession

Example

import SwiftUI
import BetterSafariView

struct ContentView: View {
    
    @State private var startingWebAuthenticationSession = false
    
    var body: some View {
        Button(action: {
            self.startingWebAuthenticationSession = true
        }) {
            Text("Start WebAuthenticationSession")
        }
        .webAuthenticationSession(isPresented: $startingWebAuthenticationSession) {
            WebAuthenticationSession(
                url: URL(string: "https://github.com/login/oauth/authorize")!,
                callbackURLScheme: "github"
            ) { callbackURL, error in
                print(callbackURL, error)
            }
            .prefersEphemeralWebBrowserSession(false)
        }
    }
}

View Modifiers

webAuthenticationSession(isPresented:content:)
/// Starts a web authentication session when a given condition is true.
func webAuthenticationSession(
    isPresented: Binding<Bool>,
    content: @escaping () -> WebAuthenticationSession
) -> some View
webAuthenticationSession(item:content:)
/// Starts a web authentication session using the given item as a data source for the `WebAuthenticationSession` to start.
func webAuthenticationSession<Item: Identifiable>(
    item: Binding<Item?>,
    content: @escaping (Item) -> WebAuthenticationSession
) -> some View

WebAuthenticationSession Initializers

init(url:callbackURLScheme:completionHandler:)
/// Creates a web authentication session instance.
init(
    url: URL,
    callbackURLScheme: String?,
    completionHandler: @escaping (URL?, Error?) -> Void
)
init(url:callbackURLScheme:onCompletion:)
/// Creates a web authentication session instance.
init(
    url: URL,
    callbackURLScheme: String?,
    onCompletion: @escaping (Result<URL, Error>) -> Void
)

WebAuthenticationSession Modifier

prefersEphemeralWebBrowserSession(_:)
/// 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.

Installation

Swift Package Manager

Add the following line to the dependencies in your Package.swift file:

.package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.3.1"))

Next, add BetterSafariView as a dependency for your targets:

.target(name: "MyTarget", dependencies: ["BetterSafariView"])

Your completed description may look like this:

// swift-tools-version:5.1

import PackageDescription

let package = Package(
    name: "MyPackage",
    dependencies: [
        .package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.3.1"))
    ],
    targets: [
        .target(name: "MyTarget", dependencies: ["BetterSafariView"])
    ]
)

Xcode

Select File > Swift Packages > Add Package Dependency, then enter the following URL:

https://github.com/stleamist/BetterSafariView.git

For more details, see Adding Package Dependencies to Your App.

Demo

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 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 for details.

Comments
  • Safari View Controller not presented

    Safari View Controller not presented

    I have a .safariView attached to a NavigationView inside a sheet presented by another sheet. When I set the item for the safari view to a non-nil value, this error message is printed out and nothing happens on the screen. How can I fix this?

    Attempt to present <SFSafariViewController: 0x10a020000>
    on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x104c0aad0> 
    (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x104c0aad0>)
    which is already presenting <_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x108305f70>
    
    bug 
    opened by j-f1 6
  • Example code does not compile

    Example code does not compile

    I get the following errors when attempting to compile.

    • Argument passed to call that takes no arguments
    • Type 'SafariView' has no member 'Configuration'
    • Cannot infer contextual base in reference to member 'systemBlue'
    • Cannot infer contextual base in reference to member 'done'

    I'm running:

    • Apple Swift version 5.2.4 Xcode: 11.7
    import SwiftUI
    import BetterSafariView
    
    struct SafariView: View {
    
        @State private var presentingSafariView = false
    
        var body: some View {
            Button(action: {
                self.presentingSafariView = true
            }) {
                Text("Present SafariView")
            }
            .safariView(isPresented: $presentingSafariView) {
                SafariView(
                    url: URL(string: "https://github.com/")!,
                    configuration: SafariView.Configuration(
                        entersReaderIfAvailable: false,
                        barCollapsingEnabled: true
                    )
                )
                .preferredControlTintColor(.systemBlue)
                .dismissButtonStyle(.done)
            }
        }
    }
    
    struct SafariView_Previews: PreviewProvider {
        static var previews: some View {
            SafariView()
        }
    }
    
    
    help wanted 
    opened by arbyruns 6
  • Configuration on NaiveSafariView

    Configuration on NaiveSafariView

    It seems there is no way to configure NaiveSafariView at the moment. For example to add entersReaderIfAvailable: true to a sheet that has NaiveSafariView as its content.

    enhancement 
    opened by yixe 4
  • Allows presenting SFSafariViewController when the rootViewController is already presenting another view controller

    Allows presenting SFSafariViewController when the rootViewController is already presenting another view controller

    The main commit fixes the issue reported at https://github.com/stleamist/BetterSafariView/issues/8

    The other commit fixes a weird case (difficult to replicate) when dismissSafariViewController is called but uiViewController is not presenting.

    bug 
    opened by boherna 3
  • List of URL Items

    List of URL Items

    I've been running into trouble when trying to create a list of items that can be tapped to redirect to the SafariView.

    I've tried to use the NaiveSafariView as shown in the demo doesn't give the behavior that I'd like (squished under the navigation view and tab bar)

    The .safariView modifier is great, but not scaleable to a list of items.

    Attempts to resolve:

    1. One work around would be to create a bunch of $isPresented State variables and assign each item to one of them, but it is not efficient or scaleable.

    2. Another work around is to just assign all of them to the same $isPresented State variable, but this poses a different problem which is that the Url of the webpage must be declared in advance and passed as context to the .safariView modifier, resulting in all items to return the same webpage. In this scenario the swipe to dismiss also causes a bug where it dismisses, goes back to the webpage, then dismisses again.

    3. The last work around tried was to assign each row in the List its own State variable and bind the .safariView to that. But when clicked on that it sends the view into an infinite loop of popping and pushing the webpage that can only be stopped by pressing the "Done" button in the top left corner.

    The only solution found currently is to use a navigation link to push a DetailView where the user is able to click "go to webpage" which then pushes to the webpage. This is not preferred as being able to go to the webpage from the original list of items is optimal.

    Any ideas are appreciated.

    bug enhancement 
    opened by cpdunphy 3
  • Errors when adding swift package via xcode

    Errors when adding swift package via xcode

    When adding the package as a dependency from xcode's add swift package dependency, I keep getting errors of this sort like it's not actually linking against the package properly: Cannot convert value of type 'Example_App_Name.SafariView' to closure result type 'BetterSafariView.SafariView'

    I've done the old derived data dance and still no luck, yet the demo project works fine.

    Update: I had been developing an app with xcode 12 beta, switched to xcode 11 & iOS 13 so I could get a testflight build uploaded, and that's when I started seeing breakage. I just opened the demo app in xcode 11 and changed the deployment target to ios 13 and I see that there are quite a few errors, so now it makes a lot more sense.

    I originally thought it was maybe a problem with the way I included the dep, or the SafariView.Configuration convenience init, but it looks like there are other issues.

    help wanted 
    opened by Andrewpk 2
  • Expose item to onDismiss closure

    Expose item to onDismiss closure

    Awesome work with v2, congrats :)

    It would be useful if the presented item is also exposed to the onDismiss closure, to adjust "cleanup" actions based on the item that was visible. Think of tracking each item that was opened, …

    struct CustomItem {
     var id: String
      var name: String
      var url: URL?
      //…
    }
    
    struct ArticleList : View {
      // …
      @State private var itemToShow: CustomItem? = nil
    
      var body: some View {
          List {
            ForEach(viewModel.feedItems) { item in
               Button(action: { itemToShow = item }) {
                   // …
              }
            }
          }.safariView(item: $itemToShow, onDismiss: { item in // <-- not possible at the moment
            print("Mark Item \(item.name) as read")
          }) { item in
                SafariView(
                  url: item.url,
                   configuration: SafariView.Configuration(
                    // …
                    ))
            }
      }
    
    opened by mlinzner 1
  • No such module 'BetterSafariView'

    No such module 'BetterSafariView'

    BetterSafariView has really messed up my project... can build but cant use editor and so many red errors.

    Could not find module 'BetterSafariView' for target 'arm64-apple-ios-simulator'; found: x86_64-apple-ios-simulator, x86_64

    bug help wanted 
    opened by RealTechyGod 1
  • Fixes Unexpectedly Found Nil When Unwrapping Optional in WebAuthentic…

    Fixes Unexpectedly Found Nil When Unwrapping Optional in WebAuthentic…

    …ationPresenter

    This pull request fixes the unexpectedly found nil when unwrapping optional by using ASPresentationAnchor() for iOS 13+ and MacOS 10.15+, otherwise defaults to original behavior.

    opened by exentrich 0
  • error: cannot find 'WebAuthenticationSessionOptions' in scope

    error: cannot find 'WebAuthenticationSessionOptions' in scope

    error: cannot find 'WebAuthenticationSessionOptions' in scope
        @State private var webAuthenticationSessionOptions =  WebAuthenticationSessionOptions()
                                                              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    /Users/brandon_kurtz/swiftauthexperiment/Sources/swiftauthexperimenttarg/main.swift:19:35: error: cannot find 'gitHubAuthorizationURLString' in scope
                            TextField(gitHubAuthorizationURLString, text: $webAuthenticationSessionOptions.urlString)
                                      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
    /Users/brandon_kurtz/swiftauthexperiment/Sources/swiftauthexperimenttarg/main.swift:25:35: error: cannot find 'gitHubAuthorizationURLString' in scope
                            TextField(gitHubAuthorizationURLString, text: $webAuthenticationSessionOptions.callbackURLScheme)
                                      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
    [1/2] Compiling swiftauthexperimenttarg main.swift
    

    I'm guessing that my personal Package.swift file is wrong in some way but would appreciate any advice if you can spot the problem:

    // swift-tools-version:5.5
    // The swift-tools-version declares the minimum version of Swift required to build this package.
    
    import PackageDescription
    
    let package = Package(
        name: "swiftauthexperiment",
        platforms: [
            .macOS(.v11)
        ],
        dependencies: [
            .package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.3.1"))
        ],
        targets: [
            .target(name: "swiftauthexperimenttarg", dependencies: ["BetterSafariView"])
        ]
    )
    
    opened by discentem 0
  • Support for system light mode.

    Support for system light mode.

    When using this package, I noticed it's always displayed the safari service view in dark mode.

    Is there a way to manually set it, or let it follows the system?

    opened by legolasW 0
  • Attempting to load the view of a view controller while it is deallocating is not allowed

    Attempting to load the view of a view controller while it is deallocating is not allowed

    Using the WebAuthenticationSession example, verbatim, from the README, on iOS 15.4.

    When the popup appears on the device saying "Test App" Wants to Use "github.com" to Sign In, press Cancel on device. The following warning is then produced in the Xcode console:

    2022-03-19 10:04:38.212806-0700 Test App[38555:3159860] [Warning] Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior (<SFAuthenticationViewController: 0x7f9a1481da00>)
    

    Ideas how to fix?

    opened by adriancable 0
  • How to load dynamic url

    How to load dynamic url

    initial var url = "https://www.github.com"

    after click a button or other event, url become another value url = "https://www.google.com"

    the SafariView how to load the lasted url

    opened by haolidong 1
  • Split into multiple packages.

    Split into multiple packages.

    Splitting into multiple packages allows users to import only the package they intend to use, which leads to smaller binary sizes. This is backwards compatible with no changes needed by end users.

    For users who may not be interested in say web auth sessions, they need only import SafariView.

    opened by ericlewis 0
  • Package does not have version tags

    Package does not have version tags

    I'm trying to use this package in Swift Playgrounds 4 and I get an error saying "package does not have version tags" when trying to add this package.

    opened by EmenezTech 1
Releases(v2.4.0)
  • v2.4.0(Sep 25, 2021)

    Changed

    • SafariViewPresenter and WebAuthenticationPresenter now conforms to UIViewRepresentable, instead of UIViewControllerRepresentable.

    Fixed

    • Fixed an issue where the SafariView is not presented on the multi-layered modal sheets (#20). Thanks @twodayslate!
    Source code(tar.gz)
    Source code(zip)
  • v2.3.1(Jan 19, 2021)

  • v2.3.0(Jan 19, 2021)

  • v2.2.2(Sep 18, 2020)

  • v2.2.1(Aug 26, 2020)

  • v2.2.0(Aug 26, 2020)

    Added

    • SafariView now conforms to View protocol, so it can be used even in the .sheet() or the .fullScreenCover() modifiers for the advanced usage.
    • Added accentColor(_:) modifier to SafariView as a convenience method of preferredControlAccentColor(_:).
    • Added a new initializer of WebAuthenticationSession where the onCompletion closure receives a Result instance, which contains either a URL or an Error.

    Fixed

    • Fixed typos on the markup.
    Source code(tar.gz)
    Source code(zip)
  • v2.1.0(Aug 24, 2020)

  • v2.0.1(Aug 22, 2020)

  • v2.0.0(Aug 16, 2020)

    Added

    • You can now authenticate a user through a web authentication session by using WebAuthenticationSession.
    • With the new SafariView representation and its modifiers, configurations and properties on SFSafariViewController also could be used.
    • Using safariView(isPresented:onDismiss:content:) modifier, actions could be performed when the Safari view dismisses.
    • Using safariView(item:onDismiss:content:) modifier, the Safari view could be replaced on the item's identity change.

    Changed

    • The package has been renamed to BetterSafariView from FullScreenSafariView.
    • safariView(isPresented:content:) modifier now gets a closure returning a SafariView representation instead of a URL instance.

    Fixed

    • Fixed an issue where the dismissed Safari view is presented and dismissed again on iOS 14.
    • Fixed an issue where page loading and parallel push animation are not working when a modifier is attached to the view in a List.
    • Improved stability during the SwiftUI view update process.
    Source code(tar.gz)
    Source code(zip)
  • v1.0.0(May 18, 2020)

Owner
Dongkyu Kim
iOS developer. Swift lover. Interested in UI design.
Dongkyu Kim
iOS custom controller used in Jobandtalent app to present new view controllers as cards

CardStackController iOS custom controller used in the Jobandtalent app to present new view controllers as cards. This controller behaves very similar

jobandtalent 527 Dec 15, 2022
UIViewController subclass to beautifully present news articles and blog posts.

LMArticleViewController This framework allows you to create Apple News-inspired UIViewControllers with ease. It is heavily inspired by MRArticleViewCo

Luca Mozzarelli 7 Feb 3, 2022
The Bloc Pattern is a way to separate UI and Logic in SwiftUI codes;

The Bloc Pattern is a way to separate UI and Logic in SwiftUI codes. The Bloc is like a state machine where it accepts an event and produce a state.

mehdi sohrabi 3 Apr 20, 2022
ToastSwiftUI-master - A simple way to show a toast or a popup in SwiftUI

ToastSwiftUI-master - A simple way to show a toast or a popup in SwiftUI

Kushal Shingote 2 May 25, 2022
A way to quickly add a notification badge icon to any view. Make any view of a full-fledged animated notification center.

BadgeHub A way to quickly add a notification badge icon to any view. Demo/Example For demo: $ pod try BadgeHub To run the example project, clone the r

Jogendra 772 Dec 28, 2022
Fashion is your helper to share and reuse UI styles in a Swifty way.

Fashion is your helper to share and reuse UI styles in a Swifty way. The main goal is not to style your native apps in CSS, but use a set

Vadym Markov 124 Nov 20, 2022
An easy way to add a shimmering effect to any view with just one line of code. It is useful as an unobtrusive loading indicator.

LoadingShimmer An easy way to add a shimmering effect to any view with just single line of code. It is useful as an unobtrusive loading indicator. Thi

Jogendra 1.4k Jan 4, 2023
☠️ An elegant way to show users that something is happening and also prepare them to which contents they are awaiting

Features • Guides • Installation • Usage • Miscellaneous • Contributing ?? README is available in other languages: ???? . ???? . ???? . ???? . ???? To

Juanpe Catalán 11.7k Jan 6, 2023
Custom emojis are a fun way to bring more life and customizability to your apps.

Custom emojis are a fun way to bring more life and customizability to your apps. They're available in some of the most popular apps, such as Slack, Di

Stream 244 Dec 11, 2022
Twinkle is a Swift and easy way to make any UIView in your iOS or tvOS app twinkle.

Twinkle ✨ Twinkle is a Swift and easy way to make any UIView in your iOS or tvOS app twinkle. This library creates several CAEmitterLayers and animate

patrick piemonte 600 Nov 24, 2022
List tree data souce to display hierachical data structures in lists-like way. It's UI agnostic, just like view-model and doesn't depend on UI framework

SwiftListTreeDataSource List tree data souce to display hierachical data structures in lists-like way. It's UI agnostic, just like view-model, so can

Dzmitry Antonenka 26 Nov 26, 2022
A simple way to hide the notch on the iPhone X

NotchKit NotchKit is a simple way to hide the notch on the iPhone X, and create a card-like interface for your apps. Inspired by this tweet from Sebas

Harshil Shah 1.8k Dec 5, 2022
CITreeView created to implement and maintain that wanted TreeView structures for IOS platforms easy way

CITreeView CITreeView created to implement and maintain that wanted TreeView structures for IOS platforms easy way. CITreeView provides endless treevi

Cenk Işık 126 May 28, 2022
A paging scroll view for SwiftUI, using internal SwiftUI components

PagingView A paging scroll view for SwiftUI, using internal SwiftUI components. This is basically the same as TabView in the paging mode with the inde

Eric Lewis 18 Dec 25, 2022
SwiftUI-Drawer - A bottom-up drawer in swiftUI

SwiftUI-Drawer A bottom-up drawer view. Contents Installation Examples Installat

Bruno Wide 9 Dec 29, 2022
SwiftUI-Margin adds a margin() viewModifier to a SwiftUI view.

SwiftUI-Margin adds a margin() viewModifier to a SwiftUI view. You will be able to layout the margins in a CSS/Flutter-like.

Masaaki Kakimoto(柿本匡章) 2 Jul 14, 2022
A number of preset loading indicators created with SwiftUI

ActivityIndicatorView A number of preset loading indicators created with SwiftUI We are a development agency building phenomenal apps. Usage Create an

Exyte 956 Dec 26, 2022
A SwiftUI Library for creating resizable partitions for View Content.

Partition Kit Recently Featured In Top 10 Trending Android and iOS Libraries in October and in 5 iOS libraries to enhance your app! What is PartitionK

Kieran Brown 230 Oct 27, 2022
A micro UIStackView convenience API inspired by SwiftUI

Stacks A micro UIStackView convenience API inspired by SwiftUI. let stack: UIView = .hStack(alignment: .center, margins: .all(16), [ .vStack(spaci

Alexander Grebenyuk 74 Jul 27, 2022