Preamble To celebrate the conclusion of our 7-part series on “Modern SwiftUI,” we are releasing a blog post each day this week exploring a modern, best practice for SwiftUI development. Today we show how make lists of data in your SwiftUI applications safer and more performant, but be sure to catch up on the other posts:
The List
and ForEach
views in SwiftUI are foundational and incredibly easy to get started with. Compared to the days of UIKit, lists have never been easier. However, if used naively, in particular using plain arrays, it is possible to introduce subtle bugs and crashes to your applications.
We will describe how to avoid those problems by using the IdentifiedArray
type, which allows you to read and modify elements of a collection by their stable ID rather than positional index.
By far the easiest way to model data for lists is to use a plain array:
struct StandupsList: View {
@State var standups: [Standup] = […]
var body: some View {
List {
ForEach(standups) { standup in
StandupRow(standup: standup)
}
}
}
}
However, using plain arrays often leads one to referencing its elements by its positional index. Doing this can be precarious, leading to corrupt data and even crashes.
For example, because ForEach
deals primarily with Identifiable
types, it is common that we have the stable ID of an element that we need to convert to a positional index so that we can perform some work, say, removing the element:
func deleteStandup(id: Standup.ID) {
guard let index = standups.firstIndex(where: { $0.id == id })
else { return }
standups.remove(at: index)
}
Try searching your code base for “.firstIndex(where
” to see how many times you do this yourself. Unfortunately, this code is both inefficient and dangerous.
It is a potential performance problem because you are linearly scanning an array to find an element by its ID. If your collection has thousands of elements (or hundreds of thousands of elements!), this can be a serious problem.
Further, this code is not safe. Suppose that we have an API service to communicate with when deleting the standup. If we do this naively:
func deleteStandup(id: Standup.ID) async throws {
guard let index = standups.firstIndex(where: { $0.id == id })
else { return }
try await apiClient.delete(id: id)
standups.remove(at: index)
}
…then we can accidentally update the wrong standup or even crash. While the API client is suspending, it is possible for the standups
array to shuffle its elements or even remove some elements when the API request is in flight. So, after the suspension resumes, the index
may no longer correspond to the correct element, or may even fall outside the bounds of the array.
To fix this you must always recompute indices you use after every suspension point, and if there are multiple suspension points then you may need to compute the index multiple times.
SwiftUI is well aware of the problems of using positional indices in lists of data, and that’s why ForEach
forces data types to have a stable identifier via the Identifiable
protocol. Unfortunately, there is no type that ships with the Swift standard library to embrace this pattern in your domain modeling. That’s precisely the gap that IdentifiedArray aims to fill.
In our series on “Modern SwiftUI” we rebuilt Apple’s “Scrumdinger” application from scratch to showcase modern, best practices, and the first change we made was to scrap plain arrays when modeling data for lists. Instead, we made use of our IdentifiedArray data type, which allows referencing elements by their stable ID rather than their unstable positional index.
In practice, this simply means changing code like this:
var standups: [Standup] = []
…to code like this:
import IdentifiedCollections
var standups: IdentifiedArrayOf<Standup> = []
Even with that change, all code should continue to compile because identified arrays mostly behave like regular arrays. However, they come with additional APIs that allow for the safe and efficient reading and modifying of elements by their ID. Such as removing an element by its ID:
func deleteStandup(id: Standup.ID) async throws {
try await apiClient.delete(id: id)
standups.remove(id: id)
}
Because we are now removing the element by its ID, it does not matter how long the API client suspends for, we will always remove the correct element.
We can also update an element by its ID:
standups[id: standup.id] = standup
…and more.
That’s it for now. We hope you have learned how to better model lists of data in your SwiftUI applications. By embracing our IdentifiedArray
data type you can more efficiently read and modify elements in your lists, and do so more safely.
Check back in tomorrow for the 3rd part of our “Modern SwiftUI” blog series, where we show how to more concisely model your domains for navigation in SwiftUI.