Complex list development and performance optimization from the perspective of iOS native development

Estimated read time 24 min read

I have finally shared the complex list of cross-platform and front-end development. Of course, I will continue to share more detailed and in-depth lists in the future. But now, as an iOS developer, I can finally talk about a list of one of the most common and important components in the process of developing iOS Apps.

A seemingly simple list often contains various complex requirements and optimization challenges in actual development. Today, let me share with you some of my experience and thoughts in the development of iOS complex lists over the years.

1. Complex list styles and application scenarios in iOS development

In iOS development, list components mainly correspond to two classes: UITableView and UICollectionView. UITableView is mainly used to display a vertically arranged list, while UICollectionView can achieve a more flexible layout. Through these two classes, we can implement various common complex list styles:

1. Group list

A grouped list is the most common type of complex list, which divides list items into different groups according to certain rules, and each group has its own head and tail. Typical application scenarios include address books, settings pages, etc.

2. Nested lists

A nested list means that each item in the list is itself a list, and the user can expand or collapse each item. This style is often used to display tree-structured data, such as file browsers, multi-level menus, etc.

3. Waterfall list

The characteristics of the waterfall list are that the list items are of different sizes, compactly arranged, and continuous like a waterfall. It is usually used for the display of pictures or cards, such as picture walls, product lists, etc.

4. Left and right linkage list

The left and right linkage list consists of two lists, the left is the classification list and the right is the content list. When the user selects different categories on the left, the content on the right will change accordingly. This style is often used for news classification in news clients, product classification in e-commerce apps, etc.

5. Chat bubble list

The chat bubble list is a common list style in IM social apps. Its characteristic is that the list items are bubble-shaped, and there are two types of bubbles on the left and right (representing two chat roles).

6. Card stack list

The stacked list of cards gives a cascading visual effect, usually the top card is fully displayed and the lower cards are only partially exposed. Users can swipe up and down to switch cards. This kind of list is often used to display information flows, such as social updates, recommended content, etc.

The above are several common complex list styles and their application scenarios in iOS development. It can be seen that although they are all lists, different styles give people completely different visual feelings and interactive experiences. As developers, we need to choose an appropriate list style based on the design needs of the App.

2. Several development methods for iOS complex lists

For the development of complex lists, iOS provides several different implementation methods, each with its own advantages and disadvantages.

1. UITableView + Cell subclassing

This is the most traditional and commonly used method. We describe the style and behavior of the list items by defining a subclass of UITableViewCell, and then return Cell instances and related data in UITableViewDelegate and UITableViewDataSource.

The advantages of this method are simple and intuitive, and strong control. But when there are many types of Cells and the logic is complex, it will cause the ViewContoller to be too bloated. Maintenance costs are higher.

2. UICollectionView + Cell subclassing

UICollectionView provides a more flexible layout method than UITableView, and is especially suitable for implementing some irregular list styles, such as waterfall flow, circular list, etc.

Similarly, we also need to define a subclass of UICollectionViewCell and implement protocol methods such as UICollectionViewDelegate and UICollectionViewDataSource.

Compared with UITableView, UICollectionView is much more flexible in layout, but it is also more complex. We need to implement an additional subclass of UICollectionViewLayout to define various parameters of the layout.

3. Development method based on MVVM

MVVM is a popular architectural pattern that decouples View and Model by introducing ViewModel as an intermediate layer, making the code clearer and easier to test.

In the MVVM mode, we usually encapsulate the data, status, business logic, etc. of the list in the ViewModel. The Cell is only responsible for obtaining data from the ViewModel and displaying it. This can avoid the problem of the View Controller being too bloated.

Common MVVM binding libraries include RxSwift, ReactiveCocoa, etc. They provide a set of declarative APIs that allow us to elegantly establish binding relationships between View and ViewModel.

4. Development method based on Texture (formerly AsyncDisplayKit)

Texture is an open source UI framework from Facebook. Its core idea is to encapsulate various properties of the UI (layout, rendering, updates, etc.) into a Node object, and then use this Node in a way similar to UIView.

