App可以让用户访问网页,但实现的方式有不止一种。我们可以让用户通过链接在浏览器中打开文档、在应用界面中内嵌一个预定义的浏览器或是在后台下载并处理数据。
链接
链接是一个关联表示文档位置的文本或图片。在用户点击链接时打开文档。链接设计之初用于网页,但我们可以将其插入应用,让系统决定在何处(浏览器或是其它应用)打开文档。SwiftUI自带有Link视图进行创建。
Link (String, destination: URL);初始化创建一个打开链接的按钮。第一个参数指定按钮的标题,destination参数是一个带有希望打开的文档位置的URL结构体。如果希望使用视图来展示标签,可以实现初始化方法Link(destination:,label")。
下例在点击按钮时打开alanhou.org。代码定义了一个@State属性存储URL,使用希望打开的链接进行初始化。我们使用该属性的值创建URL 结构体并将其赋给Link 视图。在点击按钮时,系统会读取URL,识别到它是一个网页链接,然后打开浏览器加载相应网站。
示例17-1:打开网站
scss
struct ContentView: View {
@State private var searchURL = "https://alanhou.org"
var body: some View {
NavigationStack {
VStack {
Link("Open Web", destination: URL(string: searchURL)!)
.buttonStyle(.borderedProminent)
Spacer()
}.padding()
}
}
}

图17-1: 链接
✍️跟我一起做:创建一个多平台项目。使用示例17-1 的代码更新ContentView视图。在iPhone模拟器上运行应用,点击按钮。系统会打开外部浏览器并加载网站。
本例中,我们在代码内定义了URL,但有时URL由用户提供或是通过另一个文档获取。这时,URL中可能包含不允许出现的字符,导致无法识别位置。要保障URL有效,我们需要将不安全的字符转化为百分号编码字符。这些字符由%接十六进制数字进行表示。为此String结构体中包含了如下方法。
- addingPercentEncoding (withAllowedCharacters : CharacterSet):该方法返回一个字符串,参数指定的集合中所有字符都会使用百分号编码的字符进行替换。withAllowedCharacters 参数是一个带类型属性的结构体,创建表示通用集合的实例。用于URL的有
urlFragmentAllowed、urlHostAllowed、urlPasswordAllowed、urlPathAllowed、urlQueryAllowed和urlUserAllowed。
这一方法由NSString 类实现,但可在String 结构体的任意实例中使用。这意味着可以对希望检查的URL直接应用该方法,并将其赋值给Link视图。唯一的问题是这个视图要求URL已可处理,因此要先使用一个计算属性或方法检查其值。为简化这一处理,环境中包含一个名为openURL的属性,返回可用于打开URL的方法。下例实现了一个Button视图使用百分号编码字符替换掉无效字符,然后执行openURL()方法打开链接。
示例17-2:编码URL
less
struct ContentView: View {
@Environment(.openURL) var openURL
@State private var searchURL = "https://alanhou.org"
var body: some View {
NavigationStack {
VStack {
Button("Open Web") {
if let url = searchURL.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
openURL(URL(string: url)!)
}
}
.buttonStyle(.borderedProminent)
Spacer()
}.padding()
}
}
}
上例中,我们处理了一个知道没有问题的URL ,但有时并不是这样。通常URL 来自外部数据源或由用户提供。这时我们不仅要使用addingPercentEncoding()对值进行编码,还要确定存在所有的URL 组件。 例如,用户只提供了域名(alanhou.org),没带协议(https),我们需要在尝试打开前创建完整的URL 。为阅读、创建及修改URL 组件,Foundation 框架定义了URLComponents结构体。该结构体包含如下初始化方法。
- URLComponents (string : String):这个初始化方法通过
string参数指定的URL 组成部分创建一个URLComponents结构体。
URLComponents结构体包含一些读取和修改组成部分的属性。下面是一些常用的。
- scheme :这一属性设置或返回URL 的协议(如
http)。 - host :该属性设置或返回URL的域名(如
www.google.com)。 - path :该属性设置或返回URL域名后的部分(
/index.php)。 - query :该属性设置或返回URL的参数(如
id=22)。 - queryItems: 该属性设置或返回一个
URLQueryItem结构体数组,包含URL中的所有参数。
URLComponents 结构体还包含如下属性,返回一个由各组成部分创建URL的字符串。
- string :该组成返回由各组成部分值构建URL的字符串。
在下例中,我们允许用户插入一个URL,但确保了一定会包含https协议。
示例17-3 :编码自定义URL
less
struct ContentView: View {
@Environment(.openURL) var openURL
@State private var searchURL = ""
var body: some View {
NavigationStack {
VStack {
TextField("Insert URL", text: $searchURL)
.textFieldStyle(.roundedBorder)
.autocapitalization(.none)
.autocorrectionDisabled(true)
Button("Open Web") {
if !searchURL.isEmpty {
var components = URLComponents(string: searchURL)
components?.scheme = "https"
if let newURL = components?.string {
if let url = newURL.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
openURL(URL(string: url)!)
}
}
}
}
.buttonStyle(.borderedProminent)
Spacer()
}.padding()
}
}
}
URLComponents 结构体接收一个URL 字符串,提取组成部分将它们赋值给结构体属性,以便读取或修改。本例中,我们将字符串https给scheme属性保障URL有效,可由系统处理。组成部分就绪后,我们可通过string属性获取完成的URL,使用百分号编码的字符替换无效字符并打开。

