如何在 SwiftUI 中使用 SFSafariViewController?

Posted

技术标签:

【中文标题】如何在 SwiftUI 中使用 SFSafariViewController?【英文标题】:How do I use SFSafariViewController with SwiftUI? 【发布时间】:2019-06-09 20:10:00 【问题描述】:

我正在尝试从NavigationButton 呈现SFSafariViewController,但我不确定如何使用 SwiftUI 来实现。

在 UIKit 中,我会这样做:

let vc = SFSafariViewController(url: URL(string: "https://google.com"), entersReaderIfAvailable: true)
    vc.delegate = self

    present(vc, animated: true)

【问题讨论】:

【参考方案1】:

补充Matteo Pacini post、.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>) 

    


【讨论】:

这很好用!您知道如何打开新的 Safari 视图以覆盖整个屏幕吗?当它打开时,动画将前一个视图向下推,以在这个新视图和屏幕顶部之间创建一个间隙。或者,你知道如何防止前一个View被下推,让Safari View的导航栏和前一个View的导航栏无缝衔接吗? @gsamerica 见BetterSafariView。它以全屏SFSafariViewController作为其原始样式。【参考方案2】:

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,并使用它来代替。

【讨论】:

对于您的模态示例:Use of unresolved identifier 'SafariView'。你能帮我解决这个问题吗? @CME1Crewmember SafariView 源代码在这个答案的开头 如何在不看到两个导航栏的情况下将 SafariView 推入 ContentView,我只需要 Safari 导航栏 @JAHelia 你想要的可以使用BetterSafariView【参考方案3】:

使用 BetterSafariView,您可以在 SwiftUI 中轻松呈现SFSafariViewController。它按照 Apple 的预期运行良好,而不会丢失其原始的推送过渡和滑动关闭手势。

用法

.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
                )
            )
        
    

【讨论】:

嗨@AndrewPK,BetterSafariView 本身与iOS 13 和Xcode 11 完全兼容。似乎发生错误是因为您试图将错误的实例传递给闭包。查看我刚刚在GitHub issue that you opened 上发表的评论。 是的 - 完全是我的错。打算删除我的评论,我投票赞成你的答案。 Tl;博士 - 我是个白痴? @Ixx 嗯......它甚至应该在模态上工作。请您解释一下是什么问题?【参考方案4】:

有时答案就是不要使用 SwiftUI! 这在 UIKit 中得到了很好的支持,所以我只是简单地连接到 UIKit,这样我就可以在 SwiftUI 中用一行代码调用 SafariController,如下所示:

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

我只是将应用程序顶层的 UIHostingController 替换为 HSHostingController

(注意——这个类也允许你控制模态框的展示风格)

//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 了解具有附加功能的扩展解决方案

【讨论】:

如果我要问如何在Y中使用X,你会建议不要使用Y吗? 这是我见过的唯一一个能在标准、全屏、非模态演示中正确呈现 SFSafariViewController 的答案。正如@ConfusedVorlon 所说,它可以在SwiftUI 中使用。谢谢! 这适用于 iOS 13 && Xcode 11,这意味着此解决方案适用于在 Xcode 12 GM 可用之前运送到应用商店的人们。【参考方案5】:

如果使用 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)
        
    

【讨论】:

【参考方案6】:

这在 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 造成任何重大变化。

--雅库布

【讨论】:

这完全按照我想要的方式呈现,但是 SFSafariViewController 不会加载网站,并且控制台说:Presenting view controllers on detached view controllers is discouraged &lt;UIViewController: 0x7faa34967ef0&gt;. 好吧,从您所写的内容来看,您似乎将桥梁添加到了在您尝试展示网站时未在屏幕上显示的视图。尝试确保桥连接到屏幕上显示的视图,否则尝试发布您正在使用的代码。 我在这段代码中遇到了完全相同的问题。相同的警告消息,页面保持空白。【参考方案7】:

以上都不适合我,因为我试图在按钮列表中显示 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: $0), 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"))
    

【讨论】:

以上是关于如何在 SwiftUI 中使用 SFSafariViewController?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 UIkit 中添加 SwiftUI 对象

如何在 SwiftUI 中使用 SFSafariViewController?

如何在 swiftUI 视图中使用按钮? [关闭]

如何在 SwiftUI 中使用 .fileimporter 保存音频文件并检索文件并在 SwiftUI 列表中显示它们的文件名?

如何在 Xcode Playground 中使用 SwiftUI?

如何在 SwiftUI 中使用渐变填充形状