How do I use SFSafariViewController with SwiftUI?

SFSafariViewController is a UIKit component, hence you need to make it UIViewControllerRepresentable.

See Integrating SwiftUI WWDC 19 video for more details on how to bridge UIKit components to SwiftUI.

struct SafariView: UIViewControllerRepresentable {

    let url: URL

    func makeUIViewController(context: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
        return SFSafariViewController(url: url)
    }

    func updateUIViewController(_ uiViewController: SFSafariViewController,
                                context: UIViewControllerRepresentableContext<SafariView>) {

    }

}

A note of warning: SFSafariViewController is meant to be presented on top of another view controller, not pushed in a navigation stack.

It also has a navigation bar, meaning that if you push the view controller, you will see two navigation bars.

enter image description here

It seems to work - though it's glitchy - if presented modally.

struct ContentView : View {

    let url = URL(string: "https://www.google.com")!

    var body: some View {
        EmptyView()
        .presentation(Modal(SafariView(url:url)))
    }
}

It looks like this:

enter image description here

I suggest porting WKWebView to SwiftUI via the UIViewRepresentable protocol, and use it in its stead.


Sometimes the answer is to just not use SwiftUI! This is so well supported in UIKit that I just make an easy bridge to UIKit so I can call the SafariController in a single line from SwiftUI like so:

HSHosting.openSafari(url:URL(string: "https://hobbyistsoftware.com")!)

I just replace UIHostingController at the top level of my app with HSHostingController

(note - this class also allows you to control the presentation style of modals)

//HSHostingController.swift
import Foundation
import SwiftUI
import SafariServices

class HSHosting {
    static var controller:UIViewController?
    static var nextModalPresentationStyle:UIModalPresentationStyle?

    static func openSafari(url:URL,tint:UIColor? = nil) {
        guard let controller = controller else {
            preconditionFailure("No controller present. Did you remember to use HSHostingController instead of UIHostingController in your SceneDelegate?")
        }

        let vc = SFSafariViewController(url: url)  

        vc.preferredBarTintColor = tint
        //vc.delegate = self

        controller.present(vc, animated: true)
    }
}

class HSHostingController<Content> : UIHostingController<Content> where Content : View {

    override init(rootView: Content) {
        super.init(rootView: rootView)

        HSHosting.controller = self
    }

    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {

        if let nextStyle = HSHosting.nextModalPresentationStyle {
            viewControllerToPresent.modalPresentationStyle = nextStyle
            HSHosting.nextModalPresentationStyle = nil
        }

        super.present(viewControllerToPresent, animated: flag, completion: completion)
    }

}

use HSHostingController instead of UIHostingController in your scene delegate like so:

    // Use a HSHostingController as window root view controller.
    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)

        //This is the only change from the standard boilerplate
        window.rootViewController = HSHostingController(rootView: contentView)

        self.window = window
        window.makeKeyAndVisible()
    }

then when you want to open SFSafariViewController, just call:

HSHosting.openSafari(url:URL(string: "https://hobbyistsoftware.com")!)

for example

Button(action: {
    HSHosting.openSafari(url:URL(string: "https://hobbyistsoftware.com")!)
}) {
    Text("Open Web")
}

update: see this gist for extended solution with additional capabilities


Using BetterSafariView, you can present SFSafariViewController easily in SwiftUI. It works well as Apple intended, without losing its original push transition and swipe-to-dismiss gesture.

Usage

.safariView(isPresented: $presentingSafariView) {
    SafariView(url: URL("https://github.com/")!)
}

Example

import SwiftUI
import BetterSafariView

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

Supplemental to Matteo Pacini post, .presentation(Modal()) was removed by iOS 13's release. This code should work (tested in Xcode 11.3, iOS 13.0 - 13.3):

import SwiftUI
import SafariServices

struct ContentView: View {
    // whether or not to show the Safari ViewController
    @State var showSafari = false
    // initial URL string
    @State var urlString = "https://duckduckgo.com"

    var body: some View {
        Button(action: {
            // update the URL if you'd like to
            self.urlString = "https://duckduckgo.com"
            // tell the app that we want to show the Safari VC
            self.showSafari = true
        }) {
            Text("Present Safari")
        }
        // summon the Safari sheet
        .sheet(isPresented: $showSafari) {
            SafariView(url:URL(string: self.urlString)!)
        }
    }
}

struct SafariView: UIViewControllerRepresentable {

    let url: URL

    func makeUIViewController(context: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
        return SFSafariViewController(url: url)
    }

    func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<SafariView>) {

    }

}