跳到主要内容

用SwiftUI轻松使用SFSafariViewController实现在APP里打开网页

· 阅读需 7 分钟
GoSwiftUI
goswiftui.com

SFSafariViewController 可用于让用户在应用内而非外部浏览器中打开网页。虽然该视图控制器在 UIKit 中运行良好,但在 SwiftUI 应用中让其正常工作可能具有挑战性。

每当你遇到只有 UIKit 解决方案可用的情况时,你想知道如何编写一个包装器并使 UIKit 类对 SwiftUI 视图可用。最好的情况是,它是可重用的,以便稍后可以重复使用它。让我们深入了解吧!

为 SFSafariViewController 创建 SwiftUI 包装器

我们通过创建 UIViewRepresentable 的实现来开始实现 SFSafariViewController 的 SwiftUI 包装器。该协议允许我们创建一个 SwiftUI 视图,该视图包装了 UIKit 视图控制器:

struct SFSafariView: UIViewControllerRepresentable {
let url: URL

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

func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<SFSafariView>) {
// 这里不需要做任何事情
}
}

我们需要实现两个方法:

  • makeUIViewController 将被调用以创建 UIViewController 实例
  • updateUIViewController 将被调用以使用 SwiftUI 中的新信息更新 UIViewController 的状态

在我们的情况下,我们只需使用给定的 URL 实例化 SFSafariViewController

同样的技术也适用于 UIView 实例,我在我的文章中已经解释过使用 UIViewRepresentable 将 UIView 实例集成到 SwiftUI 中

创建可重用的视图修饰符

我总是喜欢从一开始就编写可重用的代码,以允许我的代码可以重复使用。我甚至有一个专门用于扩展的包,我可以轻松地在不同的应用程序中重复使用它,从而在遇到以前见过的问题时更快地编写应用程序。

在这种情况下,我想要一个视图修饰符,它可以捕获通常在外部浏览器中打开的任何链接。在 SwiftUI 中,这些链接可以如下生成:

struct SwiftUILinksView: View {
var body: some View {
VStack(spacing: 20) {
/// 使用 `Link` 视图创建链接:
Link("SwiftUI Link 示例", destination: URL(string: "https://www.rocketsim.app")!)

/// 使用 markdown 创建链接:
Text("Markdown 链接示例: [RocketSim](https://www.rocketsim.app)")
}
}
}

关键是在环境视图修饰符内使用 openURL 环境属性。视图修饰符的代码如下:

/// 监控 `openURL` 环境变量并在应用内处理它们而不是通过外部网络浏览器处理它们。
private struct SafariViewControllerViewModifier: ViewModifier {
@State private var urlToOpen: URL?

func body(content: Content) -> some View {
content
.environment(\.openURL, OpenURLAction { url in
/// 捕获即将在外部浏览器中打开的任何 URL。
/// 相反,在这里处理它们并存储 URL 以在我们的 sheet 中重新打开。
urlToOpen = url
return .handled
})
.sheet(isPresented: $urlToOpen.mappedToBool(), onDismiss: {
urlToOpen = nil
}, content: {
SFSafariView(url: urlToOpen!)
})
}
}

我们使用视图修饰符来捕获任何正在进行的 URL,并将它们用作我们 sheet 的输入。该 sheet 将使用我们之前创建的 SFSafariView 来使用 SFSafariViewController 在应用程序中呈现 URL。

请注意,我们使用了另一个扩展,该扩展允许将任何可选绑定映射到布尔绑定:

extension Binding where Value == Bool {
init(binding: Binding<(some Any)?>) {
self.init(
get: {
binding.wrappedValue != nil
},
set: { newValue in
guard newValue == false else { return }

// 我们只处理 `false` 布尔值以将我们的可选值设置为 `nil`
// 因为我们不能处理用于恢复先前值的 `true`。
binding.wrappedValue = nil
}
)
}
}

extension Binding {
/// 将可选绑定映射到 `Binding<Bool>`。
/// 例如,可以使用 `Error?` 对象来决定是否显示警报,而无需依赖于单独处理的 `Binding<Bool>`。
func mappedToBool<Wrapped>() -> Binding<Bool> where Value == Wrapped? {
Binding<Bool>(binding: self)
}
}

这是我在编写 SwiftUI 解决方案时经常重复使用的扩展之一。

最后缺失的是一个方便的视图扩展,以更轻松地访问我们的逻辑:

extension View {
/// 监控 `openURL` 环境变量并在应用内处理它们而不是通过外部网络浏览器处理它们。
/// 使用 `SafariViewWrapper`,该包装器将在 `SFSafariViewController` 中呈现 URL。
func handleOpenURLInApp() -> some View {
modifier(SafariViewControllerViewModifier())
}
}

在 SwiftUI 中呈现 SFSafariViewController

现在我们有了所有的逻辑,我们可以开始在 SwiftUI 中使用视图扩展方法呈现任何即将离开的 URL 在 SFSafariViewController 中。我们可以通过在我们的垂直堆栈上使用视图扩展方法来实现:

struct SwiftUILinksView: View {
var body: some View {
VStack(spacing: 20) {
/// 使用 `Link` 视图创建链接:
Link("SwiftUI Link 示例", destination: URL(string: "https://www.rocketsim.app")!)

/// 使用 markdown 创建链接:
Text("Markdown 链接示例: [RocketSim](https://www.rocketsim.app)")
}
/// 这捕获任何正在进行的 URL。
.handleOpenURLInApp()
}
}

请确保你了解环境对象是如何通过子视图传递的。总的来说,这段代码允许我们捕获任何即将离开的 URL 并在应用程序中呈现它们:

创建包装器后,您可以在 SwiftUI 中使用 SFSafariViewController。

最终结果允许您通过重复使用视图修饰符轻松捕获代码中的任何即将离开的 URL。