The biggest feature of Texture is that its layout and rendering are asynchronous and concurrent, and will not block the main thread. This gives it a huge advantage in performance optimization of complex lists.

In Texture, we describe list items by defining a subclass of ASCellNode, similar to UITableViewCell. But ASCellNode is more lightweight because it is not a real View, just a container for configuration information.

5. SwiftUI’s List component

If your project uses the SwiftUI framework, the list development method is different. SwiftUI provides a List component that can easily create various lists.

In SwiftUI, we describe the UI through declarative syntax, and each item in the list is an independent View. This method is very concise and the code is highly readable.

However, SwiftUI is still in the early stages of development, and there are still some limitations in performance and functionality, especially for complex list scenarios.

The above are several common complex list development methods in iOS development. In actual projects, we need to choose the appropriate method based on specific needs and the team’s technology stack.

3. Highly adaptable and optimized iOS complex lists

In the development of complex lists, a common requirement is that the height of the list items is not fixed and needs to be adaptive according to the content. This requirement brings considerable challenges to list performance optimization.

1. Why does high adaptability affect performance?

In UITableView and UICollectionView, if we do not specify the height of the Cell in advance, the system needs to dynamically calculate its height each time the Cell is loaded. This calculation process usually requires the following steps:

  1. Create Cell instance
  2. Fill data into Cell
  3. Call the Cell layoutSubviewsmethod to let the subviews relayout
  4. Call the Cell sizeThatFitsmethod to calculate the optimal size of the Cell
  5. Returns the calculated dimensions

It can be seen that this process is synchronous and time-consuming. Especially when the layout of the Cell is complex and the content changes dynamically, the calculation overhead will be very large. If there are hundreds or thousands of Cells, this calculation process will seriously block the main thread and cause the list to freeze.

2. Highly cached

In order to avoid the overhead of repeatedly calculating Cell height, a common optimization strategy is height caching. That is, after we calculate the height of the Cell for the first time, we cache it. The next time we load a Cell with the same indexPath, we directly use the cached height instead of recalculating it.

Here is a simple highly cached implementation:

class TableViewController: UITableViewController {
    var dataSource: [String] = []
    var heightCache: [IndexPath: CGFloat] = [:]
    
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if let height = heightCache[indexPath] {
            return height
        }
        
        let height = calculateHeight(at: indexPath)
        heightCache[indexPath] = height
        return height
    }
    
    func calculateHeight(at indexPath: IndexPath) -> CGFloat {
        // 创建一个临时的Cell实例
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! CustomCell
        // 填充数据
        cell.configure(with: dataSource[indexPath.row])
        // 计算高度
        let height = cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
        return height
    }
}

In this example, we define a heightCachedictionary to cache the height, the key is IndexPath, and the value is the corresponding height. In tableView(_:heightForRowAt:)the method, we first check whether the height of the IndexPath already exists in the cache, and if so, return it directly; if not, call calculateHeight(at:)the method to calculate the height and store the result in the cache.

This method can significantly reduce the number of height calculations and improve the scrolling performance of the list. But the space occupied by the cache is also a problem. If there are too many list items, the memory overhead of the cache will be relatively large. Therefore, we also need to clear the cache at the right time (such as when we receive a memory warning).

3. Highly predictable

In addition to height caching, another commonly used optimization strategy is height estimation. That is, we do not need to accurately calculate the height of each Cell, but give an estimate. This estimate does not need to be completely accurate, as long as it can ensure that the content of the Cell will not be truncated.

In UITableView, we can tableView(_:estimatedHeightForRowAt:)provide an estimated height by implementing methods. The system will use this estimated height as the initial value to lay out the Cell, and then dynamically adjust its height when the Cell is actually displayed.

override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return 100
}

This method can significantly reduce the number of height calculations, because most of the Cell’s height calculations occur when it is visible, rather than all calculations at once. Also, since the estimated height is usually smaller than the actual height, the initial loading of the list will also be faster.

But this approach also has certain limitations. If the difference between the estimated height and the actual height is too large, it may cause the list to jump when scrolling. Therefore, we need to provide an estimate as close to the actual height as possible.

