Maintaining a clean, well-structured codebase is crucial in today’s fast-paced app development world, especially when using SwiftUI. A well-organized codebase improves app performance and creates a more intuitive, native user experience. By following a few key principles, developers can build SwiftUI applications that are responsive, maintainable, and easily scalable. This blog post will delve into essential strategies for crafting a robust SwiftUI codebase that leverages the framework’s power while promoting code clarity and simplicity.
Important points
- Ensure smooth and responsive app behavior.
- A well-structured codebase improves app speed.
- Optimise your app’s performance.
Terminology
- State: Things the view creates using for example
@State@StateObject@AppStorage - Dependencies: Things a view receives with for example
@Binding@EnvironmentObject@ObservedObject - Behavior: Ensuring the code functions correctly and achieves the intended outcome.
- Structure: Where to put which code.
- Performance: Speed of the app.
Behavior
No effect modifier vs conditional views
For smoother animations and transitions between states, use a no-op effect modifier instead of a conditional view.
So use:
SomeView()
.opacity(foo ? 1 : 0)
Instead of:
if foo {
SomeView()
}
Structure
State means a private var
When using a @State you don’t want to expose this to outside the class, you would use a @Binding for that. So when using a @State you should always make it private:
@State private var foo = Foo()
@StateObject private var foo = Foo()
Generic rendering
A view should be able to render and adapt to any context. So you should avoid using something like the device screen size. Instead use the size of the view itself by using a GeometryReader:
So use:
GeometryReader { geometry in
SomeView()
.frame(height: geometry.size.height)
}
Instead of:
SomeView()
.frame(height: UIScreen.main.bounds.height)
Use relative layout over constants
In order to make sure the design works on multiple screen and with multiple configurations it is important to let the content or screen size decide the sizing of the view. This way the view can adapt to where it is placed.
Containers with content
When creating view the top view should be the container view. This way the view itself decides how the content should be place.
So use:
struct SomeView: View {
var body: some View {
HStack {
Text(..)
Button(..)
}
}
}
Instead of:
struct SomeView: View {
var body: some View {
Text(..)
Button(..)
}
}
// Later.
HStack {
SomeView()
}
Split layout and functionality
The body of a view is there to create the layout. Not to handle the logic of it. The logic can be handle in the ViewController.
So use:
Button("Perform action", action: viewController.handleAction)
Instead of:
Button("Perform action") {
showAction = true
showLoader = true
}
Performance
Keep the body simple
The body is used to render the view. This should be the only thing that the body is responsible for. So don’t for example add logic to get or sort data. Place that in a separated function that get called on init or in the observable ViewController.
Split state to custom view types
By splitting the body to different child views you can improve the speed of the app. When a value gets updated only the child view that uses this variable will be re-rendered.
So use:
struct ParentView: some View {
@State query = ""
var body: some View {
VStack {
SearchBar(query: $query)
InfoBar() // Will not get re-rendered on query change.
ResultsView(query: $query)
}
}
}
Instead of
struct ParentView: some View {
@State query = ""
var body: some View {
VStack {
TextField($query)
InfoBar() // Will get re-rendered on query change.
ResultsView(query: $query)
}
}
}
Sources
Great SwiftUI: https://in.swiftui.wtf/great
Why conditional view modifiers are a bad idea: https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/