原文: SwiftUI:我们正在加载,我们正在加载…-如何让你的API调用加载一次…而且只加载一次.
SwiftUI:我们正在加载,我们正在加载……
如何让你的API调用加载一次,而且只加载一次.
UIKit和UIViewControllers给了我们很多关于控制生命周期事件的选项: viewDidLoad
, 那些有点
, 在viewDidAppear
, viewWillDisappear
, viewDidDisappear
,等等.
另一方面,SwiftUI基本上给了我们 onAppear
和 onDisappear
. 因此,如果我们想要为视图加载一些数据,我们通常会这样做:
struct MyAccountListView:视图{ @StateObject var 视图模型 = MyAccountListViewModel() var主体:一些视图{ {列表 ForEach(视图模型.账户,id: \.Id){帐户在 NavigationLink(目的地:详细信息(账户)){ AccountListCellView(账户:账户) } } } .onAppear { 视图模型.load () } } }
就叫 load ()
in onAppear
,世界就太平了. 正确的?
如果你用过SwiftUI,那么你可能知道答案是否定的 相当 那么简单. 虽然下面的大多数解决方案相对简单, 我在互联网上看到了足够多的问题(和可疑的解决方案),表明它们也不是那么明显.
我们开始吧. 首先,我们需要了解当前的问题.
来来去去
第一个,也是最明显的问题在于我们的导航链接. 点击列表中的“账户”,你就会进入一个新的账户详情页面. 但当你从那一页返回时会发生什么?
正确的. 您的视图再次“出现”,因此,加载数据的请求也再次发出.
SwiftUI(原因只有SwiftUI自己知道)也可以打电话,这可能会加剧这个问题 onAppear
和 onDisappear
处理程序在给定的转换期间超过一次. 3分钟后情况就好转了.0,但它仍然可能发生.
这并不重要 为什么, 它? 我们仍然有导航问题,我们仍然想一次加载数据,而且只加载一次.
那么我们该怎么做呢?
旗帜
好吧, 如果你已经编程好几天了, 第一个(也是最明显的解决方案)是在我们的工具箱中找到锤子并设置一个标志. 考虑.
类MyAccountListViewModel: ObservableObject { @Published var accounts: [Account] = [] 私有var加载= true 函数load () { 守卫加载else {return} shouldLoad = false //加载数据 } }
情况下关闭. 问题解决了. 但这个解决方案, 作为解决方案, 还有一点需要改进,因为我们必须在视图模型中声明变量, 警卫在上面, 然后记得重置我们的旗帜.
它很挑剔,我们可能会写一些额外的单元测试来确保一切都是正确的.
总而言之,它有点,呃,这么说吧,它不是很优雅. 我们能做得更好吗?
原子
我们可以导入新的原子库,并删除额外的赋值语句.
私有var加载= ManagedAtomic(true) 函数load () { 警卫队加载.交易所(错误,命令: .放松的)else {return} //加载数据 }
的 交换
函数将加载设置为新值(false)。, 但是返回原始值来求值. 它消除了额外一行代码的需要, 但这样做的代价是一些复杂性和使用的库,许多Swift开发人员可能不熟悉.
在这种情况下,这也太过分了, 因为这段代码不太可能是可重入的,也不太可能跨多个线程调用.
dispatch_once
在过去, 回到大量Objective-C程序还在地球上运行的时候, 我们可以使用GCD和dispatch_once来确保给定的代码块只被调用一次, 只有一次.
Var令牌:dispatch_once_t = 0 函数load () { dispatch_once (&令牌){ //加载数据 } }
不幸的是, dispatch_once
已在Swift 3.0,并且正在尝试使用 dispatch_once_t
今天给你一个错误,告诉你使用惰性变量代替. 我们可以编写自己的版本来处理这种情况,但是……惰性变量?
我们想一下.
懒惰的变量
惰性变量在使用之前不会被实例化, Swift保证初始化只会发生一次. 听起来正是我们需要的行为.
那么,如果我们用一个惰性加载函数替换load函数会怎样呢?
类MyAccountListViewModel: ObservableObject { @Published var accounts: [Account] = [] lazy var load: () -> Void = { //加载数据 返回{} }() }
这里,我们创建了一个带有闭包的惰性变量,该闭包执行load函数,然后返回一个空闭包. 的 ()
添加到末尾确保在访问变量时计算闭包本身.
在这个解决方案中,我们的加载代码在惰性函数第一次被求值时被调用,然后 空 闭包将在任何时候使用 load ()
又被称为.
注意,如果需要,我们仍然可以向load函数传递一个值, 注意的是, 当然, 我们的存根闭包返回的也需要反映一个空的, 未使用的价值 {_ in}
.
这个解决方案……还不错. 它消除了额外的标志变量和保护, 代价是有点棘手,我们的加载例程被调用纯粹是初始惰性求值的副作用.
调用一次
当然,确保代码只执行一次的最好方法是只调用它一次. 考虑对视图模型的以下更改.
类MyAccountListViewModel: ObservableObject { {枚举状态 情况下加载 例加载(账户) 空(字符串) 情况下误差(字符串) } @Published var state: state = .加载 函数load () { //加载数据 } }
请注意我们的状态枚举,以及我们现在正在处理错误、空状态等. 老实说,这些都是我们在现实生活中可能要做的事情吗.
现在检查对视图的相应更改.
结构MyAccountListLoadingView:视图{ @StateObject var 视图模型 = MyAccountListViewModel() var主体:一些视图{ 切换视图模型.{状态 情况下 .加载(让账户): AccountListView(账户:账户) 情况下 .空(让信息): MessageView(消息:消息,颜色: .灰色) 情况下 .错误(让消息): MessageView(消息:消息,颜色: .红色) 情况下 .加载: ProgressView () .onAppear { 视图模型.load () } } } }
在这里,我们根据视图模型的状态显示不同的视图 onAppear
现在连接到我们的 ProgressView
. 因为视图模型的初始状态是 .加载
, ProgressView
“出现”,我们的负载函数被调用.
一旦账户被加载, 进度视图被删除并替换为帐户列表视图(或错误消息或空消息).
但无论如何,视图承载 onLoad
修饰词被删除 load ()
再也不会被叫来了.
我写了一些关于这个技巧的文章 使用SwiftUI中的视图模型协议? 你做错了. 我还解释了如何将此方法与协议一起使用,以帮助测试和模拟数据. 检查一下.
当然,如果你是偏执狂,你可以使用这个技巧 和 早期的一种技术只是为了绝对正负载将只被调用一次. (有点像腰带和吊带.)
拉刷新
我们的最后一种方法的另一个优点是,它使实现像拉刷新这样的行为变得简单和容易.
就叫 load ()
在视图模型中,当它完成时,加载将再次用新数据、错误或消息更新结果状态.
你 可以 将状态重置为 .加载
, 但这将显示我们最初的进度视图以及“下拉刷新”旋转器, 哪个可能不是最好的用户体验.
完成块
这就是结果. 有很多方法可以解决我们的问题.
你自己也有一个? 请在评论中告诉我. 当然,如果你想看更多,请鼓掌并订阅.
直到下次.

我写苹果、斯威夫特和科技. 我是CRi 解决方案的首席iOS工程师, 领先的移动企业和金融应用程序.