4. Asynchronous layout and rendering of Texture

As mentioned earlier, the core advantage of the Texture framework is that its layout and rendering are asynchronous and will not block the main thread. This is a huge help for highly adaptive list performance optimization.

In Texture, we layoutSpecThatFitsdefine the layout of Cell by overriding the method of ASCellNode:

override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
    let titleNode = ASTextNode()
    titleNode.attributedText = NSAttributedString(string: "Title")
    
    let subtitleNode = ASTextNode()
    subtitleNode.attributedText = NSAttributedString(string: "Subtitle")
    
    let verticalStack = ASStackLayoutSpec(direction: .vertical,
                                          spacing: 5,
                                          justifyContent: .start,
                                          alignItems: .stretch,
                                          children: [titleNode, subtitleNode])
                                          
    return ASInsetLayoutSpec(insets: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10),
                             child: verticalStack)
}

In this example, we create two text nodes ( titleNodeand subtitleNode), then combine them with a vertical StackLayout, and finally add some padding.

When Texture needs to calculate the height of the Cell, it will call layoutSpecThatFitsthe method on the background thread, cache the result, and then apply the layout when the Cell is actually displayed. The entire process is asynchronous and will not block the main thread.

In addition, Texture also provides some other optimization methods, such as preloading, intelligent compression, incremental rendering, etc., which can further improve the performance of complex lists.

5. SwiftUI’s LazyVStack and LazyHStack

In SwiftUI, the height adaptation of the list is supported by default, and we do not need to manually calculate or cache the height of the Cell. This benefits from SwiftUI’s declarative syntax and responsive update mechanism.

SwiftUI provides LazyVStacktwo LazyHStackcomponents for creating vertical and horizontal lazy loading lists. They will only be created and loaded when the Cell is visible, which can significantly reduce memory usage.

struct ContentView: View {
    let data = (0..<100).map { "Item ($0)" }
    
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(data, id: .self) { item in
                    Text(item)
                        .padding()
                }
            }
        }
    }
}

In this example, we create an array containing 100 strings as the data source for the list, and then use the LazyVStackand ForEachloop to create the list. SwiftUI automatically handles Cell height calculation and layout.

4. Performance assessment and optimization strategies for complex lists

In addition to being highly adaptive, performance optimization of complex lists also needs to be considered from many aspects. Now let me share some performance assessment indicators and optimization strategies that I have summarized in practice.

1. Performance evaluation indicators

To optimize the performance of lists, we first need to have a set of quantifiable performance indicators. Commonly used indicators include:

  • FPS (Frames Per Second): Indicates the frame rate when the list is scrolling, reflecting the smoothness of the list. Generally speaking, the higher the FPS, the better the user experience. Usually we use 60FPS as the optimization target.
  • CPU usage: Indicates the CPU usage when scrolling the list. Excessive CPU usage will cause other tasks (such as network requests, animations, etc.) to be affected, and will also increase battery consumption.
  • Memory usage: Indicates the memory size occupied by the list. Excessive memory usage may cause the App to crash or be terminated by the system.
  • Loading time: Indicates the time from the beginning of loading to the complete display of the list. If the loading time is too long, it will affect the user experience.

In order to obtain these indicators, we can use the Instruments tools provided by Xcode, such as Core Animation, Time Profiler, Allocations, etc. By analyzing the results of these tools, we can identify bottlenecks in list performance.

2. Optimize Cell reuse

Cell reuse is a basic means for optimizing list performance. Through reuse, we can avoid constantly creating and destroying Cells when the list is scrolled, thereby reducing CPU and memory overhead.

In UITableViewand UICollectionView, Cell reuse is dequeueReusableCell(withIdentifier:for:)achieved through methods. We need to provide a reusable identifier when registering the Cell, and then use this identifier in the cellForRowAtor cellForItemAtmethod to obtain a reusable Cell instance.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomCell
    // 配置Cell...
    return cell
}

It should be noted that when reusing a Cell, we need to ensure that the Cell is reset to a clean state to avoid residual data from the last use. Usually we can prepareForReuseperform reset operations in the Cell method.

