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

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的有urlFragmentAllowedurlHostAllowedurlPasswordAllowedurlPathAllowedurlQueryAllowedurlUserAllowed

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

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(默认值)、closecancel
  • 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)。这个类还包含一些创建预定义颜色的类型属性。当前可以使用的有systemBluesystemBrownsystemCyan、systemGreensystemIndigosystemMintsystemOrangesystemPinksystemPurplesystemRedsystemTealsystemYellowsystemGraysystemGray2systemGray3systemGray4systemGray5systemGray6clearblackbluebrowncyandarkGraygraygreenlightGraymagentaorangepurpleredwhiteyellow

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

相关推荐
Jewel1051 小时前
Flutter代码混淆
android·flutter·ios
安和昂4 小时前
【iOS】知乎日报第三周总结
ios
键盘敲没电4 小时前
【iOS】知乎日报前三周总结
学习·ios·objective-c·xcode
B.-10 小时前
Flutter 应用在真机上调试的流程
android·flutter·ios·xcode·android-studio
iFlyCai20 小时前
Xcode 16 pod init失败的解决方案
ios·xcode·swift
郝晨妤1 天前
HarmonyOS和OpenHarmony区别是什么?鸿蒙和安卓IOS的区别是什么?
android·ios·harmonyos·鸿蒙
Hgc558886661 天前
iOS 18.1,未公开的新功能
ios
Hamm1 天前
先别急着喷,没好用的iOS-Ollama客户端那就自己写个然后开源吧
人工智能·llm·swift
CocoaKier1 天前
苹果商店下载链接如何获取
ios·apple
zhlx28351 天前
【免越狱】iOS砸壳 可下载AppStore任意版本 旧版本IPA下载
macos·ios·cocoa