Dynamic Lists With Asynchronous Data Loading in SwiftUI
Join the DZone community and get the full member experience.Join For Free
SwiftUI is an exciting new way to create UI on Apple platforms. But, it is still a young technology, and it can sometimes be hard to find information needed to support the implementation of more advanced use cases.
In porting an app to SwiftUI, I’ve been struggling a bit with the
List is a very powerful container view that makes it incredibly easy to create table style components.
But if you want to up your game by adding more features, such as dynamically loading more rows as the the user scrolls, or fetching data asynchronously, it gets a little trickier. That’s when you would like to know more about how
List works its magic behind the scenes — but information is scarce about things like cell reuse, redrawing, handling animations, etc.
After some experimentation (and trial and error), I managed to come up with a list that met some more advanced requirements. I also created a few helper components that I would like to share — in case someone else is looking for similar solutions.
The requirements for my list are as follows:
- The list should grow dynamically and batch-wise as the user scrolls.
- The data on each row is fetched from a (possibly) slow data source — must be possible to be performed asynchronously in the background.
- Some row data should be animated in two different situations: a) when data has been fetched and b) whenever a row becomes visible.
- Possibility to reset the list and reload data.
- Smooth scrolling throughout.
I will now walk you through the essential parts of the solution. The complete code is shown at the end of this article and can also be downloaded here: https://github.com/callistaenterprise/SwiftUIListExample.
We start with some of the plumbing, which consists of a few protocols and generic components that may be used as a foundation for any list of this type.
First off is the data model, and we begin with the
ListDataItem protocol, which should be adopted by the component representing row data. It must have an
Int index that is used to determine when more data needs to be loaded, more on this later.
It also contains a function —
fetchData — that retrieves data for the row in a second, possibly slower step.
Next out is the
ListDataProvider, a generic class that maintains the actual list of data items, stored in a published property that is used directly by the
fetchMoreItemsIfNeeded is called by the view when a new row is presented. It will check if more items need to be fetched and will also initiate the loading of additional data for each row.
prefetchMargin can be used to fine-tune the list behavior.
Now, we come to the second part of the plumbing, which concerns the views. The structure is similar to that of the data model components. There is one protocol,
DynamicListRow, that the row view should adopt and a generic struct,
DynamicList, which is used as the actual list view.
DynamicList may need some further explanation. The generic parameter
Row is a custom view that is used to visualize the row in any fashion you would like. The list gets it data from
listProvider , a property of the type
ListDataProvider that we defined above.
onAppear modifier on the row view is there to ensure that more items are fetched as needed by calling
fetchMoreItemsIfNeeded every time a row becomes visible.
There is also an
id modifier on the list view itself. What is the purpose of that, you may ask? The
listID property provides a unique id that remains the same until the list is reset, in which case a new unique id is generated. This ensures that the list is completely redrawn when it is reset. Leaving it out could sometimes cause the list to not fetch enough data when reset, especially for small batch sizes.
That’s all for the plumbing. Let’s move on to an example of how to use this to create our list.
Here is an example of how to implement a list using the components we’ve defined above.
To be able to demonstrate how it works with asynchronous data loading, we start with a very simple datastore, called
SlowDataStore. It offers a static function that will give us a random amount between 0 and 1 as a publisher, which takes between 0.5 and 2.0 seconds to deliver the value implemented as a
usleep on a background queue.
Next, we create our custom data item in the class
MyDataItem which conforms to the
ListDataItem protocol. It is a quite straightforward implementation of the protocol requirements. Note that
dataPublisher needs to be a stored property in order to keep the publisher from the
SlowDataStore alive while waiting for the data to arrive.
The final part of the list is our custom row view, which conforms to the
DynamicListRow protocol. The row contains a horizontal stack with three elements:
- A text view displaying the line number.
- A text view displaying "Loading..." if data is not yet available, otherwise the amount is displayed.
- A custom graph bar view that displays the amount graphically. The implementation of
GraphBaris given with the complete solution at the end.
The animation is triggered by the two modifiers on the horizontal stack. The first one,
onReceive, is used when data first arrives from the
SlowDataStore. The second one,
onAppear, is used when the row appears and data is already available.
Note: The animation triggered by
onAppearneeds to be mutually exclusive, otherwise the animation will not work correctly. That’s why we test the property
We now just need to wrap it all up in a container view, as shown below.
I hope this article contains some useful information that will help you build your own lists based on similar requirements. Please let me know what you think, and feel free to post any questions, comments, or suggestions.
The complete code is given below. You can also download sample XCode-project here: https://github.com/callistaenterprise/SwiftUIListExample.
Complete Source Code
Published at DZone with permission of Anders Forssell, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.