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.
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:
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>) {
}
}