3. Asynchronous loading and preloading

For some time-consuming operations, such as downloading and decoding images, we can consider using asynchronous loading and preloading technology to avoid blocking the main thread.

Asynchronous loading refers to performing time-consuming operations on a background thread, and then updating the UI on the main thread after the operation is completed. This prevents time-consuming operations from blocking scrolling of the list. Common asynchronous loading libraries include SDWebImage, Kingfisher, etc.

Preloading means loading the data and resources required by the Cell in advance before it appears on the screen. This can reduce the loading time when Cell appears and improve user experience. The preload range needs to be adjusted appropriately based on the device’s performance and network conditions.

The following is a simple image preloading implementation:

override func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
    for indexPath in indexPaths {
        let imageUrl = dataSource[indexPath.row].imageUrl
        prefetchImage(from: imageUrl)
    }
}

func prefetchImage(from url: URL) {
    URLSession.shared.dataTask(with: url) { (data, response, error) in
        // 缓存图片数据...
    }.resume()
}

In this example, we implement UITableViewDataSourcePrefetchingthe protocol tableView(_:prefetchRowsAt:)method, in which we obtain the image URL of the upcoming Cell and call prefetchImage(from:)the method to download the image data in the background thread.

4. Partial update

When the data in the list changes, we need to update the display of the list. A simple way is to call reloadDatathe method and let the list reload all data. However, the efficiency of this method is relatively low, especially when the list data is large, it will cause obvious lags.

A better approach is to use partial updates, which update only the part of the data that has changed. UITableViewand UICollectionViewprovides a series of local update methods, such as:

  • insertRows(at:with:)
  • deleteRows(at:with:)
  • reloadRows(at:with:)
  • moveRow(at:to:)

Through these methods, we can control the update of the list in a more fine-grained manner and reduce unnecessary redrawing and layout.

func updateDataSource() {
    let oldCount = dataSource.count
    dataSource.append("New Item")
    
    let indexPath = IndexPath(row: oldCount, section: 0)
    tableView.insertRows(at: [indexPath], with: .automatic)
}

In this example, when a new piece of data is added to the data source, we use insertRows(at:with:)methods to partially update the list instead of reloading the entire list.

5. Avoid unnecessary layout and drawing

As the list scrolls, the layout ( layoutSubviews) and drawing ( draw) of the list are triggered every frame. If time-consuming operations are performed in these two methods, the list will be stuck.

Therefore, we need to try to avoid performing complex calculations or modifying the view hierarchy in the layoutSubviewsand methods. drawFor some views that need to be updated in real time (such as UILabel), we can consider using asynchronous drawing or pre-rendering technology.

In addition, we also need to pay attention to the redraw range of the list. By default, the redraw range of the list is the entire visible area. But sometimes, we only need to redraw a small part of the list (such as a single Cell). At this time, we can reduce the redrawing scope to the scope of a single Cell through the overridden attributes UIView.contentMode

class CustomCell: UITableViewCell {
    override var contentMode: UIView.ContentMode {
        didSet {
            contentView.contentMode = contentMode
            textLabel?.contentMode = contentMode
            detailTextLabel?.contentMode = contentMode
        }
    }
}

In this example, we override contentModethe properties of Cell and apply them to the Cell’s contentView and subviews. In this way, when the content of the Cell changes, only the Cell itself will be redrawn without affecting other Cells.

5. Analysis of the differences and advantages and disadvantages between front-end lists and iOS lists

With the rise of front-end cross-platform frameworks such as React Native, Flutter, and Weex, more and more apps are beginning to use front-end technology to develop iOS applications. So, what is the difference between the front-end developed iOS list and the native list? What are their respective advantages and disadvantages?

1. Development efficiency

One of the advantages of front-end development is its high development efficiency. Front-end frameworks usually use declarative UI syntax, which can quickly build the page structure. Many front-end frameworks also provide a rich library of UI components that developers can use directly without developing from scratch.

Taking React Native as an example, we can use the FlatList component to quickly create a list:

