iOS App Development

Creating an iOS App With SwiftUI and Combine Using MVVM and Protocols

In this tutorial, we will implement interaction between views, work with data in your demo app using the brand new frameworks, swiftUI and Combine, released by Apple in 2019. We will use the MVVM pattern and the Protocol-Oriented Programming paradigm.

Alex Zarr
The Startup

--

Photo by Jessica Lewis on Unsplash

In this tutorial we use the latest version of Xcode (11.3.1) and macOS Catalina (10.15.4) for the moment of writing.

SwiftUI and Combine were introduced on WWDC 2019 by Apple. These two frameworks change your way to create iOS apps by making it faster and more convenient, helping you create testable and readable code. SwiftUI provides a declarative way of creating interfaces that speeds up the development, and Combine complements SwiftUI (or works apart of it, of course) and brings Functional Reactive programming (FRP) into iOS projects and helps focus more on logic and divides code into separate parts simplifies the use of MVVM or other paradigms in iOS apps.

In our project we will use Protocol-Oriented Programming that simplifies architecting iOS apps, prevents us from a tight coupling of classes with one another, allows us to use all the power of structs and enums and makes it easier to write unit tests for our apps.

Our structural design pattern will be MVVM, the pattern that divides code into three separate parts: Model, View, and ViewModel. We will see how convenient it to use in iOS apps that use SwiftUI and Combine.

In this tutorial, we will use the app developed in the Getting Started with SwiftUI and Combine Using MVVM and Protocols for iOS tutorial, so I highly recommend going through it. Otherwise, if you do not want to do this, you just can download the complete code here.

What we will learn

By the end of this tutorial we will learn:

  • How to interact between views and present a modal view in a parent view;
  • How @State and @Published variable work and how they update the UI of iOS apps;
  • How a SwiftUI View interacts with a ViewModel using Combine and how a ViewModel interacts with a Data Manager;
  • How to create Publishers that emit notifications (with an example of how to adjust your views when the keyboard appears).

We will continue creating a simple To-do list app that uses SwiftUI and Combine and powered by MVVM and Protocols. So, let’s get started.

Getting Started

Before we start, make sure you already have an app with the following file structure:

So, as a starter app we have a To-do list app with 4 main files:

  • Todo.swift is a model of our to-do tasks;
  • DataManager.swift that stores and updates the data in the app;
  • TodoListView.swift that contains the main view of our app;
  • TodoListViewModel.swift that contains the viewModel that works with the TodoListView.

If you do not have any prior experience working with SwiftUI and Combine or you do not understand any part of this project, please take a look into the tutorial where we created this starter app from scratch. It will help you understand easier the following steps.

If we build the app, we will see that it does not contain any information and we cannot add a new Todo. Even if we choose TodoListView and look at Canvas, we will see only the list of Todos from our MockDataManager and that is it. Obviously, we need to implement the interface and logic of adding new Todos. Let’s do that.

Add a new Todo

First, we need to create a protocol for a viewModel that needs for a view where we will be able to add a new Todo item and the viewModel itself as we did it for TodoListView.

NewTodoViewModel

Create a new Swift file named NewTodoViewModel.swift. Create a protocol in the file as follows:

The protocol is going to be very simple at the moment. We need nothing else but to create a new Todo with a particular title. And do not forget to import Combine. We need it to implement our viewModel.

Let’s create a NewTodoViewModel and implement the protocol:

We create a new class, initialize it with the DataManager and implement the addNewTodo(title: String) method.

But it does not look clean. I do not like that we need to create an instance of Todo in the method. Why don’t we simply pass title in the DataManager method? Let’s refactor our DataManager a bit. In this case, we need to update DataManagerProtocol, then update the implementation in the DataManager and MockDataManager classes:

Usually, it has to be done in a separate branch as a different task, but we do it right here just to show how to refactor our code and make it look better. Furthermore, you can see how protocols help us do it fast and safely.

You can also notice that in DataManager we call the initializer of Todo with only one parameter (Todo(title: String)) because id and isCompleted have default values. That makes our code even simpler.

Now we have our viewModel with all the methods we need. It is small, but it implements all the logic it has to.

NewTodoView

Create a new SwiftUI View file, name it NewTodoView and click Create. Make sure the Canvas shows the view (if the Canvas is not opened, use Cmd+Option+Return to appear it, click Resume at the top-right corner if you do not see the view).

The view will contain only three subviews: a TextField, Cancel and Add buttons. Let’s add them into the view:

That is what we have done:

  • We added viewModel;
  • We created a @State variable title to store the text from the textField there;
  • We created VStack and put there TextField in between two Spacer(). Spacer() is an expandable space that expands as much as it can, so if we have a view between two of them, the view will be in the middle of the superview. TextField has two params, the first one is a placeholder text, while the second one is a String variable to store the text typed by a user. We use the $ prefix to access a binding to a state variable;
  • We added HStack underneath the bottom Spacer() to appear the Cancel and Add buttons horizontally in a row. Buttons are basically the Text views and do nothing for now.

Well, it does not look good now, so why don’t we add some paddings and make buttons wider?

We will add the .padding() modifier after the closing bracket of VStack to create padding around the display.

And, also, we will add two modifiers to our buttons: .padding(.vertical, 16.0) to make our buttons larger vertically and .frame(minWidth: 0, maxWidth: .infinity) to expand both buttons horizontally as much as possible, therefore, making them equally wide:

That is better!

Now, we will add actions to our buttons:

  1. We create an @Environment variable presentationMode that stores the view’s state, we will use it to dismiss our view programmatically.
  2. We call self.presentationMode.wrappedValue.dismiss() on the Cancel button action to dismiss the view: we get access to the wrapped value of the variable and use the method dismiss() to initiate dismissing the view.
  3. In the Add button’s action we check if the title variable is empty, and if it is not, we call the method in the viewModel and dismiss the view.

