Reusable Cells in UIKit and Their Differences in SwiftUI

Yury Lebedev
7 min readApr 13, 2024

--

Introduction

Effective resource management when displaying large lists of data is a key task in iOS app development. UIKit, having served as the foundation for building user interfaces for many years, offers the concept of reusable cells through UITableView and UICollectionView. However, with the advent of SwiftUI, approaches to interface management have significantly changed. Let’s examine how reuse works in UIKit and what innovations SwiftUI has brought. We will also learn how to fix the issue when cells in UIKit often display incorrect data.

Analysis of Reusable Cells in UIKit

Basics

In UIKit, UITableView is used to create lists, where cells are managed through a queue of reusable cells. This optimizes memory usage and speeds up the interface loading. Developers define a cell class that can be filled with various data and reused.

In UIKit, the key to effective data list management is the UITableView or UICollectionView class, which uses cells that are reusable thanks to the queuing mechanism. We set a cell template (or more often, we create a custom cell for greater control), which can be filled with various data and reused. This saves resources since UIKit does not create a new instance of the cell for each item but reuses existing ones.

class SomeCustomTableViewCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel! // This can be anything, for simplicity, let it be a simple label

// Here we configure the custom cell, for example
}

Problems and Solutions

However, this approach has its pitfalls, such as the accidental display of incorrect data due to improper management of the state of the reusable cell. This is particularly frequent with images, but I won’t delve into why — this article is not about that, but about how to fix it.

To solve this problem, simply use the prepareForReuse() method to clean or reset the state of the cell before it is reused.

The prepareForReuse() method is called before reusing the custom cell SomeCustomTableViewCell to reset its state and prevent the display of incorrect data:

class SomeCustomTableViewCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel! // This can be anything, for simplicity, let it be a simple label

// Our magic method
override func prepareForReuse() {
super.prepareForReuse()
titleLabel.text = nil // reset the state of the label
}

// Here we configure the custom cell, for example
}

It is precisely in the prepareForReuse() method of the custom cell that I strongly recommend resetting the state for those elements that are clearly not displayed where they should be.

Additionally, this is a simplified example; in reality, you can reset virtually anything. For instance, if you have a Boolean variable that defaults to true but is set to false for some cells, you can also reset it in this method by simply reassigning the default state (not nil)

SwiftUI and the New Declarative Approach

In SwiftUI, there is no direct analog to the prepareForReuse() method used in UIKit to prepare reusable cells for their redisplay. This is due to fundamental differences in the approaches to managing the user interface between SwiftUI and UIKit.

In SwiftUI, the creation and updating of the interface are entirely declarative and depend on the current state of the data that controls the views. When data changes, SwiftUI automatically updates the relevant views. This means that we do not need to manually manage the reuse and cleaning of view states, as is done in UIKit.

To solve tasks similar to those addressed by prepareForReuse() in UIKit, in SwiftUI we typically use the following approaches:

  1. Data Binding: Using data binding (@Binding) ensures that our view always reflects the current state of the data. This guarantees that each data update is immediately displayed in the interface.

2. View State Management: Using @State or @ObservedObject to manage the local state within the view. These properties help manage temporary states inside the view.

3. Conditional Views: Views can be conditionally modified depending on the state of the data. This allows dynamic modification of interface components based on current data or state.

Operating Principles

As SwiftUI uses a declarative approach where the UI is defined by its state, not the process of its construction. This simplifies the creation and updating of the interface (but does not provide the low-level flexibility of UIKit):

struct ContentView: View {
var items = ["Item 1", "Item 2", "Item 3"]

var body: some View {
List(items, id: \.self) { item in
Text(item)
}
}
}

Automatic Memory Management

SwiftUI automatically manages memory, achieved through lazy data loading and idiomatic state management (e.g., @State and @ObservedObject), loading and unloading UI elements. Elements are removed from memory when they are out of the visible area and recreated as necessary, without explicit reuse:

ScrollView {
LazyVStack {
ForEach(items, id: \.self) { item in
Text(item)
}
}
}

In this example, LazyVStack is used to create a vertical stack of elements that load only when they appear in the visible area of the ScrollView. This demonstrates how SwiftUI optimizes the use of memory and processing time by loading and unloading elements as needed.

Comparison and Analysis

SwiftUI manages resources and memory differently than UIKit. While in UIKit cells are reused to save resources, in SwiftUI rendering and memory management are based on the current visibility context of the components.

