如何将 SFSafariViewController 与 SwiftUI 结合使用?

问题描述 投票:0回答:12

我正在尝试从

SFSafariViewController
呈现
NavigationButton
,但我不知道如何使用 SwiftUI 做到这一点。

在 UIKit 中,我会这样做:

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

    present(vc, animated: true)
ios swift safari swiftui
12个回答
82
投票

补充Matteo Pacini 帖子

.presentation(Modal())
已被 iOS 13 版本删除。此代码应该可以工作(在 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>) {

    }

}

13
投票

SFSafariViewController
是一个
UIKit
组件,因此您需要将其设为
UIViewControllerRepresentable

请参阅 集成 SwiftUI WWDC 19 视频,了解有关如何将

UIKit
组件桥接至
SwiftUI
的更多详细信息。

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

看起来像这样:

我建议通过

WKWebView
协议将
SwiftUI
移植到
UIViewRepresentable
,并代替它使用它。


6
投票

使用 BetterSafariView,您可以在 SwiftUI 中轻松呈现

SFSafariViewController
。它按照苹果的预期工作得很好,没有失去原来的推送过渡和滑动关闭手势。

使用方法

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

4
投票

有时答案就是不使用 SwiftUI! 这在 UIKit 中得到了很好的支持,我只是简单地建立了一个到 UIKit 的桥梁,这样我就可以从 SwiftUI 中以一行方式调用 SafariController,如下所示:

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

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

(注意 - 该类还允许您控制模态框的呈现样式)

//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")
}

更新:请参阅此要点,了解具有附加功能的扩展解决方案


3
投票

使用可识别协议的bheinz答案的替代版本。

为 SwiftUI 换行

SFSafariViewController

import SwiftUI
import SafariServices

struct SafariView: UIViewControllerRepresentable {

    let url: URL

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

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

    }

}

像这样使用它:

import SwiftUI

// Conformance to Identifiable is needed for sheet presentation
extension URL: Identifiable {
    public var id: String {
        self.absoluteString
    }
}

struct ContentView: View {
    
    // When set it will present Safari
    @State var presentURL: URL?

    var body: some View {
        Button("Present Safari") {
            presentURL = URL(string: "https://duckduckgo.com")!
        }
        // summon the Safari sheet
        .sheet(item: $presentUrl) { url in
            SafariView(url: url)
        }
    }
}

2
投票

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

1
投票

在 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 造成任何重大更改。

--雅库布


1
投票

由于这里的大多数(如果不是全部)答案都集中在 SwiftUI 中

SFSafariViewController
的模式呈现,这里有一个将其推送到
NavigationView
堆栈的示例(以及 在系统“完成”按钮时使“弹出”起作用)在 Safari 中点击):

struct ExampleView: View {
    let urls: [URL]

    var body: some View {
        NavigationView {
            List {
                ForEach(urls) { url in
                    NavigationLink(destination: {
                        SafariView(url: url)
                            .navigationBarTitleDisplayMode(.inline)
                            .ignoresSafeArea()
                    }, label: {
                        Text(url.absoluteString)
                    })
                }
            }
        }
    }
}

struct SafariView: UIViewControllerRepresentable {
    @Environment(\.dismiss) var dismiss

    let url: URL

    func makeUIViewController(context: Context) -> SFSafariViewController {
        let vc = SFSafariViewController(url: url)
        vc.preferredControlTintColor = .tintColor
        vc.delegate = context.coordinator
        return vc
    }

    func updateUIViewController(_ vc: SFSafariViewController, context: Context) {}

    class Coordinator: NSObject, SFSafariViewControllerDelegate {
        var dismissAction: DismissAction?

        func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
            dismissAction?()
        }
    }

    func makeCoordinator() -> Coordinator {
        let coordinator = Coordinator()
        coordinator.dismissAction = dismiss
        return coordinator
    }
}

1
投票

就像其他人已经说过的,你可以使用

UIViewControllerRepresentable
。我刚刚把代码删减了一点:

struct SafariVCRepresentable: UIViewControllerRepresentable {
    let url: URL

    func makeUIViewController(context _: Context) -> SFSafariViewController {
        .init(url: url)
    }

    func updateUIViewController(_: SFSafariViewController, context _: Context) {}
}

0
投票

以上都不适合我,因为我试图在按钮列表中呈现 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"))
    }
}

0
投票

在这里找到了 Antoine van der Lee 的良好而详尽的教程


-1
投票

XCode 14.3.1Swift 5.8macOS 13.2 上,这对我来说就这么简单:

        guard let url = URL(string: "https://google.com") else {
            return
        }
        let vc = SFSafariViewController(url: url)
        present(vc, animated: true)

将此代码放入启动浏览器打开事件(如链接/按钮单击)的处理程序中。 不要忘记导入包

import SafariServices

我忘记了这一点,并且收到以下错误:

cannot find 'sfsafariviewcontroller' in scope
在 chatGPT 足够友善地引导我走向正确的方向之前。

© www.soinside.com 2019 - 2024. All rights reserved.