<FlatList
  data={[{key: 'a'}, {key: 'b'}]}
  renderItem={({item}) => <Text>{item.key}</Text>}
/>

In contrast, native development requires manually creating UITableView or UICollectionView and implementing a series of DataSource and Delegate methods, which results in relatively low development efficiency.

2. Performance

Although front-end frameworks are constantly optimizing their performance, native development still has performance advantages in complex list scenarios. This is mainly due to the following reasons:

First of all, when the front-end framework renders the list, it needs to pass the data from JavaScript to the native layer, and then the native layer renders it. This process will have certain communication overhead. Native development, on the other hand, renders directly on the native layer without this overhead.

Secondly, the list component of the front-end framework is usually implemented based on UIScrollView, while the native UITableView and UICollectionView are specially optimized for list scenarios. They have better performance in memory management, reuse mechanism, rendering efficiency, etc.

Thirdly, native development can more directly utilize some features provided by the system, such as automatic layout and preloading of UITableViewCell. The front-end framework is limited by its encapsulation and sometimes cannot fully utilize the characteristics of the system.

Of course, this does not mean that front-end frameworks cannot develop high-performance lists. Through some optimization methods, such as paging loading, lazy loading, avoiding repeated rendering, etc., we can still achieve a smooth list experience in the front-end framework.

3. Dynamic

One advantage of front-end development is its strong dynamic ability. Front-end frameworks usually use JavaScript as the development language, which can dynamically modify the page structure and style at runtime. This is very beneficial for some business scenarios that require frequent iterations.

For example, we can modify the style or interaction logic of the list without publishing it by issuing JavaScript scripts. This is particularly important in rapidly changing fields such as e-commerce and information.

Native development is limited by the characteristics of compiled languages, and its dynamic capabilities are relatively weak. Although iOS also provides some dynamic methods, such as JSPatch, WaxPatch, etc., the threshold for their use is high, and Apple’s restrictions on dynamics are becoming increasingly strict.

4. Development costs

Another advantage of front-end development is the low cost of development. Since JavaScript is a universal web development language, there are a large number of front-end developers. In contrast, native iOS development requires specialized Objective-C or Swift development skills.

In addition, front-end frameworks usually use a single JavaScript code base to support both iOS and Android platforms. This “develop once, run many places” feature can significantly reduce development and maintenance costs. Native development requires separate development and maintenance of code libraries for each platform, which is relatively expensive.

But it should be noted that the cross-platform nature of the front-end framework also means that we cannot fully utilize the features of each platform. Sometimes in order to take into account cross-platform considerations, we have to give up some platform-specific functions or performance optimizations. This is also a point that front-end development needs to weigh.

5. User experience

In terms of user experience, native development usually performs better. Native controls have been carefully designed and optimized in terms of interactive experience, animation effects, gesture support, etc., and can provide users with a more natural and smooth experience.

In contrast, due to its cross-platform nature, front-end frameworks sometimes have difficulty in completely simulating the experience of native controls. Although front-end frameworks are constantly improving their animation performance and gesture support, native development still has certain advantages in some scenarios that require higher interactive experience.

On the other hand, front-end development is conducive to maintaining a consistent experience between iOS and Android platforms. By sharing the same set of UI components and interaction logic, the front-end framework can ensure consistent page performance on both platforms. In native development, due to differences in platform characteristics, it is difficult to achieve a consistent experience on both platforms.

6. Performance assessment and optimization strategies for complex lists

In addition to being highly adaptive, performance optimization of complex lists also needs to be considered from many aspects. Now let me share some performance assessment indicators and optimization strategies that I have summarized in practice.

Through the above analysis, we can see that front-end iOS list development and native list development each have their own advantages and limitations. Front-end development emphasizes development efficiency, dynamic capabilities and cross-platform support, while native development emphasizes performance, user experience and utilization of system features.

In actual projects, we need to choose appropriate technical solutions based on specific business needs and team situations. Some core businesses that have higher requirements for performance and interactive experience, such as homepage information flow, real-time chat, etc., are more suitable for native development; while some businesses that have higher requirements for development efficiency and dynamics, such as event pages, content details pages, etc. , more suitable for front-end development.

