This article was originally written by the author in Better Programming on Medium.
Original article: SwiftUI: We’re Loading, We’re Loading… – How to get your API calls to load once… and only once.
SwiftUI: We’re Loading, We’re Loading…
How to get your API calls to load once… and only once.
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?
Well, if you’ve done SwiftUI for awhile, then you probably know that the answer isn’t quite that simple. And while most of the following solutions are relatively straightforward, I’ve been seeing enough questions (and questionable solutions) across the internet to suggest that they’re not quite that obvious either.
So, let’s get started. First, we need to understand the issue at hand.
There and back again
The first, and most obvious problem lies with our navigation links. Click on an “account” in the list and you go to a new account details page. But what happens when you return from that page?
Correct. Your view “appears” again and as such the request to load your data is also made again.
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.
And it doesn’t really matter why, does it? We still have the navigation issue, and we still want to load our data one time, and one time only.
So, what do we do about it?
Flags
Well, if you’ve been programming more than a couple of days, the first (and most obvious solution) is to reach for the hammer in our toolbox and set a flag. Consider.
class MyAccountListViewModel: ObservableObject { @Published var accounts: [Account] = [] private var loading = true func load() { guard loading else { return } shouldLoad = false // load our data here } }
Case closed. Problem solved. But this solution, as solutions go, leaves a bit to be desired in that we have to declare the variable in our viewModel, guard on it, and then remember to reset our flag.
And it’s just finicky enough that we’d probably want to write a few extra unit tests just in order to make sure we’ve gotten everything correct.
All in all, it’s a bit… well, let’s just say it’s not very elegant. Can we do better?
Atomics
Well, we could import the new Atomics library and eliminate the extra assignment statement.
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 new value (false), 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.
It’s also overkill in this situation, as this code is highly unlikely to be reentrant and called across multiple threads.
dispatch_once
In the old days, back when massive Objective-C programs still walked the earth, we could use GCD and dispatch_once to ensure that a given block of code would be called one time, and one time only.
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?
Let’s think about that.
Lazy Variables
Lazy variables are not instantiated until they’re used, and Swift guarantees that said initialization will only occur once. Sounds exactly like the behavior we need.
So, what if we replace our load function with a lazily loaded function?
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.
So with this solution our loading code is called the first time our lazy function is evaluated and then the empty 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 }
.
This solution is… not bad. It eliminates the extra flag variable and guard, at the expense of being a bit tricky and getting our loading routine called purely as a side-effect of the initial lazy evaluation.
Calling it once
Of course, the best way to ensure that our code is only executed once is to only call it once. Consider the following changes to our view model.
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 } }
Note our state enumeration and the fact that we’re now handling errors, empty states, and the like. Which, to be honest, are all things we’d probably have to do in real life.
Now check out the corresponding change to our view.
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.
Once the accounts are loaded, the progress view is removed and replaced with our account list view (or an error message or empty message).
But in any case the view hosting the onLoad
modifier is removed and as such load()
will never be called again.
I wrote about this technique at some length in Using View Model Protocols in SwiftUI? You’re Doing it Wrong. There I also explained how this method can be used with protocols to help with testing and mocking data. Check it out.
Of course, if you’re paranoid, you could use this technique and one of the earlier techniques just in order to be absolutely positive load will only be called once. (Sort of a belt and suspenders approach.)
Pull to Refresh
Another nice thing about our final approach is that it makes implementing behaviors like pull-to-refresh simple and easy.
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 could 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.
Completion Block
So, there you have it. A handful of ways to solve our problem.
Got one of your own? Tell me about it in the comments. And, of course, clap and subscribe if you want to see more.
Until next time.

I write about Apple, Swift, and technology. I’m a Lead iOS engineer at CRi Solutions, a leader in cutting edge mobile corporate and financial applications.