Remember the use of the presentationMode variable. This is how we usually dismiss the view in SwiftUI.

Awesome work! Now, it is time to implement the opening of the window, because, otherwise, how do we get there?

Let’s get back to our main view. In the Project Navigation in the left panel of Xcode choose TodoListView.swift. We need to open NewTodoView from here, so, to do that, we will add a navigation button on the right side of the navigation bar.

  • First, create a @State variable called isShowingAddNew that equals false. This flag will show the view if the NewTodoView sheet is presented (for TodoListView it is a sheet in SwiftUI);
  • Then, create a computed variable addNewButton of type some View above the body. The variable will contain only the Button that toggles isShowingAddNew on the action and contains a system image plus;
  • Pass the addNewButton variable into the navigationBarItems modifier inside of the NavigationView to show the button in the navigation bar;
  • Finally, in the body add the .sheet modifier to. the NavigationView that is presented if isShowingAddNew is true (it gets a Binding value, so we will pass the variable with the $ prefix to access a binding to a state variable). It will present NewTodoView and update the Todos on dismiss.

Now, your TodoListView should look like this:

Now, you can click Live Preview in the bottom-right corner of the view in Canvas. Wait till it loads and then try to tap the Add button. It will present NewTodoView. You see, you can use Live Preview to test your views without building and running the whole app.

But, anyway, let’s build and run the app. When it is started, tap the Add button in the top-right corner. Now, try to tap on the TextField and start typing something. You see that the keyboard hides the buttons (if you use a simulator your keyboard may not appear when you focus on a text field, in this case, try Cmd+K to show it).

We need to update the view once the keyboard’s shown. But how do we do that? We can try to observe the keyboard notifications like in the old days. But it makes sense to subscribe to them using our great Combine framework that helps with that. To be able to reuse that, we will create a new class.

KeyboardResponder

Create a new Swift file named KeyboardResponder.swift. First, as usual, we will create KeyboardResponderProtocol. As you may know, in UIKit we used to observe the keyboardWillShow and keyboardWillHide notifications to create a proper animation for our views. We used to get the keyboardHeight and animationDuration values for that. So, our protocol will contain only two variables: currentHeight and duration.

After that, we will create a class KeyboardResponder that conforms to KeyboardResponderProtocol and ObservableObject (we do not create an extension this time, because we have only variables, and variables cannot be stored in extensions. I think it is not a big deal this time).

Finally, our class will look like this:

  • We create two variables to conform to the protocol, only one of them is @Published because we know these variables will be updated at the same time we do not need to force views to update twice;
  • We create a variable cancellableBag to store our set of AnyCancellable, so we will not erase the publishers and make sure we will be receiving the notifications until the instance of KeyboardResponder exists;
  • In the initializer method we create two variables that store publishers for keyboardWillShow and keyboardWillHide;
  • Then, we merge our notifications using Publishers.Merge as our actions on these notifications are the same and we do not need to duplicate code;
  • We receive the notifications in the main thread since we are going to update the UI on these events;
  • We call sink to attach a subscriber to the publisher and call keyboardNotification in the closure;
  • Finally, we store the subscriber in the cancellableBag variable while the instance of KeyboardResponder exists.

The keyboardNotification method is almost similar to the one we use in UIKit to update a UIView when the keyboard is about to be shown or hidden. But we basically put the animation duration and the keyboard’s height into the variables we created before.

This is all we need to do in the KeyboardResponder class. Now, we need to use this class in our view. Choose NewTodoView.swift in Project Navigator.

Create an @ObservedObject variable keyboard for the new class. To move all the subviews in the view up when the keyboard appears we need to update the bottom padding. To do so, we need to add the .padding modifier right below the existing one, but with the edges parameter equals to .bottom and the length parameter should be keyboard.currentHeight. Doing so, the view will have the bottom padding always equals to the keyboard’s height (when the keyboard is hidden the padding will be zero).

As we added the bottom padding modifier below .padding(), the padding overrides the previous modifier and these two modifiers work properly.

And, as a final step, add the .animation modifier right below the one we just added. Set the animation as .easeOut with keyboard.duration. Now, the padding updates with the animation and it looks good. You can build the app and check how it works.

The only thing that bothers me now is that the Add button looks active even when there is no text in the TextField. Why don’t we add some change to the button when the user types something?

  • First, we create a new variable named isAddButtonDisabled that returns title.isEmpty. It may seem unnecessary but it makes the code more readable;
  • We create a variable addButtonColor that returns either .gray or .blue depending on isAddButtonDisabled;
  • Now, since we have this variable, we replace the if statement in the Add button action;
  • We add the .foregroundColor(.black) modifier to Text(“Add”) as we want to use the blue color as a background;
  • Finally, we add two modifiers to the Add button: .background(addButtonColor) and .disabled(isAddButtonDisabled) to update the button color and make it enabled/disabled.

The complete code should be as follows:

Great job! Now you can build your app and see how the button changes when you type something in the text field. And if you type some title and tap the Add button you will notice a new task in the main view! Awesome. Now, our app finally has some logic that actually works.

If you restart the app, you will lose all the data because we have not implemented any persistent storage. We will do it in another tutorial.

What’s Next

Congratulations! You have done it! And now you have developed an app that uses SwiftUI and Combine, you have implemented the MVVM pattern and used Protocols to make your code clean, readable and testable. In the next part we will add checkmarks to the rows, implement marking Todos as completed, will implement showing/hiding completed Todos and some other features that will improve our app and help to develop the swiftUI and Combine knowledge.

The complete code of the app is available here.

The next tutorial is available here.

--

--