本文由作者原创于 更好的编程 on 中等的.

来源文章: SwiftUI:我们正在加载,我们正在加载……——如何让你的 API 调用加载一次……而且只加载一次。


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

如何让你的 API 调用加载一次……而且只加载一次。

UIKit and UIViewControllers gave us quite a few options in regard to controlling lifecycle events: viewDidLoad, viewWillAppear, viewDidAppear, viewWillDisappear, viewDidDisappear, and so on.

SwiftUI, on the other hand, basically gives us onAppear and onDisappear. So if we want to load some data for a view, we typically end up doing something like the following:

struct MyAccountListView: View {

    @StateObject var viewModel = MyAccountListViewModel()

    var body: some View {
        List {
            ForEach(viewModel.accounts, id: \.id) { account in
                NavigationLink(destination: Details(account)) {
                    AccountListCellView(account: account)
                }
            }
        }
        .onAppear {
            viewModel.load()
        }
    }
}

Just call load() in onAppear, and all is well with the world. Right?

好吧,如果您已经使用 SwiftUI 一段时间,那么您可能知道答案不是 相当 就这么简单。虽然以下大多数解决方案都相对简单,但我在互联网上看到了足够多的问题(和有问题的解决方案),表明它们也不是那么明显。

所以让我们开始吧。首先,我们需要了解手头的问题。

来来回回

第一个也是最明显的问题在于我们的导航链接。单击列表中的“帐户”,您将转到新帐户详细信息页面。但是当你从那个页面返回时会发生什么?

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

This problem can be exacerbated by the fact that SwiftUI (for reasons known only to SwiftUI) can also call onAppear and onDisappear handlers more than once during a given transition. It’s gotten better with this in 3.0, but it can still happen.

这并不重要 为什么, 可以?我们仍然有导航问题,我们仍然想加载我们的数据一次,而且只加载一次。

那么我们该怎么办呢?

标志

好吧,如果您已经编程超过几天,第一个(也是最明显的解决方案)是伸手去拿我们工具箱中的锤子并设置一个标志。考虑。

class MyAccountListViewModel: ObservableObject {

    @Published var accounts: [Account] = []

    private var loading = true

    func load() {
        guard loading else { return }
        shouldLoad = false
        // load our data here
    }
}

结案。问题解决了。但是,随着解决方案的发展,这个解决方案还有一些不足之处,因为我们必须在 viewModel 中声明变量,保护它,然后记得重置我们的标志。

而且它已经够挑剔了,我们可能想要编写一些额外的单元测试,以确保我们得到了一切正确。

总而言之,它有点......好吧,我们只是说它不是很优雅。我们能做得更好吗?

原子

好吧,我们可以导入新的 Atomics 库并消除额外的赋值语句。

private var loading = ManagedAtomic(true)

func load() {
  guard loading.exchange(false, ordering: .relaxed) else { return }
  // load our data here
}

The exchange function on the atomic value will set loading to the the new value (false), but but return the original value for evaluation. It eliminates the need for an extra line of code, but it does so at the cost of some complexity and the use of a library with which many Swift developers might not be conversant.

在这种情况下它也有点矫枉过正,因为这段代码不太可能是可重入的,也不太可能跨多个线程调用。

dispatch_once

在过去,当大量的 Objective-C 程序还在地球上运行时,我们可以使用 GCD 和 dispatch_once 来确保给定的代码块将被调用一次,并且只调用一次。

var token: dispatch_once_t = 0

func load() {
  dispatch_once(&token) {
    // load our data here
  }
}

Unfortunately, dispatch_once was deprecated in Swift 3.0, and attempting to use dispatch_once_t today gives you an error, telling you to use lazy variables instead. We could write our own version to handle this type of situation, but… lazy variables?

让我们考虑一下。

惰性变量

惰性变量在使用之前不会被实例化,Swift 保证所说的初始化只会发生一次。听起来就像我们需要的行为。

那么如果我们用一个延迟加载的函数替换我们的加载函数呢?

class MyAccountListViewModel: ObservableObject {

  @Published var accounts: [Account] = []

  lazy var load: () -> Void = {
     // load our data here
     return {}
  }()
}

Here we create a lazy variable with a closure that performs our load function and then returns an empty closure. The () added to the end ensures that the closure itself is evaluated when the variable is accessed.

因此,在这个解决方案中,我们的加载代码在第一次评估惰性函数时被调用,然后 空的 closure will be used whenever load() is called again.

Note that we could still pass a value to the load function if needed, noting, of course, that our stub closure returned would also need to reflect an empty, unused value { _ in }.

这个解决方案……还不错。它消除了额外的标志变量和保护,但代价是有点棘手,并且我们的加载例程被调用纯粹是作为初始惰性评估的副作用。

调用一次

当然,确保我们的代码只执行一次的最好方法是只调用一次。考虑以下对我们的视图模型的更改。

class MyAccountListViewModel: ObservableObject {

    enum State {
        case loading
        case loaded([Account])
        case empty(String)
        case error(String)
    }

    @Published var state: State = .loading

    func load() {
        // load our data here
    }
}

请注意我们的状态枚举以及我们现在正在处理错误、空状态等的事实。老实说,这是我们在现实生活中可能必须做的所有事情。

现在查看我们视图的相应更改。

struct MyAccountListLoadingView: View {

    @StateObject var viewModel = MyAccountListViewModel()

    var body: some View {
        switch viewModel.state {
        case .loaded(let accounts):
            AccountListView(accounts: accounts)
        case .empty(let message):
            MessageView(message: message, color: .gray)
        case .error(let message):
            MessageView(message: message, color: .red)
        case .loading:
            ProgressView()
                .onAppear {
                    viewModel.load()
                }
        }
    }
}

Here we display different views depending on the state of our view model, and that onAppear is now attached to our ProgressView. Since the initial state of our view model is .loading, the ProgressView “appears” and our load function is called.

加载帐户后,进度视图将被删除并替换为我们的帐户列表视图(或错误消息或空消息)。

But in any case the view hosting the onLoad modifier is removed and as such load() will never be called again.

我在 在 SwiftUI 中使用视图模型协议?你这样做是错的。 在那里,我还解释了如何将此方法与协议一起使用,以帮助测试和模拟数据。看看这个。

当然,如果你是偏执狂,你可以使用这种技术 and 早期的技术之一只是为了绝对正负载只会被调用一次。 (一种腰带和吊带的方法。)

拉动刷新

我们最终方法的另一个好处是,它使实现诸如拉动刷新之类的行为变得简单易行。

Just call load() in the view model again and when it’s finished load will update the result state again with new data or an error or a message.

You 可以 reset the state to .loading, but that would show our original progress view as well as the pull-to-refresh spinner, which probably isn’t the best user experience.

完成块

所以你有它。解决我们问题的几种方法。

有你自己的吗?在评论中告诉我。当然,如果您想查看更多内容,请拍手并订阅。

直到下一次。