Optimizing and Testing View Redraws in SwiftUI
SwiftUI has gained significant popularity due to its declarative syntax and automatic UI updates. However, these automatic updates can sometimes lead to inefficient rendering if not managed correctly. The key to maintaining performance is to prevent unnecessary redraws of views. This article explores techniques to achieve this and how to write tests to ensure your views update as expected.
Preventing Unnecessary Redraws
There are several strategies for preventing unnecessary redraws in SwiftUI.
Avoid unnecessary state changes: SwiftUI redraws views in response to their state changes. Therefore, avoid changing the state of a view unless necessary. Additionally, ensure you’re not updating the state in a way that inadvertently creates a loop of redraws.
Use ‘EquatableView’: By conforming to the EquatableView
protocol when creating your custom SwiftUI views, you can define an Equatable
conformance that SwiftUI uses to determine if a redraw is necessary. If your custom view has a @State
property, you can define an Equatable
conformance that checks whether the new state is different from the old state.
struct MyView: View, EquatableView {
@State private var count: Int
var body: some View {
Button("Increment") {
count += 1
}
}
}
extension MyView: Equatable {
static func == (lhs: MyView, rhs: MyView) -> Bool {
lhs.count == rhs.count
}
}
In this example, SwiftUI will only redraw the view when count
changes.
Use ‘id(_:)’: You can attach an ID to a view in SwiftUI, triggering a redraw only when the ID changes. This is particularly useful when you have a list of items, with only a few that change.
ForEach(items, id: \.self) { item in
MyView(item: item)
.id(item.id)
}
In this example, SwiftUI only redraws MyView
instances when their corresponding item.id
changes.
Use onAppear and onDisappear effectively: Avoid doing work in onAppear
and onDisappear
that may cause unnecessary redraws.
By employing these techniques, you can reduce unnecessary redraws, improving your SwiftUI application’s performance.
Testing for View Redraws
Although SwiftUI does not provide first-class support for directly testing view redraws, you can structure your code to test conditions leading to view redraws. One approach to achieving this is the Model-View-ViewModel (MVVM) design pattern. In this pattern, your view models provide the logic that drives your views, which you can then test.
Here’s a simple counter-example, broken down into model, view model, and view:
// Model
class CounterModel {
@Published var value: Int = 0
}
// ViewModel
class CounterViewModel: ObservableObject {
@Published var model: CounterModel
var value: String {
"\(model.value)"
}
func increment() {
model.value += 1
}
init(model: CounterModel) {
self.model = model
}
}
// View
struct CounterView: View {
@ObservedObject var viewModel: CounterViewModel
var body: some View {
VStack {
Text(viewModel.value)
Button("Increment") {
viewModel.increment()
}
}
}
}
In this case, any change to the CounterModel
's value
would cause the CounterView
to redraw. We can write a unit test to ensure that the increment
method in the CounterViewModel
changes the value:
func testIncrement() {
let model = CounterModel()
let viewModel = CounterViewModel(model: model)
let initialValue = model.value
viewModel.increment()
XCTAssertNotEqual(model.value, initialValue, "Model value should change after incrementing")
}
If this test passes, you can confidently say that calling the increment
the method will cause the CounterView
to redraw.
One bonus tip to see what in each view is changed is to use this:
if #available(iOS 15, *) {
let _ = self._printChanges()
}
In conclusion, while SwiftUI simplifies UI development, understanding, and managing view redraws is essential to maintain performance. By separating logic from views and testing this logic, you can ensure your views update as expected.