如何将 SFSafariViewController 与 SwiftUI 一起使用?
How do I use SFSafariViewController with SwiftUI?
我正在尝试从 NavigationButton
呈现 SFSafariViewController
,但我不确定如何使用 SwiftUI 来实现。
在 UIKit 中,我会这样做:
let vc = SFSafariViewController(url: URL(string: "https://google.com"), entersReaderIfAvailable: true)
vc.delegate = self
present(vc, animated: true)
SFSafariViewController
是一个 UIKit
组件,因此您需要将其设为 UIViewControllerRepresentable
.
有关如何将 UIKit
组件桥接到 SwiftUI
的更多详细信息,请参阅 Integrating SwiftUI WWDC 19 视频。
struct SafariView: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
return SFSafariViewController(url: url)
}
func updateUIViewController(_ uiViewController: SFSafariViewController,
context: UIViewControllerRepresentableContext<SafariView>) {
}
}
警告说明:SFSafariViewController
旨在显示在另一个视图控制器之上,而不是推送到导航堆栈中。
它还有一个导航栏,这意味着如果你按下视图控制器,你会看到两个个导航栏。
如果以模态方式呈现,它似乎可以工作 - 虽然它有问题。
struct ContentView : View {
let url = URL(string: "https://www.google.com")!
var body: some View {
EmptyView()
.presentation(Modal(SafariView(url:url)))
}
}
看起来像这样:
我建议通过 UIViewRepresentable
协议将 WKWebView
移植到 SwiftUI
,并使用它代替。
这是使用 WKWebView 的答案,但它看起来仍然不正确。
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)
}
}
}
, .presentation(Modal())
was removed by iOS 13's release 的补充。此代码应该有效(在 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>) {
}
}
有时答案就是不使用 SwiftUI!
这在 UIKit 中得到了很好的支持,我只是做了一个简单的 UIKit 桥接,所以我可以从 SwiftUI 中在一行中调用 SafariController,如下所示:
HSHosting.openSafari(url:URL(string: "https://hobbyistsoftware.com")!)
我只是用 HSHostingController 替换了应用顶层的 UIHostingController
(注意 - 此 class 还允许您控制模式的呈现样式)
//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)
}
}
在场景委托中使用 HSHostingController 而不是 UIHostingController
像这样:
// 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()
}
那么当你想打开SFSafariViewController时,只需调用:
HSHosting.openSafari(url:URL(string: "https://hobbyistsoftware.com")!)
例如
Button(action: {
HSHosting.openSafari(url:URL(string: "https://hobbyistsoftware.com")!)
}) {
Text("Open Web")
}
更新:请参阅 this gist 以了解具有附加功能的扩展解决方案
在 SwiftUI 中是可能的,甚至保留默认外观,但您需要公开一个 UIViewController 才能使用。从定义一个 SwiftUI UIViewControllerRepresentable 开始,它传递了一个布尔绑定和一个激活处理程序:
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)
}
}
然后,给你计划显示 SafariVC 的小部件从一个状态 属性 确定它是否应该显示,然后添加这个桥以显示 VC 当状态变化。
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)
)
}
}
请注意,我给 ViewControllerBridge 一个宽度和高度为零的固定框架,这意味着您可以将它放在视图层次结构中的任何位置,并且不会对您的 UI。
--雅库布
使用BetterSafariView,你可以在SwiftUI中轻松呈现SFSafariViewController
。它按照 Apple 的预期运行良好,不会丢失其原始的推送过渡和 swipe-to-dismiss 手势。
用法
.safariView(isPresented: $presentingSafariView) {
SafariView(url: URL("https://github.com/")!)
}
示例
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
)
)
}
}
}
上面的 None 对我有用,因为我试图在按钮列表中显示 SFSafariViewController。我最终使用了绑定。
在您的视图控制器中进行绑定:
class YourViewController: UIViewController {
private lazy var guideHostingViewController = UIHostingController(rootView: UserGuideView(presentUrl: presentBinding))
@objc private func showWebsite() {
let navVC = UINavigationController(rootViewController: guideHostingViewController)
present(navVC, animated: true, completion: nil)
}
private var presentBinding: Binding<URL> {
return Binding<URL>(
get: {
return URL(string: "https://www.percento.app")!
},
set: {
self.guideHostingViewController.present(SFSafariViewController(url: [=10=]), animated: true, completion: nil)
}
)
}
}
SwiftUI 列表:
struct UserGuideView: View {
private let guidePages: [SitePage] = [.multiCurrency, .stockSync, .dataSafety, .privacy]
@Binding var presentUrl: URL
var body: some View {
VStack(alignment: .leading) {
ForEach(guidePages) { page in
Button(action: {
presentUrl = page.localiedContentUrl
}) {
Text(page.description)
.foregroundColor(Color(UIColor.label))
.modifier(UserGuideRowModifier(icon: .init(systemName: page.systemIconName ?? "")))
}
}
Spacer()
}
.padding()
.navigationBarTitle(NSLocalizedString("User Guide", comment: "User Guide navigation bar"))
}
}
我正在尝试从 NavigationButton
呈现 SFSafariViewController
,但我不确定如何使用 SwiftUI 来实现。
在 UIKit 中,我会这样做:
let vc = SFSafariViewController(url: URL(string: "https://google.com"), entersReaderIfAvailable: true)
vc.delegate = self
present(vc, animated: true)
SFSafariViewController
是一个 UIKit
组件,因此您需要将其设为 UIViewControllerRepresentable
.
有关如何将 UIKit
组件桥接到 SwiftUI
的更多详细信息,请参阅 Integrating SwiftUI WWDC 19 视频。
struct SafariView: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
return SFSafariViewController(url: url)
}
func updateUIViewController(_ uiViewController: SFSafariViewController,
context: UIViewControllerRepresentableContext<SafariView>) {
}
}
警告说明:SFSafariViewController
旨在显示在另一个视图控制器之上,而不是推送到导航堆栈中。
它还有一个导航栏,这意味着如果你按下视图控制器,你会看到两个个导航栏。
如果以模态方式呈现,它似乎可以工作 - 虽然它有问题。
struct ContentView : View {
let url = URL(string: "https://www.google.com")!
var body: some View {
EmptyView()
.presentation(Modal(SafariView(url:url)))
}
}
看起来像这样:
我建议通过 UIViewRepresentable
协议将 WKWebView
移植到 SwiftUI
,并使用它代替。
这是使用 WKWebView 的答案,但它看起来仍然不正确。
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)
}
}
}
.presentation(Modal())
was removed by iOS 13's release 的补充。此代码应该有效(在 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>) {
}
}
有时答案就是不使用 SwiftUI! 这在 UIKit 中得到了很好的支持,我只是做了一个简单的 UIKit 桥接,所以我可以从 SwiftUI 中在一行中调用 SafariController,如下所示:
HSHosting.openSafari(url:URL(string: "https://hobbyistsoftware.com")!)
我只是用 HSHostingController 替换了应用顶层的 UIHostingController
(注意 - 此 class 还允许您控制模式的呈现样式)
//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)
}
}
在场景委托中使用 HSHostingController 而不是 UIHostingController 像这样:
// 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()
}
那么当你想打开SFSafariViewController时,只需调用:
HSHosting.openSafari(url:URL(string: "https://hobbyistsoftware.com")!)
例如
Button(action: {
HSHosting.openSafari(url:URL(string: "https://hobbyistsoftware.com")!)
}) {
Text("Open Web")
}
更新:请参阅 this gist 以了解具有附加功能的扩展解决方案
在 SwiftUI 中是可能的,甚至保留默认外观,但您需要公开一个 UIViewController 才能使用。从定义一个 SwiftUI UIViewControllerRepresentable 开始,它传递了一个布尔绑定和一个激活处理程序:
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)
}
}
然后,给你计划显示 SafariVC 的小部件从一个状态 属性 确定它是否应该显示,然后添加这个桥以显示 VC 当状态变化。
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)
)
}
}
请注意,我给 ViewControllerBridge 一个宽度和高度为零的固定框架,这意味着您可以将它放在视图层次结构中的任何位置,并且不会对您的 UI。
--雅库布
使用BetterSafariView,你可以在SwiftUI中轻松呈现SFSafariViewController
。它按照 Apple 的预期运行良好,不会丢失其原始的推送过渡和 swipe-to-dismiss 手势。
用法
.safariView(isPresented: $presentingSafariView) {
SafariView(url: URL("https://github.com/")!)
}
示例
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
)
)
}
}
}
None 对我有用,因为我试图在按钮列表中显示 SFSafariViewController。我最终使用了绑定。
在您的视图控制器中进行绑定:
class YourViewController: UIViewController {
private lazy var guideHostingViewController = UIHostingController(rootView: UserGuideView(presentUrl: presentBinding))
@objc private func showWebsite() {
let navVC = UINavigationController(rootViewController: guideHostingViewController)
present(navVC, animated: true, completion: nil)
}
private var presentBinding: Binding<URL> {
return Binding<URL>(
get: {
return URL(string: "https://www.percento.app")!
},
set: {
self.guideHostingViewController.present(SFSafariViewController(url: [=10=]), animated: true, completion: nil)
}
)
}
}
SwiftUI 列表:
struct UserGuideView: View {
private let guidePages: [SitePage] = [.multiCurrency, .stockSync, .dataSafety, .privacy]
@Binding var presentUrl: URL
var body: some View {
VStack(alignment: .leading) {
ForEach(guidePages) { page in
Button(action: {
presentUrl = page.localiedContentUrl
}) {
Text(page.description)
.foregroundColor(Color(UIColor.label))
.modifier(UserGuideRowModifier(icon: .init(systemName: page.systemIconName ?? "")))
}
}
Spacer()
}
.padding()
.navigationBarTitle(NSLocalizedString("User Guide", comment: "User Guide navigation bar"))
}
}