大师学SwiftUI第17章Part1 - Web内容访问及自定义Safari视图控制器

121 阅读5分钟

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: 链接

✍️跟我一起做:创建一个多平台项目。使用示例17-1的代码更新ContentView视图。在iPhone模拟器上运行应用,点击按钮。系统会打开外部浏览器并加载网站。

本例中,我们在代码内定义了URL,但有时URL由用户提供或是通过另一个文档获取。这时,URL中可能包含不允许出现的字符,导致无法识别位置。要保障URL有效,我们需要将不安全的字符转化为百分号编码字符。这些字符由%接十六进制数字进行表示。为此String结构体中包含了如下方法。

  • addingPercentEncoding(withAllowedCharacters: CharacterSet):该方法返回一个字符串,参数指定的集合中所有字符都会使用百分号编码的字符进行替换。withAllowedCharacters参数是一个带类型属性的结构体,创建表示通用集合的实例。用于URL的有urlFragmentAllowedurlHostAllowedurlPasswordAllowedurlPathAllowedurlQueryAllowedurlUserAllowed

这一方法由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字符串,提取组成部分将它们赋值给结构体属性,以便读取或修改。本例中,我们将字符串httpsscheme属性保障URL有效,可由系统处理。组成部分就绪后,我们可通过string属性获取完成的URL,使用百分号编码的字符替换无效字符并打开。

图17-2:自定义URL

图17-2:自定义URL

Safari视图控制器

链接为我们提供了在应用内对网页的访问,但是在外部应用中打开的文档。考虑到抓住用户的注意力非常重要,苹果内置了一个名为SafaraServices的框架。通过该框架,我们可以在应用中内置Safari流星器,为用户提供更好的体验。框架包含一个SFSafariViewController类,创建包含显示网页的视图及导航工具的视图控制器。

  • SFSafariViewController(url: URL, configuration: Configuration):这个初始化方法创建一个新的自动加载url参数指定网站的Safari视图控制器。configuration参数是SFSafariViewController类中Configuration类的对象的一个属性。可以使用的属性有entersReaderIfAvailablebarCollapsingEnabled

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-3:Safari浏览器

✍️跟我一起做:创建一个多平台项目。使用示例17-4中的代码创建一个名为SafariBrowser.swift的Swift文件。使用示例17-5中的代码更新ContentView视图。在iPhone模拟器上运行程序,点击按钮。这时会在弹窗中打开Safari游览器访问网址https://www.formasterminds.com

SFSafariViewController类还提供了如下的配置属性:

  • dismissButtonStyle:该属性设置或返回一个值,用于决定视图控制器释放视图所显示的按钮类型。它是一个类型为DismissButtonStyle的枚举,值有done(默认值)、closecancel
  • 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视图控制器

图17-4:自定义Safari视图控制器

注意UIColor类是由UIKit框架所定义的类。该类包含很多的初始化方法。最常的是UIColor(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)。这个类还包含一些创建预定义颜色的类型属性。当前可以使用的有systemBluesystemBrownsystemCyan、systemGreensystemIndigosystemMintsystemOrangesystemPinksystemPurplesystemRedsystemTealsystemYellowsystemGraysystemGray2systemGray3systemGray4systemGray5systemGray6clearblackbluebrowncyandarkGraygraygreenlightGraymagentaorangepurpleredwhiteyellow

在用户滚动页面时,控制器会收成导航栏为内容让出空间。这会对用户退出或访问工具造成困难。如果我们觉得应用保留导航栏为原始尺寸更为合理,可以使用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视图控制器。此时按钮被禁用。

代码请见:GitHub仓库

本文首发地址:AlanHou的个人博客,整理自2023年10月版《SwiftUI for Masterminds》