From UIKit to SwiftUI: Navigating the Complexities of Infinite Scrolling

Daniel Zhang
4 min readAug 20, 2023

--

A heart rate graphing app that displays an unparalleled amount of data via an infinitely scrolling list helps users spot trends and patterns easily. Navigating the transition from UIKit’s table view to SwiftUI’s List was a complex hurdle, adding a layer of challenge to the creation process.

SwiftUI lacks much of the control of UIKit’s methods for optimizing table views like reusable cells, prefetching data, and fine-grained gesture recognition. We are seeing some of these shortcomings addressed in iOS 17 like with the much anticipated scrollPosition(id:anchor:).

For now, delivering a smooth, non-janky, infinite scrolling experience with SwiftUI means carefully refining operational efficiency without UIKit. Of course, the benefits of efficient code will stay in style even when SwiftUI is a distant memory.

I’m going to walk you through our process of fine-grained optimization of performance using Combine. We benefit from granular profiling using the amazing open-source TimeLane custom instrument by Marin Todorov.

If you are a project developer for HeartBond, open Instruments and TimeLane, installing it into Instruments if needed. Then choose Product > Profile in Xcode. You should then be able to record the app’s subscriptions and events. We are here to help if you have any questions.

We found an inefficiency related to the cancellation of chart data operations as list row views are first constructed. On rare occasions, it might cause the momentary display of a blank row. Our system has pathways for recovering from this condition, but preventing it from happening is a win for an improved user experience.

Figure 1: The “UH OH” Moment in Infinite Scrolling — This illustration highlights a potential pitfall where a view is left without its corresponding data. This situation occurs when an outer subscription is canceled, leading to a disconnect between the content and its presentation.

The process of profiling involves setting up an A/B test, toggling a single function between returning a Publisher with two different versions. We then analyze the results recorded by the TimeLane instrument. Our choosing Combine gave us the powerful capability to step into time like it is standing still.

Figure 2: Originally, multiDeviceChartDataPublisher would get canceled due to subsequent operations taking precedence.

We changed our updateMultiDeviceEntriesPublisher(day:) to no longer have susceptibility to cancellation of its chart entry adding. Once the operations complete, we do not have to repeat processing for a day. By preventing repetition, we save a few hundred milliseconds and prevent users from missing a view.

Figure 3: We changed our code to have chart data processing complete if the outer subscription is canceled. The result is a verifiable performance gain and prevention of a missing view. Please pay attention to the timings expressed in microsecond units, as they provide further verification of the performance optimizations in our code. A microsecond is 1000 times shorter in duration than a millisecond, allowing for a more precise measurement of improvements.

This optimization only scratches the surface of what is possible. It shows how we designed our project to be maintainable, scalable for future growth, while making verifiable performance gains.

Our app is already blazingly fast on Macs. Further refinements will bring more of that speed to iOS devices.

How do we have code run to completion when a subscription is canceled?

We use the technique of wrapping a Future in a Deferred publisher. This allows us to run code to completion when an outer subscription is canceled.

From the original version susceptible to cancellation, shown below, we adjusted chart data processing to finish during such an event. We verified the results with TimeLane in Instruments.

Wrapping the code in a Deferred Future publisher enables it to complete if the outer subscription is canceled.

How did we reduce jankiness when scrolling?

We observed the limitations of SwiftUI List scrolling and designed our view presentation to have a stepwise progression of data loading. This allows us to process data in a series of intervals that are short enough, and interleaved with small reliefs for UI updates, to lower jankiness. Those milliseconds we saved before can really add up. Our autoscrolling feature illustrates how we coordinate the loading of data with the scrolling of a List.

Take care with DispatchQueue.async

One of the most important things to remember when using Combine is you lose determinism in an operation chain once you dispatch to main, or another queue, asynchronously. Therefore, keeping such calls to a minimum, or not at all as in our code examples, is a good practice. Wrapping code in a Future can often fix problems related to dispatching.

Your feedback

We hope you enjoyed this article. Please let us know what you think in the comments below. Our apps are available for free on the App Stores. We would love to hear from you.

--

--

Daniel Zhang

Always learning when embracing diverse perspectives. Building software patterns designed for scalable collaboration and innovation.