这篇文章最初是由作者在 更好的编程 on 媒介.

原文: SwiftUI:我们正在加载,我们正在加载…-如何让你的API调用加载一次…而且只加载一次.


SwiftUI:我们正在加载,我们正在加载……

如何让你的API调用加载一次,而且只加载一次.

UIKit和UIViewControllers给了我们很多关于控制生命周期事件的选项: viewDidLoad, 那些有点, 在viewDidAppear, viewWillDisappear, viewDidDisappear,等等.

另一方面,SwiftUI基本上给了我们 onAppearonDisappear. 因此,如果我们想要为视图加载一些数据,我们通常会这样做:

struct MyAccountListView:视图{

    @StateObject var 视图模型 = MyAccountListViewModel()

    var主体:一些视图{
        {列表
            ForEach(视图模型.账户,id: \.Id){帐户在
                NavigationLink(目的地:详细信息(账户)){
                    AccountListCellView(账户:账户)
                }
            }
        }
        .onAppear {
            视图模型.load ()
        }
    }
}

就叫 load () in onAppear,世界就太平了. 正确的?

如果你用过SwiftUI,那么你可能知道答案是否定的 相当 那么简单. 虽然下面的大多数解决方案相对简单, 我在互联网上看到了足够多的问题(和可疑的解决方案),表明它们也不是那么明显.

我们开始吧. 首先,我们需要了解当前的问题.

来来去去

第一个,也是最明显的问题在于我们的导航链接. 点击列表中的“账户”,你就会进入一个新的账户详情页面. 但当你从那一页返回时会发生什么?

正确的. 您的视图再次“出现”,因此,加载数据的请求也再次发出.

SwiftUI(原因只有SwiftUI自己知道)也可以打电话,这可能会加剧这个问题 onAppearonDisappear 处理程序在给定的转换期间超过一次. 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 () 在视图模型中,当它完成时,加载将再次用新数据、错误或消息更新结果状态.

可以 将状态重置为 .加载, 但这将显示我们最初的进度视图以及“下拉刷新”旋转器, 哪个可能不是最好的用户体验.

完成块

这就是结果. 有很多方法可以解决我们的问题.

你自己也有一个? 请在评论中告诉我. 当然,如果你想看更多,请鼓掌并订阅.

直到下次.