图17-2:自定义URL
Safari视图控制器
链接为我们提供了在应用内对网页的访问,但是在外部应用中打开的文档。考虑到抓住用户的注意力非常重要,苹果内置了一个名为SafaraServices 的框架。通过该框架,我们可以在应用中内置Safari流星器,为用户提供更好的体验。框架包含一个SFSafariViewController类,创建包含显示网页的视图及导航工具的视图控制器。
- SFSafariViewController (url : URL, configuration : Configuration):这个初始化方法创建一个新的自动加载
url参数指定网站的Safari视图控制器。configuration参数是SFSafariViewController类中Configuration类的对象的一个属性。可以使用的属性有entersReaderIfAvailable和barCollapsingEnabled。
SFSafariViewController类创建一个UIKit 视图控制器。因此,我们必须通过UIViewControllerRepresentable协议定义一个representable视图控制器,添加到我们的SwiftUI 界面中,如下例所示。(更多有关representable视图控制器的内容,请参见第16章。)
示例17-4:创建Safari浏览器
swift
import SwiftUI
import SafariServices
struct SafariBrowser: UIViewControllerRepresentable {
@Binding var searchURL: URL
func makeUIViewController(context: Context) -> SFSafariViewController {
let safari = SFSafariViewController(url: searchURL)
return safari
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) {
}
}
该结构体创建一个包含可使用的Safari游览器的视图控制器。下例中,我们在sheet弹窗中打开这个视图。
示例17-5:打开Safari游览器
less
struct ContentView: View {
@State private var searchURL: URL = URL(string: "https://www.formasterminds.com")!
@State private var openSheet: Bool = false
var body: some View {
VStack {
Button("Open Browser") {
openSheet = true
}.buttonStyle(.borderedProminent)
Spacer()
}.padding()
.sheet(isPresented: $openSheet) {
SafariBrowser(searchURL: $searchURL)
}
}
}
这一视图定义了一个类型为URL 的@State属性,使用https://www.formasterminds.com进行初始化。点击按钮时,SafariBrowser视图使用该值进行初始化,在弹窗中打开浏览器并加载网站。

图17-3:Safari浏览器
✍️跟我一起做:创建一个多平台项目。使用示例17-4 中的代码创建一个名为SafariBrowser.swift的Swift文件。使用示例17-5 中的代码更新ContentView视图。在iPhone模拟器上运行程序,点击按钮。这时会在弹窗中打开Safari游览器访问网址https://www.formasterminds.com。
SFSafariViewController类还提供了如下的配置属性:
- dismissButtonStyle :该属性设置或返回一个值,用于决定视图控制器释放视图所显示的按钮类型。它是一个类型为
DismissButtonStyle的枚举,值有done(默认值)、close和cancel。 - preferredBarTintColor :该属性设置或返回一个决定导航栏颜色的
UIColor值。 - preferredControlTintColor :该属性设置或返回一个决定控件颜色的
UIColor值。
下例使用这三个属性将浏览器的颜色适配www.formasterminds.com网站。
示例17-6:配置视图控制器
swift
struct SafariBrowser: UIViewControllerRepresentable {
@Binding var searchURL: URL
func makeUIViewController(context: Context) -> SFSafariViewController {
let safari = SFSafariViewController(url: searchURL)
safari.dismissButtonStyle = .close
safari.preferredBarTintColor = UIColor(red: 81/255, green: 91/255, blue: 119/255, alpha: 1.0)
safari.preferredControlTintColor = UIColor.white
return safari
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) {
}
}
示例17-6 中的代码还修改了dismissButtonStyle属性,来改变浏览器所显示的按钮类型。Done 按钮变成了Close。