Resource Optimization in SwiftUI: SwiftUI optimizes rendering and memory usage by dynamically loading and unloading interface elements as they appear and disappear from the visible area. This means that:

  1. Lazy Loading: Like UITableView in UIKit, SwiftUI uses “lazy loading” for collections, such as List and ScrollView. This means that views are loaded and removed from memory as they scroll.
  2. Automatic Memory Management: In SwiftUI, there is no need to explicitly manage memory for views. The framework itself ensures that views that are not visible do not occupy resources. Components that have moved out of the user’s field of view can be removed from memory and recreated as necessary. However, if something goes wrong, debugging can be much more difficult than in UIKit.
  3. Efficient Updates: SwiftUI updates only those parts of the interface that have actually changed, thereby saving both computational and memory resources.

Comparison with UIKit:

  • In UIKit, we need to actively manage the reuse of cells, which can lead to errors (but provides more flexibility and is easier to debug) if the state of the cell is not properly cleared. This often happens, I must say!
  • In SwiftUI, the framework itself takes care of reuse and memory management, reducing the likelihood of errors related to incorrect data display or excessive memory consumption.

Thus, SwiftUI can indeed clear views from memory, similar to UIKit, but does it more automatically, allowing developers to focus on application logic rather than low-level resource management. However, it should be noted that debugging errors in SwiftUI can be more challenging due to its high-level abstraction.

For those not closely familiar with SwiftUI, a brief aside:

State Management in SwiftUI

  • @State: This property wrapper is used to store and manage mutable state within a single view. SwiftUI automatically monitors changes in properties marked with @State, and redraws those parts of the interface that depend on this data.
struct ContentView: View {
@State private var counter = 0

var body: some View {
Button("Click Me") {
counter += 1
}
Text("Number of clicks: \(counter)")
}
}

In this example, each time the button is pressed, the value of counter increases, and the interface is automatically updated.

  • @Binding: Allows for a two-way binding between the state stored in one view and the view that needs to display this state or allow it to be modified. This maintains data synchronization across different parts of the interface.
struct ContentView: View {
@State private var isOn = false

var body: some View {
ToggleView(isOn: $isOn)
}
}

struct ToggleView: View {
@Binding var isOn: Bool

var body: some View {
Toggle("Enable", isOn: $isOn)
}
}

Here, ToggleView receives a @Binding to the isOn state from ContentView, allowing it to change this state.

  • @ObservedObject: Used to create a reactive link between the view and an external reference type of data (usually a class) that conforms to the ObservableObject protocol. This is useful for more complex or shared states that need to be used by multiple views.
class UserData: ObservableObject {
@Published var username = "Guest"
}

struct ContentView: View {
@ObservedObject var userData = UserData()

var body: some View {
Text("User: \(userData.username)")
}
}

@Published inside ObservableObject automatically notifies views when data changes, necessitating an update.

Memory Management and Data Loading in SwiftUI

SwiftUI uses lazy loading and smart memory management to optimize performance. Interface elements are loaded only when they need to be displayed and are unloaded when they are no longer visible, saving resources.

ScrollView {
LazyVStack {
ForEach(0..<1000) { index in
Text("Element \(index)")
.onAppear {
print("Loading element \(index)")
}
.onDisappear {
print("Unloading element \(index)")
}
}
}
}

In this example, LazyVStack is used to create a vertical stack of elements that are loaded only when they appear in the visible area of the ScrollView. This demonstrates how SwiftUI optimizes the use of memory and processing time by loading and unloading elements as needed.

Advantages and Disadvantages

UIKit requires explicit management of cell reuse and state. SwiftUI, on the other hand, simplifies state and presentation management, minimizing the possibility of errors and speeding up development through automatic handling of states and memory.

This approach not only simplifies the work of developers but also minimizes risks associated with “sticking” of old data that could appear when using reusable cells in UIKit. SwiftUI essentially guarantees that the user interface is always up-to-date and corresponds to the current state of the data.

Unlike UIKit, SwiftUI uses a fully declarative approach, where the user interface is described by its current state, not by a sequence of commands to change it. This simplifies development and makes the code cleaner and more understandable.

When Each Approach is Preferable

UIKit remains the preferred choice for complex interactive and dynamically changing interfaces with a high level of customization.

SwiftUI is better suited for new projects with simple or moderately complex UIs, where the speed of development and support is important.

Conclusion

Understanding the differences in managing dynamic data and interfaces between UIKit and SwiftUI will help choose the most effective approach for each project. SwiftUI offers a progressive approach to building interfaces that can significantly simplify the development process and reduce the risks associated with managing the state of components.

The choice between UIKit and SwiftUI largely depends on the specifics of the project and the preferences of the development team. However, understanding the fundamental differences in approaches to managing dynamic data and interfaces can help determine the most effective methods of work for each specific application.

--

--