问题
I'm trying to present a SFSafariViewController from a NavigationButton but I'm not sure how to do that with SwiftUI.
In UIKit, I would just do:
let vc = SFSafariViewController(url: URL(string: "https://google.com"), entersReaderIfAvailable: true)
vc.delegate = self
present(vc, animated: true)
回答1:
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>) {
}
}
回答2:
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.
回答3:
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
回答4:
Here is the answer if using a WKWebView, but again it still doesn't look right.
struct SafariView: UIViewRepresentable {
let url: String
func makeUIView(context: Context) -> WKWebView {
return WKWebView(frame: .zero)
}
func updateUIView(_ view: WKWebView, context: Context) {
if let url = URL(string: url) {
let request = URLRequest(url: url)
view.load(request)
}
}
}
回答5:
It's possible in SwiftUI, even preserving the default appearance, but you need to expose a UIViewController to work with. Start with defining a SwiftUI UIViewControllerRepresentable that is passed a boolean binding and an activation handler:
import SwiftUI
struct ViewControllerBridge: UIViewControllerRepresentable {
@Binding var isActive: Bool
let action: (UIViewController, Bool) -> Void
func makeUIViewController(context: Context) -> UIViewController {
return UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
action(uiViewController, isActive)
}
}
Then, give the widget you plan to show the SafariVC from a state property determining if it should be shown, then add this bridge to show the VC when the state changes.
struct MyView: View {
@State private var isSafariShown = false
var body: some View {
Button("Show Safari") {
self.isSafariShown = true
}
.background(
ViewControllerBridge(isActive: $isSafariShown) { vc, active in
if active {
let safariVC = SFSafariViewController(url: URL(string: "https://google.com")!)
vc.present(safariVC, animated: true) {
// Set the variable to false when the user dismisses the safari VC
self.isSafariShown = false
}
}
}
.frame(width: 0, height: 0)
)
}
}
Note that I give the ViewControllerBridge a fixed frame with a zero width and height, that means you can put this anywhere in your view hierarchy and it won't cause any significant change to your UI.
--Jakub
来源:https://stackoverflow.com/questions/56518029/how-do-i-use-sfsafariviewcontroller-with-swiftui