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

简介: App可以让用户访问网页,但实现的方式有不止一种。我们可以让用户通过链接在浏览器中打开文档、在应用界面中内嵌一个预定义的浏览器或是在后台下载并处理数据。

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

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-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视图控制器

注意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视图控制器。此时按钮被禁用。


其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记

相关文章
|
26天前
|
安全 前端开发 API
【Azure 应用服务】Azure Web App 服务默认支持一些 Weak TLS Ciphers Suite,是否有办法自定义修改呢?
【Azure 应用服务】Azure Web App 服务默认支持一些 Weak TLS Ciphers Suite,是否有办法自定义修改呢?
|
6天前
|
数据处理 Python
Django视图:构建动态Web页面的核心技术
Django视图:构建动态Web页面的核心技术
|
1月前
|
Java 应用服务中间件 Apache
使用IDEA修改Web项目访问路径,以及解决Apache Tomcat控制台中文乱码问题
本文介绍了在IntelliJ IDEA中修改Web项目访问路径的步骤,包括修改项目、模块、Artifacts的配置,编辑Tomcat服务器设置,以及解决Apache Tomcat控制台中文乱码问题的方法。
51 0
使用IDEA修改Web项目访问路径,以及解决Apache Tomcat控制台中文乱码问题
|
20天前
|
Java 数据库连接 数据库
强强联手!JSF 与 Hibernate 打造高效数据访问层,让你的应用如虎添翼,性能飙升!
【8月更文挑战第31天】本文通过具体示例详细介绍了如何在 JavaServer Faces (JSF) 应用程序中集成 Hibernate,实现数据访问层的最佳实践。首先,创建一个 JSF 项目并在 Eclipse 中配置支持 JSF 的服务器版本。接着,添加 JSF 和 Hibernate 依赖,并配置数据库连接池和 Hibernate 配置文件。然后,定义实体类 `User` 和 DAO 类 `UserDAO` 处理数据库操作。
42 0
|
1月前
|
运维 安全 网络安全
"革新远程访问体验:Docker化部署webssh2,一键启动Web SSH客户端,让远程管理如虎添翼!"
【8月更文挑战第2天】Docker作为软件开发与运维的关键工具,以其轻量级、可移植及强隔离特性简化了应用部署。结合webssh2这一开源Web SSH客户端,可通过浏览器安全便捷地访问SSH服务器,无需额外软件。首先确保已安装Docker,接着拉取webssh2镜像并运行容器,映射端口以便外部访问。配置好SSH服务器后,通过浏览器访问指定URL即可开始SSH会话。此方案不仅提升了用户体验,还加强了访问控制与系统安全。
110 7
|
24天前
|
JavaScript PHP 开发者
PHP中的异常处理与自定义错误处理器构建高效Web应用:Node.js与Express框架实战指南
【8月更文挑战第27天】在PHP编程世界中,异常处理和错误管理是代码健壮性的关键。本文将深入探讨PHP的异常处理机制,并指导你如何创建自定义错误处理器,以便优雅地管理运行时错误。我们将一起学习如何使用try-catch块捕获异常,以及如何通过set_error_handler函数定制错误响应。准备好让你的代码变得更加可靠,同时提供更友好的错误信息给最终用户。
|
2月前
|
开发框架 缓存 NoSQL
基于SqlSugar的数据库访问处理的封装,在.net6框架的Web API上开发应用
基于SqlSugar的数据库访问处理的封装,在.net6框架的Web API上开发应用
|
27天前
|
网络协议 NoSQL 网络安全
【Azure 应用服务】由Web App“无法连接数据库”而逐步分析到解析内网地址的办法(SQL和Redis开启private endpoint,只能通过内网访问,无法从公网访问的情况下)
【Azure 应用服务】由Web App“无法连接数据库”而逐步分析到解析内网地址的办法(SQL和Redis开启private endpoint,只能通过内网访问,无法从公网访问的情况下)
|
27天前
|
API
【Azure API 管理】在 Azure API 管理中使用 OAuth 2.0 授权和 Azure AD 保护 Web API 后端,在请求中携带Token访问后报401的错误
【Azure API 管理】在 Azure API 管理中使用 OAuth 2.0 授权和 Azure AD 保护 Web API 后端,在请求中携带Token访问后报401的错误
|
27天前
【Azure 应用服务】Web.config中设置域名访问限制,IP地址限制访问特定的页面资源 (Rewrite)
【Azure 应用服务】Web.config中设置域名访问限制,IP地址限制访问特定的页面资源 (Rewrite)

热门文章

最新文章