Building Dynamic Lists in SwiftUI
The Ultimate Guide to SwiftUI List Views - Part 2
Previously, we looked at how to use List
views to create static list views. Static list views are useful for creating menus or settings screens in iOS apps, but List
views become a lot more useful when we connect them to a data source.
Today, we’re going to look at a couple of examples how you can use List
views to display a dynamic list of data, such as a list of books. We will also learn how to use some of the new features that Apple added to the latest version of SwiftUI in iOS 15, such as pull-to-refresh, a search UI, and an easy way to use async/await to fetch data from asynchronous APIs, such as remote services.
Displaying a list of elements
There are a number of ways to create lists, and as we will see later in this series, you can create both flat lists as well as hierarchical, nested lists. Since all list rows are computed on demand, List
views perform well even for collections with many items.
The easiest way to create a List
view based on a collection of elements is to use its constructor that takes a RandomAccessCollection
and a view builder for the row content:
Inside the view builder, we get access to the individual elements of the collection in a type-safe way. This means we can access the properties of the collection elements and use SwiftUI views like Text
to render the individual rows, like in the following example:
As this view acts as the owner of the data we want to display, we use a @StateObject
to hold the view model. The view model exposes a published property which holds the list of books. In the interest of simplicity, this is a static list, but in a real-world application you would fetch this data from a remote API or a local database.
Notice how we can access the properties of the Book
elements inside the List
by writing book.title
or book.author
. Here, we use a Text
view to display the title and the author of a book using string interpolation.
Thanks to SwiftUI’s declarative syntax, we can easily build more complex custom UIs to present data - just like we saw last time.
Let’s replace the Text
view in the above snippet with a more elaborate row that displays the book cover, title, author and number of pages:
Using Xcode’s refactoring tools for SwiftUI, we can extract this code into a custom view, to make our code easier to read.
Check out my video on building SwiftUI views to see this (and other) refactoring in action:
Since we’re not planning to modify the data inside the list row (or inside a details view), we pass the list item to the row as a simple reference. If we want to modify the data inside the list row (e.g. by marking a book as a favourite, or passing it on to an child screen where the user can edit the book details), we’ll have to use a list binding.
Using List Bindings to allow modifying list items
Normally, data inside a view is unmodifiable. To modify data, it needs to be managed as a @State
property or a @ObservedObject
view model. To allows users to modify data in a child view (e.g. a TextField
or a details screen), we need to use a binding to connect the data in the child view to the state in the parent view.
Until SwiftUI 3, there wasn’t a direct way to get a binding to the elements of the list, so people had to come up with their own solutions. I’ve written about this before in this blog post, in which I contrasted incorrect and correct ways to do this.
With SwiftUI 3, Apple has introduced a straight-forward way to access list items as bindings, using the following syntax:
To allow users of our sample app to edit the title of a book inline in the list view, all we have to do is to update the book list view as follows:
Of course, this also works for custom views. Here is how to update the BookRowView
to make the book title editable:
The key point here is to use @Binding
in the child view. By doing so, the parent view retains ownership of the data that you pass in to the child view, while letting the child view modify the data. The source of truth is the @Published
property on the ObservableObject
in the parent view.
To read more about list bindings, and how this feature works under the hood, check out my article SwiftUI List Bindings.
Asynchronously fetching data
The next sections of this article have one thing in common - they’re all based on Apple’s new APIs for handling asynchronous code.
At WWDC 21, Apple introduced Swift’s new concurrency model as part of Swift 5.5. This next version of Swift is currently in beta, but you can already get yours hands on it and experiment with the new features by downloading the beta versions of Xcode 13 from https://developer.apple.com/download/.
In the previous examples, we used a static list of data. The advantage of this approach is that we didn’t have to fetch (and wait for) this data, as it was already in memory. This was fine for the examples, as it allowed us to focus on what’s relevant, but it doesn’t reflect reality. In a real-world application, we usually display data from remote APIs, and this usually means performing asynchronous calls: while we’re waiting for the results to come in from the remote API, the app needs to continue updating the UI. If it didn’t do so, users might get the impression the app was hanging or even crashed.
So in the next examples, I’m going to demonstrate how to make use of Swift’s new concurrency model to handle asynchronous code.
A good moment to fetch data is when the user navigates to a new screen and the screen just appears. In previous versions of SwiftUI, using the .onAppear
view modifier was a good place to request data. Starting with iOS 15, SwiftUI includes a new view modifier that makes this even easier: .task
. It will start an asynchronous Task
when the view appears, and will cancel this task once the view disappears (if the task is still running). This is useful if your task is a long-running download that you automatically want to abort when the user leaves the screen.
Using .task
is as easy as applying it to your List
view:
In the view model, you can then use asynchronous APIs to fetch data. In this example, I’ve mocked the backend to make the code a bit easier to read, and added an artificial delay:
If you’d try and run the code like this, you would end up with a runtime warning, saying that “Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.”
The reason for this runtime error is that the code inside refreshable
is not executed on the main thread. However, UI updates must be executed on the main thread. In the past, we would’ve had to use DispatchQueue.main.async { … }
to make sure any UI updates are executed on the main thread. However, with Swift’s new concurrency model, there is an easier way: all we have to do is to any methods (or classes) that perform UI updates using the @MainActor
property wrapper. This instructs the compiler to switch to the main actor when executing this code, and thus make sure any UI updates run on the main thread. Here’s the updated code:
To learn more about Swift’s new concurrency model, check out this video series on YouTube, as well as the following articles on my blog:
- Getting Started with async/await in SwiftUI - SwiftUI Concurrency Essentials
- Cooperative Task Cancellation - SwiftUI Concurrency Essentials
Pull to refresh
Unless you use an SDK like Cloud Firestore that allows you to listen to updates in your backend in realtime, you will want to add some UI affordances to your app that make it easy for your users to request the latest data. One of the most common ways to let users refresh data is pull to refresh, made popular in 2008 by Loren Brichter in the Tweetie app (later acquired by Twitter and relaunched as Twitter for iOS).
SwiftUI makes it easy to add this functionality to your app with just a few lines of code, thanks to its declarative nature. And - as mentioned above - this feature also makes use of Swift’s new concurrency model, to ensure that your app’s UI remains responsive even while it needs to wait for any updates to arrive.
Adding the refreshable
view modifier to your List
view is all it takes to add pull to refresh to your app:
As indicated by the await
keyword, refreshable
opens an asynchronous execution context. This requires that the code you’re calling from within refreshable
can execute asynchronously (if the code you’re calling can execute non-asynchronously, because it returns immediately, that’s fine as well, but more often than not you’ll want to communicate with a remote API that requires being called asynchronously).
To give you an idea of how this might look like, I’ve created a view model that simulates an asynchronous remote API by adding some artificial wait time:
Let’s take a look at this code to understand what’s going on.
- As in the previous samples,
books
is a published property that the view subscribes to. generateNewBook
is a local function that produces a random newBook
instance using the excellent LoremSwiftum library.- Inside
refresh
, we callgenerateBook
to produce a new book and then insert it into the published propertybooks
, but before we do so, we tell the app to sleep for 2 seconds, using theTask.sleep
call. This is an asynchronous call, so we need to useawait
to call it.
Just the same as in the previous example, this code will produce a purple runtime warning: “Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates”, so we need to use @MainActor
to ensure all updates happen on the main actor. This time, instead of marking just the refresh
method, we’re going to mark the entire view model as @MainActor
:
One final adjustment before we can wrap up this section: you will notice that, when adding new items to the list by pulling to refresh, the newly added items will appear instantly, without a smooth transitions.
Thanks to SwiftUI’s declarative syntax, adding animations to make this feel more natural is super easy: all we need to do is adding an animation
view modifier to the List
view:
By providing the value
parameter, we can make sure this animation is only run when the contents of the list view changes, for example when new items are inserted or removed.
To perfect the animations, we’ll also add a short pause to the end of the refresh
function on the view model - this makes sure that the new rows appear with a smooth transition before the progress spinner disappears:
Searching
SwiftUI makes it easy to implement search in List
views - all you need to do is apply the .searchable
view modifier to the list view, and SwiftUI will handle all the UI aspects for you automatically: it will display a search field (and make sure it is off screen when you first display the list view, just like you’d expect from a native app). It also has all the UI affordances to trigger the search and clear the search field).
The only thing that’s left to do is to actually perform the search and provide the appropriate result set.
Generally speaking, a search screen can either act locally (i.e. filter the items being displayed in a list view), or remotely (i.e. perform a query against a remote API, and only display the results of this call).
For this section, we’re going to look at filtering the elements being displayed in the list view. To do so, we’ll be using a combination of async/await and Combine.
To get started, we’ll build a simple List
view that displays a list of books from a view model. This should look very familiar to you, as we’re in fact reusing much of the code we’ve used for the previous examples:
The view model is very similar to the ones we used previously, with one important difference: the collection of books is empty initially:
To add a search UI to SearchableBooksListView
, we apply the .searchable
view modifier, and bind its text
parameter to a new searchTerm
property on the view model:
This will install the search UI in the List
view, but if you run this code, nothing will happen. In fact, you won’t even see any books in the list view.
To change this, we will add a new private property to the view model which holds the original unfiltered list of books. And finally, we will set up a Combine pipeline that filters this list based on the search term entered by the user:
How does this Combine pipeline work?
- We use
Publishers.CombineLatest
to take the latest state of the two publishers,$originalBooks
and$searchTerm
. In a real-world application, we might receive updates to the collection of books in the background, and we’ll want these to be included in the search result as well. TheCombineLatest
publisher will publish a new tuple containing the latest value oforiginalBooks
andsearchTerm
every time one of those publishers send a new event. - We then use the
.map
operator to transform the(books, searchTerm)
tuple into an array of books that we eventually assign to the published$books
property, which is connected to theSearchableBooksListView
. - Inside the
.map
closure, we usefilter
to return only the books that contain the search term either in their title or in the author’s name. This part of the process actually is not Combine-specific -filter
is a method onArray
.
If you run this code, you will notice that everything you type into the search field will be auto-capitalised. To prevent this, we can apply the . autocapitalization
view modifier - after the searchable
view modifier:
Closure
List views in SwiftUI are very powerful, and with the features announced at WWDC 21, Apple is closing many of the gaps that existed between UIKit and SwiftUI’s List
API.
Some of the features are a bit hidden, but thankfully, the official documentation is catching up, and it always pays off to read the source documentation in the SwiftUI header file. It’d be even better if we had access to SwiftUI’s source code, but I wouldn’t hold my breath for this one.
In the meantime, I hope this series of articles helps to shed some light into what’s probably one of the most used user interface paradigms on Apple’s platforms. Feel free to connect with me on Twitter, and if you have any questions or remarks, don’t hesitate to send a tweet or DM.
In the next episode, we will look into styling List
views - Apple has added some exciting styling capabilities in the latest release, making it easier to adopt List
s to your app’s branding.
Thanks for reading, and until next time!
Improve your app's UX with SwiftUI's task view modifier
Mastering the art of the pause
SwiftUI Hero Animations with NavigationTransition
Replicating the App Store Hero Animation
Styling SwiftUI Views
How does view styling work?
Previewing Stateful SwiftUI Views
Interactive Previews for your SwiftUI views
Asynchronous programming with SwiftUI and Combine
The Future of Combine and async/await