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》