With this and the next episode, I want to make sure you choose for the Model-View-ViewModel pattern for the right reasons. I want to avoid that you adopt MVVM in a project because you were told it is a sound architecture or because you think you need view models if you use SwiftUI. In this episode, I want to highlight a few problems a typical SwiftUI application can suffer from. Those problems can be resolved by the Model-View-ViewModel pattern with relative ease.
Goodbye View Controllers
SwiftUI changes how we build applications and this introduces several challenges. In a traditional UIKit application, a view is managed by a view controller hence the name. This typically results in fat view controllers with too many responsibilities. Adding view models to the mix can drastically simplify the view controllers of a project.
View controllers are absent in a SwiftUI application. That is an improvement because developers are no longer tempted to burden view controllers with responsibilities they shouldn't own. Views should be dumb and lightweight so you shouldn't move those responsibilities of the view controller to the view. This brings up the question where should you move your view controller logic to? There are plenty of solutions. In this series, we explore how the Model-View-ViewModel pattern solves the problem.
Model, View, and ViewModel
Let's skip the theory and learn more about the Model-View-ViewModel through an example. Open the starter project of this episode in Xcode. The application displays a static list of notes. That's it.
Despite the application's simplicity, there are a numbers of problems. First, the NotesView struct has a property, notes, that stores an array of Note objects. The view is aware of the models it displays and that is a code smell. Remember that views should be dumb and lightweight. A view should display what it is given and it shouldn't even know what it displays.
Second, the view fetches the array of notes from a remote server. That is another responsibility that is atypical for a view. The view shouldn't have access to the models it displays let alone know where those models came from.
Third, the preview also suffers from these code smells. While the preview is functional, it performs a network request every time it is refreshed. That is far from ideal. Just like a unit test, the environment a preview runs in should be under your control. The network shouldn't be a dependency.
struct NotesView: View {
// MARK: - Properties
@State private var notes: [Note] = []
// MARK: - View
var body: some View {
NavigationView {
List(notes) { note in
NoteView(note: note)
}
.navigationTitle("Notes")
}
.task {
Task {
do {
let url = URL(string: "https://cdn.cocoacasts.com/2354d51028d53fcc00ceb0c66f25475d5c79bff0/notes.json")!
let (data, _) = try await URLSession.shared.data(from: url)
notes = try JSONDecoder().decode([Note].self, from: data)
} catch {
print("Unable to Fetch Notes \(error)")
}
}
}
}
}
The NoteView struct suffers from similar problems. It defines a property of type Note and uses it to populate its text views. It knows what it displays, which is a code smell. An additional problem is that the NoteView struct exposes its note property. That too is a subtle code smell.
struct NoteView: View {
// MARK: - Properties
let note: Note
// MARK: - View
var body: some View {
VStack(alignment: .leading) {
Text(note.title)
.font(.title)
Text(note.contents)
.font(.body)
}
}
}
Testability
While it is possible to write unit tests for SwiftUI views, it isn't trivial. Keep in mind that SwiftUI views describe the user interface we have in mind. The system translates that description into a user interface. SwiftUI views weren't designed with testability in mind and that means we can improve the testability of the project by moving as much business logic to types that are testable.
As I mentioned earlier, keep your views dumb and lightweight by moving business logic to other types that meet two requirements, (1) testable and (2) mockable.
No View Controllers
The good news is that the application doesn't suffer from fat view controllers since a SwiftUI application doesn't have view controllers. Don't fall into the trap of pushing the responsibilities that are typically owned by the view controller in a Model-View-Controller application to the view. This is important to understand because it is more problematic than it seems.
A SwiftUI view should be cheap to create. It should take very little time to initialize a view. A user interface can consist of dozens or hundreds of views and it is the system that decides when it is appropriate to create or destroy a view. Remember that a view should simply describe your application's user interface. That's it.
View Models to the Rescue
As I mentioned earlier, there are several solutions to put a view on a diet. In this series, we take a look at one such solution, the Model-View-ViewModel pattern. In the next episode, I show you how the Model-View-ViewModel pattern can resolve the code smells we encountered in this episode.