At the same time, we do not have to completely oppose front-end development and native development. Many teams adopt a hybrid development model, which is to embed the front-end framework in the native framework to achieve the complementary advantages of the two. For example, we can embed the React Native page as a Cell in the native UITableView, taking advantage of the performance advantages of the native list and the dynamic advantages of the front-end framework.

In short, as an iOS developer, we need to understand both front-end development and native development, and choose flexibly according to the actual situation, rather than dogmatically insisting on a certain technology. Only by using a variety of technologies comprehensively can we develop the best list experience.

7. Outlook

Looking to the future, I think there are several other trends and directions in iOS list development:

1. Intelligent list

With the development of artificial intelligence technology, future lists may become more intelligent. For example, the list can automatically adjust the sorting and filtering of content based on the user’s preferences, reading history, time, location and other information, and recommend the content that the user is most interested in.

Lists can also automatically optimize their layout and design through machine learning algorithms. For example, based on the user’s clicking and scrolling behavior, the list can dynamically adjust the height, spacing, font size, etc. of each Item to provide the best reading experience.

2. Dynamic list

With the popularization of the 5G era, the content of the list will be more real-time and dynamic. For example, live broadcast list, real-time bidding list, online collaboration list, etc. This places higher requirements on the performance of the list, which requires the list to respond quickly to data changes and update the UI smoothly.

At the same time, dynamic lists also pose new challenges to the front-end framework. How to provide a flexible UI update mechanism while ensuring performance will be a problem that the front-end framework needs to solve.

3. Interactive innovation of lists

As mobile devices continue to upgrade, the interaction methods of lists are also constantly innovating. For example, the emergence of new technologies such as 3D Touch, tactile feedback, and gesture recognition provides more possibilities for list interaction.

List interactions may become more natural and intuitive in the future. For example, users can control the scrolling speed of lists through pressure sensitivity, merge and split list items through gestures, and filter and search lists through voice.

4. Visualization of lists

In addition to traditional text and picture lists, the content presentation of future lists may be more diverse and visual. For example, VR/AR list, 3D list, chart list, etc. These new types of lists not only provide a more intuitive and attractive visual experience, but also help users understand and analyze list data faster.

5. Personalization of lists

Future lists may focus more on personalization and customization. The list content, style, and interaction methods seen by each user may be different. This requires list frameworks and developers to provide flexible configuration and customization capabilities, and to quickly respond to the personalized needs of different users.

8. Reference materials

Here are some inspirational and helpful reference resources for this article:

  1. WWDC 2018: High Performance Auto Layout  – Introducing performance optimization techniques for automatic layout
  2. WWDC 2019: Advances in Collection View Layout  – Introducing new features and improvements to CollectionView layout
  3. ASDK Documentation  – The official documentation of Texture (ASDK), which introduces its asynchronous rendering architecture
  4. IGListKit Documentation  – The official documentation of IGListKit, introducing its data-driven list framework
  5. React Native Documentation  – The official documentation of React Native, which introduces its principles and basic usage
  6. Flutter Documentation  – Flutter’s official documentation, introducing its responsive UI framework
  7. SwiftUI Documentation  – The official documentation of SwiftUI, introducing its declarative UI syntax
  8. Weex Documentation  – The official documentation of Weex, introducing its cross-platform solution
  9. Flutterverse  – contains a large number of Flutter-related articles and practical cases, including list development and optimization
  10. RxSwift  – a functional reactive programming framework that can be used for list data processing and state management

Of course, iOS list development is a very broad and in-depth topic, and there are many excellent articles, books, and open source libraries worth learning and referring to. We need to continuously absorb and apply this knowledge in practice, and combine it with our own thinking and innovation to design and develop an excellent list experience.


The above is all I have shared about complex list development for iOS. I hope these contents can give you some inspiration and help. You are also welcome to leave messages and share your experiences and ideas. Let us discuss and optimize the development of mobile lists to create a better product experience for users.

thank you all!

You May Also Like

More From Author

+ There are no comments

Add yours