在 SwiftUI 中实现应用程序架构的现代方法有哪些?本文从 SwiftUI 的发展过程切入,进而对 SwiftUI 的前沿状况进行了分析,并解答了一些关于 SwiftUI 微服务的问题。
SwiftUI 在 iOS、macOS 和其他所有苹果设备上,为应用程序开发带来了一种新的声明式、状态驱动和基于组件的方法。与此同时,我们的应用程序架构方法也应该向前发展了。但在我们前进之前,先来简要回顾一下历史和现在的前沿状况。
开发 iOS 应用程序的经典方法是站在 MVC(模型 - 视图 - 控制器)的肩膀之上的。在 MVC 中,控制器(controller)在模型(model)和代表我们接口的各种视图(view)之间来回传递信息。
在 iOS 中,控制器将自身显示为单个对象,UIViewController。视图控制器管理所有用户交互和状态更改——包括信息加载、操作和数据更新,它还处理用户在我们的应用中各个屏幕和页面之间的来回跳转。
这种方法意味着控制器在我们的应用程序体系结构中负担着过于繁重的任务。实际上,它的作用是如此之大,以至于人们普遍称其为"巨型视图控制器"(Massive View Controller)。
显然,这种方法并不是最佳的。
为了取代巨型视图控制器,人们提出了很多解决方案,其中大多数方案都能归结为某种形式的模型 - 视图 - 视图模型(MVVM)。
在这种方法中,模型和视图以及视图控制器仍然存在。但应用程序的内部结构、数据处理和业务逻辑已经从视图控制器提取出来,移到了视图模型(ViewModel)中。
为什么这样做呢?一方面,它简化了视图控制器,但之所以从视图控制器中提取所有逻辑,主要目的是让这些逻辑可测试。我们可以实例化视图模型,并向其提供信息并调用其方法,还能直接观察被视图模型呈现给视图控制器的状态更改。
由于视图控制器的工作已简化为,只把这些状态更改传递给构成我们应用程序的视图,因此我们可以确信,只要视图模型的输出正确,我们的应用程序也将正确。
这种方法有多种变体:模型 - 视图 - 呈现器(MVP,model-view-presenter)、VIPER、Clean。但它们都是基于相同的基本概念,主要区别在于它们如何在一组组件之间划分职责。
但所有人都认同一件事,那就是视图控制器应该尽可能简单。
苹果公司显然同意这一点,并在 WWDC 19 大会上推出了 SwiftUI,其一大特性就是取消了大多数用户定义和托管视图的控制器。
在 SwiftUI 中,你可以使用一种简单的语法来声明你的用户界面。
此外,该接口完全由任意给定时间点的应用程序状态驱动。更改应用程序状态时,应用程序界面将立即更新以反映这些更改。
苹果将此概念称为“单一事实来源”(Single Source of Truth)。
WWDC19“通过 SwiftUI 的数据流”讲座
但是,应用程序的任何给定部分都应有一个单一事实来源,并不一定意味着整个应用程序也应该有一个单一事实来源。
搞糊涂了?下面具体解释。
正如我在文章“SwiftUI 中的 View Composition[1]”中所写的那样,苹果鼓励你将视图分解为许多小的、紧凑的、独立的组件,其中每个视图控制用户界面的一个特定部分。
我们再来看一下那篇文章中的一个组件,是一个收藏按钮(下图右上),用于指示给定项目应该已经被记录了,并显示在应用的“收藏夹”(favorites)列表中。
收藏按钮背后的代码如下:
struct FavoritesButton: View {
let item: MenuItem
@EnvironmentObject var favorites: FavoritesService
var imageName: String {
favorites.isFavorite(item) ? "star.fill" : "star"
}
var body: some View {
Image(systemName: imageName)
.foregroundColor(.accentColor)
.scaleEffect(1.2)
.onTapGesture {
self.favorites.toggleFavorite(self.item)
}
}
}
收藏按钮的界面和行为是完全自包含的,可以用在我们应用程序中任何视图的任何位置。如屏幕截图所示,我们甚至可以将其放入导航栏中。
struct DetailView: View {
let item: MenuItem
var body: some View {
ScrollView(.vertical) {
VStack {
...
}
}
.navigationBarTitle("Details", displayMode: .inline)
.navigationBarItems(trailing: FavoritesButton(item: item))
}
}
点击导航栏中的收藏按钮,当前项目会被标记为收藏状态。再点一下会移除收藏。无论如何,DetailView 都不了解按钮的内部细节或实现。
尽管收藏按钮界面背后的代码是自包含的,但视图的基本功能在内部依赖 FaovritesService,这是一个已定义的环境对象,已插入视图层次结构中的较高层级上。
FavoritesService 是一个 SwiftUI ObservableObject(可观察对象),它向我们的视图暴露一个发布的值和两个方法。一个是 isFavorite(item) 方法,该方法确定该项目是否已收入收藏夹;另一个是 toggleFavorite(item),该方法根据收藏情况切换项目的状态。
请注意,从此处或应用程序中的任何位置调用 toggleFavorite(item) 时,我们的收藏夹项目列表都会更新,进而依赖 FavoritesService 的任何视图都会被要求更新其视图表示。
class FavoritesService: ObservableObject {
@Published var items: [MenuItem] = []
func isFavorite(_ menuItem: MenuItem) -> Bool {
items.firstIndex(where: { $0.id == menuItem.id }) != nil
}
func toggleFavorite(_ menuItem: MenuItem) {
if let index = items.firstIndex(where: { $0.id == menuItem.id }) {
items.remove(at: index)
} else {
items.append(menuItem)
}
}
}
FavoritesService 是此特定视图的单一事实来源。它对于其他视图也可能是一个事实来源,但 FavoritesButton 不关心这个。FavoritesService 还遵守“单一责任原则”。它的目的是管理收藏夹菜单项列表,仅此而已。
我们看一下另一种服务,是一个非常简单的服务。
enum AppTabs: Int {
case favorites
case menu
case order
}
class AppState: ObservableObject {
@Published var currentTab = AppTabs.favorites
}
我们在这里跟踪应用程序的当前标签页状态,这样就可以根据需要在程序中转到特定标签页。
struct AppTabView: View {
@EnvironmentObject var appState: AppState
var body: some View {
TabView(selection: $appState.currentTab) {
FavoritesView()
.tabItem {
Image(systemName: "star")
Text("Favorites")
}
.tag(AppTabs.favorites)
...
}
}
}
还有一个服务。这里是来自同一应用的 OrderService,用于跟踪已订购的商品。
class OrderService: ObservableObject {
@Published var items = [MenuItem]()
var total: Int {
items.reduce(0) { $0 + $1.price }
}
func isInCart(_ menuItem: MenuItem) -> Bool {
items.firstIndex(where: { $0.id == menuItem.id }) != nil
}
func add(item: MenuItem) {
items.append(item)
}
func remove(item: MenuItem) {
if let index = items.firstIndex(of: item) {
items.remove(at: index)
}
}
}
由于应用程序的每个组件都应该有一个单一事实来源,因此有人提议 SwiftUI 应转向 Redux 风格的状态模型,整个应用程序应该有一个单一事实来源。
class AppState: ObservableObject {
@Published var currentTab = AppTabs.favorites
@Published var menuItems: [MenuItem] = []
@Published var favoriteItems: [MenuItem] = []
@Published var orderItems: [MenuItem] = []
}
或者,如果你想维护组件行之间的功能,则可以尝试以下操作:
class AppState: ObservableObject {
@Published var currentTab = AppTabs.favorites
@Published var menu = MenuService()
@Published var favorites = FavoritesService()
@Published var order = OrderService()
}
将全局 AppState 导入到各个需要数据的视图中,就完成了。
单个 AppState 的优点主要在于简单性。如前所述,你只需要处理一个 environmentObject 导入即可。
但对我来说,它的缺点有很多。
首先,它们会影响性能。对应用程序状态进行单个更改(例如,将单个项目标记为收藏状态),现在需要遍历应用程序中的每个单一视图树并检查更改。为什么?因为每个视图依赖的单一环境对象都发出了信号,表示一个更新已经发生了。
对于较小的应用程序,这里的性能影响可能不大。但是对于更大的应用呢?
(应该注意,这也是大型 React/Redux Web 应用程序面临的问题。)
我认为的第二大缺点涉及应用程序数据的全局暴露。
将 AppState 导入到单个视图中,然后所有内容都会暴露给所有人查看。既然如此,如果不仔细检查视图的每行代码,你如何确定特定视图可能正在访问或操纵的信息是什么?
上面的 FavoritesButton 就是一个很好的反例。只要看一下代码的开头部分,我就能看出这段代码可以看到或更改的唯一内容就是 FavoritesService,因为这是从应用程序环境中导入的唯一对象。
此外,如果我想在其他应用程序中使用 FavoritesButton,也很容易看出我还需要转移哪些内容到其他应用程序中。
第三个缺点涉及测试。我们将代码分解为视图模型和服务的主要动机之一,就是让代码更容易测试。
在 SwiftUI 中,我们的应用程序完全由其状态控制。因此,如果我们将该状态放入模型或服务,并且该状态由于用户触发的操作而更改,那么在测试中我们就可以触发这些操作并观察状态的变化。
如果状态针对每个可能的更改或动作能正确更新,那么我们就能对我们的应用程序是否正确具有很高的信心。
但将所有状态放到一个容器中,就很难单独测试各个模型或服务。这样做很适合集成测试,但不适合单元测试。
即便如此,因为全局状态的关系,出现未测试内容并因此产生预期之外的副作用的可能性也非常高。“哦,我没意识到它也在改变 那个 变量!”
正如我在 view-composition 那篇文章中所指出的,另一个 SwiftUI 最佳实践是将状态绑定到层次结构中尽可能低的位置上。
WWDC 19“通过 SwiftUI 的数据流”讲座
当我们绑定在层次结构中较低的位置时,由于任意给定更新只会影响视图树的一小部分,因而极大地减少了所需的接口更新和重新渲染的次数。
所有这些都显著提高了应用程序性能。
在上面的例子中,FavoritesService 直接绑定到需要它的对象上。要显示收藏按钮的 DetailView 既不知道也不在乎此事。当然,应该有较高级别的事物来提供它,但这是其他事物的另一种职责。
有人可能会问为什么我们称它们为服务,而不只是视图模型。
在这里,关键的区别因素在于,视图模型通常是为驱动单个屏幕、页面或视图而编写的,并且该视图拥有该视图模型。
另一方面,服务会被注入应用程序环境中视图层次结构的某个级别,供较低级别的元素使用,从而在 SwiftUI 应用程序中的多个视图和组件之间共享。只要这个级别持续存在,对应的服务就会持续存在。
实际上,当在 SceneDelegate 中创建初始内容视图时,往往会创建许多服务并将它们注入到视图层次结构的最顶层。
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// 创建 SwiftUI 视图,用来提供窗口内容。
let contentView = AppTabView()
.environmentObject(AppState())
.environmentObject(MenuService())
.environmentObject(MessageService())
.environmentObject(FavoritesService())
.environmentObject(OrderService())
.environmentObject(RatingsService())
// 使用一个 UIHostingController 作为窗口根视图控制器。
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
尽管更好的解决方案可能是使用一个系统服务修饰符,如“SwiftUI 和缺少的环境对象[2]”这篇文章中所述。
let contentView = AppTabView()
.modifier(SystemServices())
使用服务修饰符时如下所示:
struct SystemServices: ViewModifier {
private static var appState: AppState = AppState()
private static var menu = MenuService()
private static var messages = MessageService()
private static var favorites = FavoritesService()
private static var ratings = RatingsService()
private static var order = OrderService()
func body(content: Content) -> some View {
content
// defaults
.accentColor(.red)
// messages
.overlay(MessageOverlayView(), alignment: .top)
// services
.environmentObject(Self.appState)
.environmentObject(Self.menu)
.environmentObject(Self.messages)
.environmentObject(Self.favorites)
.environmentObject(Self.order)
.environmentObject(Self.ratings)
}
}
请注意,我们的 SystemServices 修饰符仅用来在需要时(例如当我们提供新的模态视图或动作表时)将服务注入 SwiftUI 环境。这就是为什么其成员是私有的原因。
微服务架构的含义是将应用程序安排为一组松散耦合的服务。这些服务是细粒度的,它们之间的协议是轻量级的。
在微服务架构中,服务是可独立部署的。拿上面的 FavoritesService 的例子来说,我们看到了我们可以轻松地在另一个应用程序中重新部署这个服务和对应的接口组件。
最后,将它们称为"微"服务进一步强化了这样的理念,也就是说我们的服务应该小巧、定义明确,并且每个服务的实现都应针对性管理我们应用程序的某个方面。
单一事实来源。
如果 SwiftUI 背后的主要目标是使用结构良好、独立且可复用的视图来构建应用程序,那么我们是否应该考虑以相同的方式实现内部服务架构?
这是我的观点,但如果你有其他意见,我也想听听。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/bxo1uPvGw8n231MCXPE4xg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。