Implement a Clean MVVM Architecture in SwiftUI With Combine
By: Zafar Ivaev
Tags: Programming, iOS, Coffee2021, Swiftui, Swift

Presentation

First, we want to create this screen:

Let’s start with a View Model for this screen:

We have a Published property that our View will use to render an array of users. We have a dependency on the UsersServiceProtocol , a business logic component. By depending on the protocol rather than a concrete class, we achieve dependency inversion. This allows us to supply the UsersViewModel with a fake service when needed. We will call the service’s getUsers(count:Int) method and bind the result to the users property defined in the first step. We also add the cancellables property to store future subscriptions. In case we want to have initial Users data, the initializer has a property for that. We can also inject a UsersServiceProtocol -conforming class. By default, it will be the UsersService . We will run the onAppear() method in our View . View doesn’t know what will happen when it’s run. Behind the scenes, the UsersViewModel will fetch users and update the Published property. As a result, the UI update will be triggered and we will see a list of users displayed on the screen. Here, we have a private getUsers(count: Int) method that is run once the View triggers the onAppear() method.

Now we can create the actual UsersView that will display the users:

We provide the dependency on the UsersViewModel . We add a NavigationView with the title “Users.” Inside the NavigationView , we display a list having the view model’s users property as its data. We display a Text showing a user’s first name. It’s embedded in a NavigationLink to allow the navigation to the UserDetailView , which we will define later. Note that the UsersRouter provides the destination for this particular action. Once the NavigationView appears, we signal to UsersViewModel . Behind the scenes, the view model will fetch users.

With View and View Model done, now we can define the remaining components: Configurator and Router .

The UsersConfigurator looks simple. It provides the UsersView with the UsersViewModel and returns it:

We use it in the UsersApp to set the initial View for the window:

Now let’s add the UsersRouter , which is responsible for navigating to the UserDetail screen:

If you remember, we called this method inside the NavigationLink of the UsersView to obtain the destination View . Inside the method, we call the UserDetailConfigurator ’s method for creating UserDetailView with a provided User object. We will define the UserDetailConfigurator and the UserDetailView soon.

Great! The Users module is finished. Now we can create the UserDetail one.

The User Detail module displays this simple screen:

We start with a UserDetailViewModel :

Just like in UsersViewModel , we also have a Published property that our UserDetailView will use to render an image on the screen. We have a dependency on the User object that was selected in the UsersView . Similarly, we have a UserPictureServiceProtocol dependency and the cancellables property. We will use the service to fetch a picture of the user. This time, the onAppear() method triggers an image-loading operation under the hood. The private getAvatarData() method obtains a UIImage and binds it to the Published property defined earlier. As a result, the UserDetailView is updated with the user’s image.

We implement the UserDetailView as follows:

Similarly to the UsersView , we have a dependency on the UserDetailViewModel . We add a State property that we will later use to show a modal sheet on the screen containing more info about the user. We add a VStack to display content in a vertical line. The Image view is bound to a Published property of the UserDetailViewModel . Once a UIImage is obtained, it is displayed in the Image view. An HStack is added to display the user’s first name and last name. We add a “Get more info” button. Tapping on it will present a modal sheet containing more information about the user. When we tap on it, the stateful showingModalSheet property toggles, which results in a sheet being shown in step 8. Similarly to how we did in the UsersView , we refer to the UserDetailRouter to obtain the destination View for a button tap action performed by the user. When the VStack above appears on the screen, we signal to the UserDetailView to start its work.

The UserDetailConfigurator is as simple as the UsersConfigurator defined earlier:

If you remember, the configureUserDetailView() method was called in the UsersRouter when we needed to navigate to the UserDetailView and a NavigationLink required a destination View .

The UserDetailRouter is responsible for providing the destination View when a user taps on the “Get more info” button:

This method was called inside the UserDetailView ’s .sheet(isPresented:content:) method.

Now the final screen — the modal sheet — is all that remains. I have provided it to show how we can perform modal presentation:

The MoreInfoViewModel simply depends on the User object:

Inside the MoreInfoView , we have nothing new. It looks very simple:

All we do here is display four Text views inside a VStack .

MoreInfoConfigurator configures the module using the provided User object: