How do I use SFSafariViewController with SwiftUI?

♀尐吖头ヾ 提交于 2020-07-05 03:51:55

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!