图17-4:自定义Safari视图控制器
注意 :
UIColor类是由UIKit 框架所定义的类。该类包含很多的初始化方法。最常的是UIColor(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)。这个类还包含一些创建预定义颜色的类型属性。当前可以使用的有systemBlue、systemBrown、systemCyan、、systemGreen、systemIndigo、systemMint、systemOrange、systemPink、systemPurple、systemRed、systemTeal、systemYellow、systemGray、systemGray2、systemGray3、systemGray4、systemGray5、systemGray6、clear、black、blue、brown、cyan、darkGray、gray、green、lightGray、magenta、orange、purple、red、white和yellow。
在用户滚动页面时,控制器会收成导航栏为内容让出空间。这会对用户退出或访问工具造成困难。如果我们觉得应用保留导航栏为原始尺寸更为合理,可以使用Configuration对象初始化控制器。这个类位于SFSafariViewController类之中,包含如下控制导航栏的属性。
barCollapsingEnabled:这一属性设置或返回决定导航栏收起或展开的布尔值。
创建好Configuration对象后,我们可以配置这个属性,通过控制器的初始化方法将其赋值给Safari视图控制器。
示例17-7:导航栏保留为原始大小
swift
struct SafariBrowser: UIViewControllerRepresentable {
@Binding var searchURL: URL
func makeUIViewController(context: Context) -> SFSafariViewController {
let config = SFSafariViewController.Configuration()
config.barCollapsingEnabled = false
let safari = SFSafariViewController(url: searchURL, configuration: config)
return safari
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) {
}
}
✍️跟我一起做:使用示例17-7 中的代码更新SafariBrowser结构体。运行应用、滑动页面。导航栏会保持在原始大小,按钮也一直可见。
该框架还定义了一个SFSafariViewControllerDelegate协议,这样可以对Safari视图控制器添加一个代理用于控制流程。以下是一部分协议中定义的方法。
- safariViewController (SFSafariViewController, didCompleteInitialLoad: Bool):这个方法在初始网站完成加载时由控制器调用。
- safariViewControllerDidFinish (SFSafariViewController):这一方法在视图释放后(用户点击Done按钮)由控制器调用。
Safari视图控制器有一个delegate属性用于设置代理。下例中创建了一个coordinator,赋值给了视图的代理,并实现了safariViewControllerDidFinish()方法来在用户释放视图时禁用界面上的按钮。(用户仅能打开视图一次。)
示例17-8:为Safari视图控制器添加代理
swift
struct SafariBrowser: UIViewControllerRepresentable {
@Binding var disable: Bool
@Binding var searchURL: URL
func makeUIViewController(context: Context) -> SFSafariViewController {
let config = SFSafariViewController.Configuration()
config.barCollapsingEnabled = false
let safari = SFSafariViewController(url: searchURL, configuration: config)
safari.delegate = context.coordinator
return safari
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) {
}
func makeCoordinator() -> SafariCoordinator {
SafariCoordinator(disableCoordinator: $disable)
}
}
class SafariCoordinator: NSObject, SFSafariViewControllerDelegate {
@Binding var disableCoordinator: Bool
init(disableCoordinator: Binding<Bool>) {
self._disableCoordinator = disableCoordinator
}
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
disableCoordinator = true
}
}
在这个视图中,我们需要定义一个@State属性存储一个布尔值并在Button视图中实现disable()修饰符来根据这个值启用或禁用按钮。
示例17-9:通过Safari视图控制器代理禁用按钮
less
struct ContentView: View {
@State private var searchURL: URL = URL(string: "https://www.formasterminds.com")!
@State private var openSheet: Bool = false
@State private var disableButton: Bool = false
var body: some View {
VStack {
Button("Open Browser") {
openSheet = true
}.buttonStyle(.borderedProminent)
.disabled(disableButton)
Spacer()
}.padding()
.sheet(isPresented: $openSheet) {
SafariBrowser(disable: $disableButton, searchURL: $searchURL)
}
}
}
本例中,我们添加了一个Bool类型的@State属性disableButton,将其传递给representable视图控制器,因此可以通过coordinator修改其值。在释放Safari视图控制器时,执行safariViewControllerDidFinish()方法,disableButton属性的设为true,因此用户无法再次点击按钮。
✍️跟我一起做:使用示例17-8 中的代码更新SafariBrowser.swift文件、示例17-9 中的代码更新ContentView视图。在iPhone模拟中运行应用、按下按钮。点击Done按钮关闭Safari视图控制器。此时按钮被禁用。
代码请见:GitHub仓库
本文首发地址:AlanHou的个人博客,整理自2023年10月版《SwiftUI for Masterminds》