Beginner’s Guide: SwiftUI and Additional Essentials
If you’re new to iOS development or just beginning your SwiftUI journey using MVVM, and hoping your code won’t become cluttered.
Then go ahead, and dive into this article. 🚀😄
1. Breaking Down Views with Computed Properties:
I initially put everything in ‘body,’ but for me writing code I wouldn’t want to revisit wasn’t the way to go. So, found a solution: breaking down larger views into smaller computed properties or methods(if required to pass data).
struct EditProfileView: View {
var body: some View {
VStack {
headerSection
userDetail
}
}
var headerSection: some View {
HStack(spacing: 10) {
Text("Edit your profile")
Spacer()
btnEdit
}
}
var userDetail: some View {
Text("your detail goes here.")
}
}
2. Separate Button Actions for Clarity:
Keep SwiftUI button actions clean by placing complex logic in separate methods, instead of writing all logic in an action block. Enhance code readability and maintainability.
var btnEdit: some View {
Button("Edit", action: viewModel.editProfile)
.themeButton(width: width)
}
3. Enhance Efficiency using @ViewBuilder Property Wrapper:
When we need to conditionally display different views in SwiftUI, the framework requires a view to return a single, definite type. To overcome this and get rid of compile-time errors, we sometimes wrap views in AnyView
or Group
. However, AnyView is not the best practice because it recreates the whole hierarchy every time. Which can impact performance and efficiency. Instead, use the @ViewBuilder property wrapper.
4. DRY (Don’t Repeat Yourself):
Say no to repetitive code. If you’re copying and pasting code, explore the option of developing a shared function or component.
- Create custom modifiers for creating common UI components at once
- Some Common Components like Separator lines can be created in CommonUI files
- Create a Shared Function for Handling common Business Logic and API Requests
5. The improved best practices for using MVVM:
Use ViewModel classes to manage the state and logic of your views and use the model for data and business logic.
Decoupling from View: Keep your View clean by avoiding the following tasks in the View file:
- Calling API: Move API calls to the ViewModel.
- Business Logic: Business logic should be kept separate from the view.
- Static Data: Use model classes to set up and manage static data if required. ViewModel can hold and expose this data as needed.
- State Management: Manage every aspect of UI state (e.g., loading, API success, API failure) within the ViewModel.
By following these practices, you achieve a clear separation of concerns, making your code more maintainable and testable while keeping your View files focused on UI presentation and interactions.
6. Naming:
- Follow Swift’s naming conventions: Use camelCase for variables and functions, PascalCase for classes/structs, and choose descriptive names that convey the object’s purpose.
- Using prefixes for UI components in computed properties provides clarity, allowing readers to instantly identify an object’s purpose without relying on context.
7. Optionals:
Swift optional handle nil values. Use them when values are absent. Avoid forced unwrapping; instead, use optional binding, guard, or nil coalescing for safe handling.
var selectedId: Int? // Declaring an optional variable
// Optional binding
if let id = selectedId {
// Use optionalValue safely
} else {
// Handle the absence of a value
}
// Using guard to safely unwrap the optional
guard selectedId else {
return
}
// Nil coalescing - When required to provide default value
let value = selectedId ?? 1
8. Avoid Static Strings: Utilize Constants or Enums for Clarity:
It’s best practice to replace static string literals with constants or enums. This ensures code clarity, reduces errors, and promotes maintainability by centralizing your string values for easy updates and consistency.
9. Keep your methods small:
Remember the ‘Keep It Simple’ rule: Each method should tackle just one problem, not a bunch of them. If you’ve got lots of conditions in a method, try breaking them into smaller, easy-to-read methods. It makes your code less confusing and helps you spot bugs faster.
10. Use Final:
The final keyword restricts further inheritance or overriding and increases the runtime performance of the code. If you are writing a class or function, that logically can’t be overridden or don’t want to be subclassed by another developer it’s better to mark it as final. The Notification ViewModel above is marked as ‘final’ to prevent inheritance, not only restricting inheritance but also improving performance.
11. Commenting:
Check Functional and Document Comment Guide By Mohit Chug
TODO, FIXME, and MARK comment guide by Christopher Boynton
12. Preventing Retain Cycles in Swift: Utilizing weak, unowned, and Capture Lists:
These practices are essential for maintaining memory efficiency and preventing strong reference cycles. Dive into the details by checking out this blog by André Gimenez Faria
13. Use delegation and protocols:
Delegation lets one object ask another to do work and is great for communication. A protocol in Swift is like a contract that defines a set of methods or properties. Any Swift type that conforms to the protocol must implement the specified methods and properties. Create protocols and extend them using Protocol Extensions to provide default method implementations. Use delegation and protocols for cleaner, more modular code and seamless communication between components. For detail Checkout this blog by Abhimuralidharan
14. Use SwiftLint:
SwiftLint is a tool to enforce Swift style guide rules and conventions.
15. Avoid Code smells:
Some of the known code smells:
- Code should not nest more than 2 closure expressions.
Noncompliant Code Example
var notificationsList: some View {
List {
ForEach(notifications) { notification in
VStack { // Noncompliant
Text(notification.title)
Text(notification.subTitle)
}
}
}
}
Compliant Code Example
var notificationsList: some View {
List {
ForEach(notifications) { notification in
notificationCell(for: notification)
}
}
}
func notificationCell(for notification: NotificationsData): some View {
VStack {
Text(notification.title)
Text(notification.subTitle)
}
}
- Remove commented-out code: Programmers should not comment out code as it bloats programs and reduces readability.
- Refactor function to reduce its Cognitive Complexity from 17 to the 15 allowed. To refactor a function and reduce its Cognitive Complexity, you can consider the following strategies:
- Break down complex logic into smaller functions.
- Apply the Single Responsibility Principle.
- Reduce Nesting
- Simplify Conditions
- Use Guard Clauses
- Remove Redundant Code - Add a nested comment explaining why this function is empty, or complete the implementation.
- Remove the unnecessary Boolean literal.
//Noncompliant Code Example
if locationEdited == true { // Remove the unnecessary Boolean literal.
}
//Compliant Code Example
// You don't need to compare it explicitly to true. You can directly use it in the if statement to check if it's true.
if locationEdited{
}
- Merge the if statement with the nested one.
//Noncompliant Code Example
if locationEdited {
if locationName == "XYZ" { //Merge this if statement with the nested one.
}
}
//Compliant Code Example
if locationEdited && locationName == "XYZ"{
}
Some other Key Considerations
- Avoid Nested if.
- Avoid long parameter lists.
- Prefer switch over if-else.
- Use Guard for early exits.
- Always delete unused code also code that is generated by IDE.
- Handle all the possible errors for tasks eg. no internet connection, API failure…etc
- Use Generics when you want to create flexible and reusable code that can work with different types
- Follow SOLID Principle
PS: I hadn’t planned to write this article, but I ended up creating a beginner’s guide for our team and decided to publish it to assist fellow beginners and will be releasing more detailed articles in the future. Stay tuned for updates!
Follow Swiftable for more updates !!