App可以让用户访问网页,但实现的方式有不止一种。我们可以让用户通过链接在浏览器中打开文档、在应用界面中内嵌一个预定义的浏览器或是在后台下载并处理数据。
链接
链接是一个关联表示文档位置的文本或图片。在用户点击链接时打开文档。链接设计之初用于网页,但我们可以将其插入应用,让系统决定在何处(浏览器或是其它应用)打开文档。SwiftUI自带有Link视图进行创建。
Link(String, destination: URL);初始化创建一个打开链接的按钮。第一个参数指定按钮的标题,destination参数是一个带有希望打开的文档位置的URL结构体。如果希望使用视图来展示标签,可以实现初始化方法Link(destination:,label")。
下例在点击按钮时打开alanhou.org。代码定义了一个@State属性存储URL,使用希望打开的链接进行初始化。我们使用该属性的值创建URL结构体并将其赋给Link视图。在点击按钮时,系统会读取URL,识别到它是一个网页链接,然后打开浏览器加载相应网站。
示例17-1:打开网站
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
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
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浏览器
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游览器
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:配置视图控制器
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:导航栏保留为原始大小
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视图控制器添加代理
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视图控制器代理禁用按钮
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视图控制器。此时按钮被禁用。
其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记



