MVVM is the most popular architecture for iOS development. I won’t delve into the reasons for its popularity, as there are numerous resources available on the topic. Historically, the coordinator pattern has paired well with MVVM and is very useful for separating navigation logic.
However, with SwiftUI becoming increasingly common, several teams are shifting away from MVVM. A significant reason for this is that Apple doesn’t seem very interested in making SwiftUI flexible enough to work seamlessly with MVVM out of the box. A notable example is SwiftData, the highly anticipated successor to CoreData, where queries are tightly coupled to the view layer. This tight coupling makes separating business logic from the view (the primary objective of MVVM) very challenging, if not impossible.
Nevertheless, with careful planning and some effort, it can be achieved, and the results can be quite elegant. Let’s dive in.
I’ve developed a project that I’ve named BKSwifty. This project comprises several modules that I refer to as scenes.
Each scene consists of three components:
- A coordinator
- A view
- A view model
Starting to sound familiar? Great. Let’s examine the root App struct.
import SwiftUI
@main
struct BKSwiftyApp: App {
var body: some Scene {
WindowGroup {
PeopleCoordinator()
}
}
}
As you can see, I simply declare a PeopleCoordinator. You might be wondering how I’m able to do this, considering that SwiftUI mandates declaring a view. The answer is simple: my coordinator is, in fact, a view. Let’s take a closer look.
import SwiftUI
struct PeopleCoordinator: View {
var body: some View {
PeopleView(viewModel: createViewModel())
}
func createViewModel() -> PeopleView.ViewModel {
.init()
}
}
You may wonder why the coordinator is a view instead of a completely different class type. We’ll explore that shortly.
Now let’s examine an example view and view model.
import SwiftUI
struct PeopleView: View {
@State var viewModel: ViewModel
var body: some View {
List {
ForEach(viewModel.people, id: \.self) { person in
Text(person)
}
}
}
}
#Preview {
PeopleView(viewModel: PeopleView.ViewModel())
}
import Observation
extension PeopleView {
@Observable
class ViewModel {
var people: [String]
init(people: [String] = ["Ben", "Jessica", "Nora"]) {
self.people = people
}
}
}
You’ll notice that my view model imports the iOS 17 Observation framework. Tagging my view model with the @Observable macro is all I need to do to make my entire view model observable. That means whenever any properties change in my view model, the view will automatically update as needed. This feature is very handy and serves as a welcome replacement for Combine, which is notoriously difficult to debug.
So, that’s all we need to construct a scene. This is an extremely basic example that we will continue to build upon in my next post.
In my next post, we’ll construct a second scene called “Person” and utilize the PeopleCoordinator to both inject the selected person into the scene and manage navigation.
Here is the repository if you would like to clone it and follow along:
Leave a Reply