【锁定直播】字节、华为云、阿里云等技术专家讨论如何将大模型接入 AIOps 解决实际问题,戳>>> 了解详情
写点什么

如何实现 SwiftUI 微服务?

  • 2019-12-03
  • 本文字数:5993 字

    阅读完需:约 20 分钟

如何实现SwiftUI微服务?

在 SwiftUI 中实现应用程序架构的现代方法有哪些?本文从 SwiftUI 的发展过程切入,进而对 SwiftUI 的前沿状况进行了分析,并解答了一些关于 SwiftUI 微服务的问题。


SwiftUI 在 iOS、macOS 和其他所有苹果设备上,为应用程序开发带来了一种新的声明式、状态驱动和基于组件的方法。与此同时,我们的应用程序架构方法也应该向前发展了。但在我们前进之前,先来简要回顾一下历史和现在的前沿状况。

Model View Controller

开发 iOS 应用程序的经典方法是站在 MVC(模型-视图-控制器)的肩膀之上的。在 MVC 中,控制器(controller)在模型(model)和代表我们接口的各种视图(view)之间来回传递信息。



在 iOS 中,控制器将自身显示为单个对象,UIViewController。视图控制器管理所有用户交互和状态更改——包括信息加载、操作和数据更新,它还处理用户在我们的应用中各个屏幕和页面之间的来回跳转。


这种方法意味着控制器在我们的应用程序体系结构中负担着过于繁重的任务。实际上,它的作用是如此之大,以至于人们普遍称其为"巨型视图控制器"(Massive View Controller)。


显然,这种方法并不是最佳的。

Model-View-ViewModel

为了取代巨型视图控制器,人们提出了很多解决方案,其中大多数方案都能归结为某种形式的模型-视图-视图模型(MVVM)。


在这种方法中,模型和视图以及视图控制器仍然存在。但应用程序的内部结构、数据处理和业务逻辑已经从视图控制器提取出来,移到了视图模型(ViewModel)中。


为什么这样做呢?一方面来说它简化了视图控制器,但之所以从视图控制器中提取所有逻辑,主要目的是让这些逻辑可测试。我们可以实例化视图模型,并向其提供信息并调用其方法,还能直接观察被视图模型呈现给视图控制器的状态更改。



由于视图控制器的工作已简化为,只把这些状态更改传递给构成我们应用程序的视图,因此我们可以确信,只要视图模型的输出正确,我们的应用程序也将正确。


这种方法有多种变体:模型-视图-呈现器(MVP,model-view-presenter);VIPER;Clean。但它们都是基于相同的基本概念,主要区别在于它们如何在一组组件之间划分职责。


但所有人都认同一件事,那就是视图控制器应该尽可能地简单。

SwiftUI

苹果公司显然同意这一点,并在 WWDC 19 大会上推出了 SwiftUI,其一大特性就是取消了大多数用户定义和托管视图的控制器。在 SwiftUI 中,你可以使用一种简单的语法来声明你的用户界面。


此外,该接口完全由任意给定时间点的应用程序状态驱动。更改应用程序状态时,应用程序界面将立即更新以反映这些更改。苹果将​​此概念称为“单一事实来源”(Single Source of Truth)。



WWDC19“通过 SwiftUI 的数据流”讲座


但是,应用程序的任何给定部分都应有一个单一事实来源,并不一定意味着整个应用程序也应该有一个单一事实来源。


搞糊涂了?下面具体解释。

Composition

正如我在文章“SwiftUI中的View Composition”中所写的那样,苹果鼓励你将视图分解为许多小的、紧凑的、独立的组件,其中每个视图控制用户界面的一个特定部分。


我们再来看一下那篇文章中的一个组件,是一个收藏按钮(下图右上),用于指示给定项目应该已经被记录了,并显示在应用的“收藏夹”(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 = <a href="">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)        }    }}</a href="">
复制代码

Redux

由于应用程序的每个组件都应该有一个单一事实来源,因此有人提议 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 中,我们的应用程序完全由其状态控制。因此,如果我们将该状态放入模型或服务,并且该状态由于用户触发的操作而更改,那么在测试中我们就可以触发这些操作并观察状态的变化。


如果状态针对每个可能的更改或动作能正确更新,那么我们就能对我们的应用程序是否正确具有很高的信心。


但将所有状态放到一个容器中,就很难单独测试各个模型或服务。这样做很适合集成测试,但不适合单元测试。


即便如此,因为全局状态的关系,出现未测试内容并因此产生预期之外的副作用的可能性也非常高。“哦,我没意识到它也在改变那个变量!”

Late binding

正如我在 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和缺少的环境对象》这篇文章中所述。


    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 环境。这就是为什么其成员是私有的原因。

SwiftUI 中的微服务

微服务架构的含义是将应用程序安排为一组松散耦合的服务。这些服务是细粒度的,它们之间的协议是轻量级的。


在微服务架构中,服务是可独立部署的。拿上面的 FavoritesService 的例子来说,我们看到了我们可以轻松地在另一个应用程序中重新部署这个服务和对应的接口组件。


最后,将它们称为"微"服务进一步强化了这样的理念,也就是说我们的服务应该小巧、定义明确,并且每个服务的实现都应针对性管理我们应用程序的某个方面。


单一事实来源。

结语

如果 SwiftUI 背后的主要目标是使用结构良好、独立且可复用的视图来构建应用程序,那么我们是否应该考虑以相同的方式实现内部服务架构?


这是我的观点,但如果你有其他意见,我也想听听。


注意:我的 iDine 应用程序源代码版本可从 GitHub 上的 iDine 仓库获取。它包括本文提到的示例


作者介绍:


Michael Long 是 CRi Solutions 的 iOS 高级首席工程师,这家公司是一流的 iOS、Android 以及移动公司和金融应用程序的开发商。


原文链接


https://medium.com/better-programming/swiftui-microservices-c7002228710


2019-12-03 14:364920

评论

发布
暂无评论
发现更多内容

2023年ha软件采购就选Skybility HA!6大优势看这里!

行云管家

高可用 ha 双机热备

火山引擎DataTester:无需研发人力,即刻开启企业A/B实验

字节跳动数据平台

A/B测试

两步开启研发团队专属ChatOps|极狐GitLab ChatOps 的设计与实践

极狐GitLab

团队管理 DevOps ChatOps 极狐GitLab ChatGPT

ClickHouse 挺快,esProc SPL 更快

王磊

如何使用 Towify 在小程序中实现勾选用户协议后登录?

Towify

微信小程序 无代码

2023年中国企业数字化技术应用十大趋势

易观分析

企业 数字化

【合作案例】科协基地预约小程序 | 闵行区科普资源地图

天天预约

如何在滑至页面底端添加提示?

Towify

微信小程序 无代码

Wallys/industrial M.2 card/DR9074E vs DR90746E/Minipcie /qsdk/qcn9074

wallysSK

QCN9074 QCN9024 QCN9072 qcn9064

激活海量数据价值,实现生产过程优化|智慧工厂系列专题07

EMQ映云科技

人工智能 物联网 IoT 云边协同 12 月 PK 榜

低碳正在成为春城的新名片

Geek_2d6073

2022高通人工智能开发者大会暨高通人工智能应用创新大赛颁奖典礼圆满落幕

科技热闻

下一代架构?从组装式企业到组装式应用

华为云开发者联盟

云计算 后端 数字化 华为云 12 月 PK 榜

Tapdata 携手阿里云,实现数据平滑上云以及毫秒级在线查询和检索能力

云布道师

阿里云

省会城市昆明分布式光伏项目落地 引领低碳化转型实践

Geek_2d6073

人工智能顶会AAAI 2023放榜!网易伏羲7篇论文入选

网易伏羲

人工智能

OpenMLDB 贡献者任务第六期 | 暖冬时节,活力继续

第四范式开发者社区

人工智能 机器学习 数据库 开源 特征

从数据治理到数据应用,制造业企业如何突破数字化转型困境丨行业方案

袋鼠云数栈

数字化转型

VoneBaaS与飞腾CPU完成产品兼容性互认证

旺链科技

区块链 产业区块链 VoneBaaS 12 月 PK 榜

强化学习调参技巧二:DDPG、TD3、SAC算法为例:

汀丶人工智能

强化学习 深度强化学习 12月日更 12月月更

熹乐科技范维肖CC:基于开源 YoMo 框架构建“全球同服”的 Realtime Metaverse Application

声网

框架 #开源

了不起的程序员们,瞧,你的 2023 年度惊喜终于来了!

图灵社区

程序员

HIFIVE音加加提供曲库、评分、修音功能的K歌SDK-iOS版本

HIFIVE音加加

ios 泛娱乐 版权音乐 K歌 K歌SDK

【服务故障问题排查心得】「内存诊断系列」Docker容器经常被kill掉,k8s中该节点的pod也被驱赶,怎么分析?

洛神灬殇

Docker Linux 12 月 PK 榜 容器内存问题

IAA品类洞察:扫描品类加快变现,如何抓住增长机遇?

易观分析

广告业 IAA

选择合适的BI工具,解决中国式报表难题

对不起该用户已成仙‖

chatGPT实战之「基于你的数据库,为你智能生成SQL」

非喵鱼

Java MySQL sql openai ChatGPT

Github标星42K!不愧是腾讯云大神亲码的“redis深度笔记”

架构师之道

编程 程序员 java面试

团队新人多,稳定性经验不足,研发质量怎么保障?|TakinTalks论道

TakinTalks稳定性社区

技术管理

最近大火的高性能计算ChatGPT究竟是什么?

蓝海大脑GPU

深度学习 高性能计算, ChatGPT

优化 20% 资源成本,新东方的 Serverless 实践之路

Serverless Devs

Serverless

如何实现SwiftUI微服务?_大前端_Michael Long_InfoQ精选文章