SwiftUIからSFSafariViewController
を使いたい場面は多い。
SafariView
SFSafariViewController
はビューコントローラーだから、UIViewControllerRepresentable
を使ってSwiftUIのビューにしてしまうのが簡単か。
import SwiftUI import SafariServices struct SafariView: UIViewControllerRepresentable { typealias UIViewControllerType = SFSafariViewController typealias Configuration = SFSafariViewController.Configuration private let url: URL private let configuration: Configuration? init(url: URL, configuration: Configuration? = nil) { self.url = url self.configuration = configuration } func makeUIViewController(context: Context) -> SFSafariViewController { let safariViewController: SFSafariViewController if let configuration { safariViewController = SFSafariViewController(url: url, configuration: configuration) } else { safariViewController = SFSafariViewController(url: url) } return safariViewController } func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) { } }
誰でもこういうコードを書いたことがあるんじゃないか。
あとは何らかのきっかけでfullScreenCover(isPresented:onDismiss:content:)
)なんかを使って、このSafariView
を表示してやればいい。
UIViewController
のpresent(_:animated:completion:)
とは少し違うけど、他にいい方法を知らない。
じゃあ「何らかのきっかけ」ってなんだろう、というところが本題。
OpenURLAction
SwiftUIでは、Text
のイニシャライザにAttributedString
を渡せば、文字列中にリンクを埋め込める。
あるいはそのものずばりLink
ビューというのがあって、URLへのリンクを表現できる。
こういうのをクリックしたときもSFSafariViewController
を表示したい。
実はLink
のドキュメンテーションにいい例があった。
Link("Visit Our Site", destination: URL(string: "https://www.example.com")!) .environment(\.openURL, OpenURLAction { url in print("Open \(url)") return .handled })
Link
やText
内のリンクをクリックすると、Environment
からOpenURLAction
が呼び出される。
そしてこれは上書き可能になっている。
OpenURLAction
を上書きして、SFSafariViewController
を開くようにフックしてやればよいらしい。
渡ってくるURLを状態として持って、URLが存在するときSafariView
を開く、という風にしたい。
何とか再利用性のあるコードにしたいので、ちょっと考える。
ViewModifier
ここで、ViewModifier
を使えばモディファイア内に状態を持てることを思い出す。
ちょっと書いてみるとこういう感じになる。
import SwiftUI struct OpenURLInSafariViewModifier: ViewModifier { @State private var url: URL? = nil private var isPresented: Binding<Bool> { Binding { url != nil } set: { newValue in if newValue == false { url = nil } } } private let configuration: SafariView.Configuration? init(configuration: SafariView.Configuration?) { self.configuration = configuration } func body(content: Content) -> some View { content .environment(\.openURL, OpenURLAction { url in switch url.scheme { case "https"?, "http"?: self.url = url return .handled default: return .systemAction(url) } }) .fullScreenCover(isPresented: isPresented) { if let url { SafariView(url: url, configuration: configuration) .edgesIgnoringSafeArea(.all) } } } } extension View { func openURLInSafariView(configuration: SafariView.Configuration? = nil) -> some View { return modifier(OpenURLInSafariViewModifier(configuration: configuration)) } }
いっけんよさそう。
SafariServicesUI
こういう感じで使う。
import SwiftUI struct ContentView: View { var body: some View { Link("Open in SFSafariViewController", destination: URL(string: "https://developer.apple.com")!) .openURLInSafariView() } }
import SwiftUI struct ContentView: View { var body: some View { Text("Open in SFSafariViewController with [Attributed String](https://developer.apple.com)") .openURLInSafariView() } }
状態が隠蔽されていて、シンプルだし、SwiftUIっぽいインターフェースだと思う。
ということでライブラリっぽくSwift Packageにしてみた。
どうでしょうか。