Android Fall Sale

Get up to 55% off all Android books!

MVVM with Combine Tutorial for iOS

In this MVVM with Combine Tutorial, you’ll learn how to get started using the Combine framework along with SwiftUI to build an app using the MVVM pattern

4.7/5 15 Ratings

Version

  • Swift 5, iOS 13, Xcode 11
Note: This tutorial requires macOS Catalina beta 6 or later and Xcode 11 beta 6 or later.

Apple’s newest framework Combine, alongside SwiftUI, took WWDC by storm. Combine is a framework which provides logical streams of data which can emit values and then optionally end in a success or error. These streams are at the core of Functional Reactive Programming (FRP) which has become popular over recent years. It has become clear that Apple is moving forward, not only with a declarative way of creating interfaces with SwiftUI, but also with Combine to manage state over time. In this MVVM with Combine tutorial, you’ll create a weather app that takes advantage of SwiftUI, Combine and MVVM as the architectural pattern. By the end of it, you’ll be comfortable with:

  • Using Combine to manage state.
  • Creating bindings between your UI and your ViewMode with SwiftUI.
  • Understanding of how all these three concepts fit together.

By the end of this tutorial, your app should look like this:

View built using MVVM with Combine from this tutorial

You’ll also explore the pros and cons of this particular approach and how you could tackle the problem differently. This way you’ll be better prepared for whatever comes your way! :]

Getting Started

Start by downloading the project materials using the Download Materials button at the top or bottom of this tutorial. Open the project located inside the CombineWeatherApp-Starter folder.

Before you can see any weather information, you must register at OpenWeatherMap and get an API key. This process shouldn’t take you more than a couple of minutes and, by the end, you’ll see a screen similar to this:

Open WeatherFetcher.swift. Then update WeatherFetcher.OpenWeatherAPI with your key inside struct OpenWeatherAPI, like so:

struct OpenWeatherAPI {
  ...
  static let key = "<your key>" // Replace with your own API Key
}

Once this is done, build and run the project. The main screen shows you a button to tap:

Tapping “Best weather app” will show you more detail:

Right now it doesn’t look like much, but by the end of the tutorial, it’ll look a lot better. :]

An Introduction to the MVVM Pattern

The Model-View-ViewModel (MVVM) pattern is a UI design pattern. It’s a member of a larger family of patterns collectively known as MV*, these include Model View Controller (MVC), Model View Presenter (MVP) and a number of others.

Each of these patterns addresses separating UI logic from business logic in order to make apps easier to develop and test.

Note: Check out Design Patterns by Tutorials or Advanced iOS App Architecture for more on design patterns and iOS architecture.

It helps to look back at the origins of MVVM to understand the pattern better.

MVC was the first UI design pattern, and its origins track back to the Smalltalk language of the 1970s. The image below illustrates the main components of the MVC pattern:

MVCPattern-2

This pattern separates the UI into the Model that represents the application state, the View, which in turn is composed of UI controls, and a Controller which handles user interactions and updates the model accordingly.

One big problem with the MVC pattern is that it’s quite confusing. The concepts look good, but often when people come to implement MVC, the seemingly circular relationships illustrated above result in the Model, View and Controller becoming a big, horrible mess.

More recently, Martin Fowler introduced a variation of the MVC pattern termed the Presentation Model, which was adopted and popularized by Microsoft under the name MVVM.

MVVMPattern

At the core of this pattern is the ViewModel, which is a special type of model that represents the UI state of the app. It contains properties that detail the state of each and every UI control. For example, the current text for a text field, or whether a specific button is enabled. It also exposes the actions the view can perform, like button taps or gestures.

It can help to think of the ViewModel as the model-of-the-view.

The relationships between the three components of the MVVM pattern are simpler than the MVC equivalents, following these strict rules:

  1. The View has a reference to the ViewModel, but not vice-versa.
  2. The ViewModel has a reference to the Model, but not vice-versa.
  3. The View has no reference to the Model or vice-versa.

If you break these rules, you’re doing MVVM wrong!

A couple of immediate advantages of this pattern are:

  1. Lightweight Views: All your UI logic resides within the ViewModel, resulting in a very lightweight view.
  2. Testing: You can run your entire app without the View which greatly enhances its testability.
Note: Testing views is notoriously difficult because tests run as small, contained chunks of code. Usually controllers add and configure views to the scene that rely on other app states. This means running small tests can become a fragile and cumbersome proposition.

At this point, you might have spotted a problem. If the View has a reference to the ViewModel but not vice-versa, how does the ViewModel update the View?

Ah-ha!!! This is where the secret-sauce of the MVVM pattern comes in.

MVVM and Data Binding

Data Binding is what allows you to connect a View to its ViewModel. Before this year’s WWDC, you would have to use something akin to RxSwift (via RxCocoa) or ReactiveSwift (via ReactiveCocoa). In this tutorial, you’ll explore how you can achieve this connection using SwiftUI and Combine.

MVVM With Combine

Combine is not actually necessary for bindings to happen, but this doesn’t mean you can’t harness its power. You can use SwiftUI on its own to create bindings. But using Combine allows more power. As you’ll see throughout the tutorial, once you are on the ViewModel side, using Combine becomes the natural choice. It allows you to cleanly define a chain that starts in your UI, way down to a network call. You can achieve all this power easily by combining (pun intended) SwiftUI and Combine. It’s possible to use another communication pattern (e.g. delegation), but by doing so you are trading the declarative approach set by SwiftUI, and its bindings, for an imperative one.

Building the App

Note: If you are new to something like SwiftUI or Combine, you may be confused by some of the snippets. Don’t worry if that’s the case! This is an advanced topic and it requires some time and practice. If something doesn’t make sense, run the app and set breakpoints to see how it behaves.

You’ll start with the model layer and move upwards to the UI.

Since you are dealing with JSON coming from the OpenWeatherMap API, you need a utility method to convert the data into a decoded object. Open Parsing.swift and add the following:

import Foundation
import Combine

func decode<T: Decodable>(_ data: Data) -> AnyPublisher<T, WeatherError> {
  let decoder = JSONDecoder()
  decoder.dateDecodingStrategy = .secondsSince1970

  return Just(data)
    .decode(type: T.self, decoder: decoder)
    .mapError { error in
      .parsing(description: error.localizedDescription)
    }
    .eraseToAnyPublisher()
}

This uses a standard JSONDecoder to decode the JSON from the OpenWeatherMap API. You’ll find out more about mapError(_:) and eraseToAnyPublisher() shortly.

Note: You can write the decode logic by hand or you can use a service like QuickType. As a rule of thumb, for services I own, I do it by hand. For third party services, I generate the boilerplate using QuickType. In this project, you’ll find the entities generated with this service in Responses.swift.

Now open WeatherFetcher.swift. This entity is responsible for fetching information from the OpenWeatherMap API, parsing the data and providing it to its consumer.

Like a good Swift citizen, you’ll start with a protocol. Add the following below the imports:

protocol WeatherFetchable {
  func weeklyWeatherForecast(
    forCity city: String
  ) -> AnyPublisher<WeeklyForecastResponse, WeatherError>

  func currentWeatherForecast(
    forCity city: String
  ) -> AnyPublisher<CurrentWeatherForecastResponse, WeatherError>
}

You’ll use the first method for the first screen to display the weather forecast for the next five days. You’ll use the second to view more detailed weather information.

You might be wondering what AnyPublisher is and why it has two type parameters. You can think of this as a computation to-be, or something that will execute once you subscribed to it. The first parameter (WeeklyForecastResponse) refers to the type it returns if the computation is successful and, as you might have guessed, the second refers to the type if it fails (WeatherError).

Implement those two methods by adding the following code below the class declaration:

// MARK: - WeatherFetchable
extension WeatherFetcher: WeatherFetchable {
  func weeklyWeatherForecast(
    forCity city: String
  ) -> AnyPublisher<WeeklyForecastResponse, WeatherError> {
    return forecast(with: makeWeeklyForecastComponents(withCity: city))
  }

  func currentWeatherForecast(
    forCity city: String
  ) -> AnyPublisher<CurrentWeatherForecastResponse, WeatherError> {
    return forecast(with: makeCurrentDayForecastComponents(withCity: city))
  }

  private func forecast<T>(
    with components: URLComponents
  ) -> AnyPublisher<T, WeatherError> where T: Decodable {
    // 1
    guard let url = components.url else {
      let error = WeatherError.network(description: "Couldn't create URL")
      return Fail(error: error).eraseToAnyPublisher()
    }

    // 2
    return session.dataTaskPublisher(for: URLRequest(url: url))
      // 3
      .mapError { error in
        .network(description: error.localizedDescription)
      }
      // 4
      .flatMap(maxPublishers: .max(1)) { pair in
        decode(pair.data)
      }
      // 5
      .eraseToAnyPublisher()
  }
}

Here’s what this does:

  1. Try to create an instance of URL from the URLComponents. If this fails, return an error wrapped in a Fail value. Then, erase its type to AnyPublisher, since that’s the method’s return type.
  2. Uses the new URLSession method dataTaskPublisher(for:) to fetch the data. This method takes an instance of URLRequest and returns either a tuple (Data, URLResponse) or a URLError.
  3. Because the method returns AnyPublisher<T, WeatherError>, you map the error from URLError to WeatherError.
  4. The uses of flatMap deserves a post of their own. Here, you use it to convert the data coming from the server as JSON to a fully-fledged object. You use decode(_:) as an auxiliary function to achieve this. Since you are only interested in the first value emitted by the network request, you set .max(1).
  5. If you don’t use eraseToAnyPublisher() you’ll have to carry over the full type returned by flatMap: Publishers.FlatMap<AnyPublisher<_, WeatherError>, Publishers.MapError<URLSession.DataTaskPublisher, WeatherError>>. As a consumer of the API, you don’t want to be burdened with these details. So, to improve the API ergonomics, you erase the type to AnyPublisher. This is also useful because adding any new transformation (e.g. filter) changes the returned type and, therefore, leaks implementation details.

At the model level, you should have everything you need. Build the app to make sure everything is working.

Diving Into the ViewModels

Next, you’ll work on the ViewModel that powers the weekly forecast screen:

Open WeeklyWeatherViewModel.swift and add:

import SwiftUI
import Combine

// 1
class WeeklyWeatherViewModel: ObservableObject, Identifiable {
  // 2
  @Published var city: String = ""

  // 3
  @Published var dataSource: [DailyWeatherRowViewModel] = []

  private let weatherFetcher: WeatherFetchable

  // 4
  private var disposables = Set<AnyCancellable>()

  init(weatherFetcher: WeatherFetchable) {
    self.weatherFetcher = weatherFetcher
  }
}

Here’s what that code does:

  1. Make WeeklyWeatherViewModel conform to ObservableObject and Identifiable. Conforming to these means that the WeeklyWeatherViewModel‘s properties can be used as bindings. You’ll see how to create them once you reach the View layer.]
  2. The properly delegate @Published modifier makes it possible to observe the city property. You’ll see in a moment how to leverage this.
  3. You’ll keep the View’s data source in the ViewModel. This is in contrast to what you might be used to doing in MVC. Because the property is marked @Published, the compiler automatically synthesizes a publisher for it. SwiftUI subscribes to that publisher and redraws the screen when you change the property.
  4. Think of disposables as a collection of references to requests. Without keeping these references, the network requests you’ll make won’t be kept alive, preventing you from getting responses from the server.

Now, use the WeatherFetcher by adding the following below the initializer:

func fetchWeather(forCity city: String) {
  // 1
  weatherFetcher.weeklyWeatherForecast(forCity: city)
    .map { response in
      // 2
      response.list.map(DailyWeatherRowViewModel.init)
    }

    // 3
    .map(Array.removeDuplicates)

    // 4
    .receive(on: DispatchQueue.main)

    // 5
    .sink(
      receiveCompletion: { [weak self] value in
        guard let self = self else { return }
        switch value {
        case .failure:
          // 6
          self.dataSource = []
        case .finished:
          break
        }
      },
      receiveValue: { [weak self] forecast in
        guard let self = self else { return }

        // 7
        self.dataSource = forecast
    })

    // 8
    .store(in: &disposables)
}

There’s quite a lot going on here, but I promise after this, everything will be easier!

  1. Start by making a new request to fetch the information from the OpenWeatherMap API. Pass the city name as the argument.
  2. Map the response (WeeklyForecastResponse object) to an array of DailyWeatherRowViewModel objects. This entity represents a single row in the list. You can check the implementation located in DailyWeatherRowViewModel.swift. With MVVM, it’s paramount for the ViewModel layer to expose to the View exactly the data it will need. It doesn’t make sense to expose directly to the View a WeeklyForecastResponse, since this forces the View layer to format the model in order to consume it. It’s a good idea to make the View as dumb as possible and concerned only with rendering.
  3. The OpenWeatherMap API returns multiple temperatures for the same day depending on the time of the day, so remove the duplicates. You can check Array+Filtering.swift to see how that’s done.
  4. Although fetching data from the server, or parsing a blob of JSON, happens on a background queue, updating the UI must happen on the main queue. With receive(on:), you ensure the update you do in steps 5, 6 and 7 occurs in the right place.
  5. Start the publisher via sink(receiveCompletion:receiveValue:). This is where you update dataSource accordingly. It’s important to notice that handling a completion — either a successful or failed one — happens separately from handling values.
  6. In the event of a failure, set dataSource as an empty array.
  7. Update dataSource when a new forecast arrives.
  8. Finally, add the cancellable reference to the disposables set. As previously mentioned, without keeping this reference alive, the network publisher will terminate immediately.

Build the app. Everything should compile! Right now, the app still doesn’t do much, because you don’t have a view so it’s time to take care of that!

Weekly Weather View

Start by opening WeeklyWeatherView.swift. Then, add the viewModel property and an initializer inside the struct:

@ObservedObject var viewModel: WeeklyWeatherViewModel

init(viewModel: WeeklyWeatherViewModel) {
  self.viewModel = viewModel
}

The @ObservedObject property delegate establishes a connection between the WeeklyWeatherView and the WeeklyWeatherViewModel. This means that, when the WeeklyWeatherView‘s property objectWillChange sends a value, the view is notified that the data source is about to change and consequently the view is re-rendered.

Now open SceneDelegate.swift and replace the old weeklyView property with the following:

let fetcher = WeatherFetcher()
let viewModel = WeeklyWeatherViewModel(weatherFetcher: fetcher)
let weeklyView = WeeklyWeatherView(viewModel: viewModel)

Build the project again to make sure that everything compiles.

Head back into WeeklyWeatherView.swift and replace body with the actual implementation for your app:

var body: some View {
  NavigationView {
    List {
      searchField

      if viewModel.dataSource.isEmpty {
        emptySection
      } else {
        cityHourlyWeatherSection
        forecastSection
      }
    }
    .listStyle(GroupedListStyle())
    .navigationBarTitle("Weather ⛅️")
  }
}

When the dataSource is empty, you’ll show an empty section. Otherwise, you’ll show the forecast section and the ability to see more detail about the particular city that you searched for. Add the following at the bottom of the file:

private extension WeeklyWeatherView {
  var searchField: some View {
    HStack(alignment: .center) {
      // 1
      TextField("e.g. Cupertino", text: $viewModel.city)
    }
  }

  var forecastSection: some View {
    Section {
      // 2
      ForEach(viewModel.dataSource, content: DailyWeatherRow.init(viewModel:))
    }
  }

  var cityHourlyWeatherSection: some View {
    Section {
      NavigationLink(destination: CurrentWeatherView()) {
        VStack(alignment: .leading) {
          // 3
          Text(viewModel.city)
          Text("Weather today")
            .font(.caption)
            .foregroundColor(.gray)
        }
      }
    }
  }

  var emptySection: some View {
    Section {
      Text("No results")
        .foregroundColor(.gray)
    }
  }
}

Although there is quite a bit of code here, there are only three main parts:

  1. Your first bind! $viewModel.city establishes a connection between the values you’re typing in the TextField and the WeeklyWeatherViewModel‘s city property. Using $ allows you to turn the city property into a Binding<String>. This is only possible because WeeklyWeatherViewModel conforms to ObservableObject and is declared with the @ObservedObject property wrapper.
  2. Initialize the daily weather forecast rows with their own ViewModels. Open DailyWeatherRow.swift to see how it works.
  3. You can still use and access the WeeklyWeatherViewModel properties without any fancy binds. This just displays the city name in a Text.

Build and run the app and you should see the following:

Surprisingly, or not, nothing happens. The reason for this is that you haven’t connected the city bind to an actual HTTP request yet. Time to fix that.

Open WeeklyWeatherViewModel.swift and replace your current initializer with the following:

// 1
init(
  weatherFetcher: WeatherFetchable,
  scheduler: DispatchQueue = DispatchQueue(label: "WeatherViewModel")
) {
  self.weatherFetcher = weatherFetcher
  
  // 2
  _ = $city
    // 3
    .dropFirst(1)
    // 4
    .debounce(for: .seconds(0.5), scheduler: scheduler)
    // 5
    .sink(receiveValue: fetchWeather(forCity:))
}

This code is crucial because it bridges both worlds: SwiftUI and Combine.

  1. Add a scheduler parameter, so you can specify which queue the HTTP request will use.
  2. The city property uses the @Published property delegate so it acts like any other Publisher. This means it can be observed and can also make use of any other method that is available to Publisher.
  3. As soon as you create the observation, $city emits its first value. Since the first value is an empty string, you need to skip it to avoid an unintended network call.
  4. Use debounce(for:scheduler:) to provide a better user experience. Without it the fetchWeather would make a new HTTP request for every letter typed. debounce works by waiting half a second (0.5) until the user stops typing and finally sending a value. You can find a great visualization of this behavior at RxMarbles. You also pass scheduler as an argument, which means that any value emitted will be on that specific queue. Rule of thumb: You should process values on a background queue and deliver them on the main queue.
  5. Finally, you observe these events via sink(receiveValue:) and handle them with fetchWeather(forCity:) that you previously implemented.

Build and run the project. You should finally see the main screen in action:

Navigation and Current Weather Screen

MVVM as an architectural pattern doesn’t get into the nitty-gritty details. Some decisions are left up to the developer’s discretion. One of those is how you navigate from one screen to another, and what entity owns that responsibility. SwiftUI hints on the usage of NavigationLink, and, as such, this is what you’ll use in this tutorial.

If you look at NavigationLink‘s most basic initializer: public init<V>(destination: V, label: () -> Label) where V : View, you can see that it expects a View as an argument. This, in essence ties your current View (origin) to another View (destination). This relationship might be okay in simpler apps but when you have complex flows that require different destinations based on external logic (like a server response) you might get into trouble.

Following the MVVM recipe, the View should ask the ViewModel what to do next, but this is tricky because the parameter expected is a View and a ViewModel should be agnostic about those concerns. This problem is solved via FlowControllers or Coordinators, which are represented by yet another entity that works alongside the ViewModel to manage routing across app. This approach scales well, but it would stop you from using something like NavigationLink.

All of that is beyond the scope of this tutorial so, for now, you’ll be pragmatic and use a hybrid approach.

Before diving into navigation, first update CurrentWeatherView and CurrentWeatherViewModel. Open CurrentWeatherViewModel.swift and add the following:

import SwiftUI
import Combine

// 1
class CurrentWeatherViewModel: ObservableObject, Identifiable {
  // 2
  @Published var dataSource: CurrentWeatherRowViewModel?

  let city: String
  private let weatherFetcher: WeatherFetchable
  private var disposables = Set<AnyCancellable>()

  init(city: String, weatherFetcher: WeatherFetchable) {
    self.weatherFetcher = weatherFetcher
    self.city = city
  }

  func refresh() {
    weatherFetcher
      .currentWeatherForecast(forCity: city)
      // 3
      .map(CurrentWeatherRowViewModel.init)
      .receive(on: DispatchQueue.main)
      .sink(receiveCompletion: { [weak self] value in
        guard let self = self else { return }
        switch value {
        case .failure:
          self.dataSource = nil
        case .finished:
          break
        }
        }, receiveValue: { [weak self] weather in
          guard let self = self else { return }
          self.dataSource = weather
      })
      .store(in: &disposables)
  }
}

CurrentWeatherViewModel mimics what you did previously in WeeklyWeatherViewModel:

  1. Make CurrentWeatherViewModel conform to ObservableObject and Identifiable.
  2. Expose an optional CurrentWeatherRowViewModel as the data source.
  3. Transform new values to a CurrentWeatherRowViewModel as they come in the form of a CurrentWeatherForecastResponse.

Now, take care of the UI. Open CurrentWeatherView.swift and add an initializer at the top of the struct:

@ObservedObject var viewModel: CurrentWeatherViewModel

init(viewModel: CurrentWeatherViewModel) {
  self.viewModel = viewModel
}

This follows the same pattern you applied in WeeklyWeatherView and it’s most likely what you’ll be doing when using SwiftUI in your own projects: You inject a ViewModel in the View and access its public API.

Now, update the body computed property:

var body: some View {
  List(content: content)
    .onAppear(perform: viewModel.refresh)
    .navigationBarTitle(viewModel.city)
    .listStyle(GroupedListStyle())
}

You’ll notice the use of the onAppear(perform:) method. This takes a function of type () -> Void and executes it when the view appears. In this case, you call refresh() on the View Model so the dataSource can be refreshed.

Finally, add the following at the bottom of the file:

private extension CurrentWeatherView {
  func content() -> some View {
    if let viewModel = viewModel.dataSource {
      return AnyView(details(for: viewModel))
    } else {
      return AnyView(loading)
    }
  }

  func details(for viewModel: CurrentWeatherRowViewModel) -> some View {
    CurrentWeatherRow(viewModel: viewModel)
  }

  var loading: some View {
    Text("Loading \(viewModel.city)'s weather...")
      .foregroundColor(.gray)
  }
}

This adds the remaining UI bits.

The project doesn’t compile yet, because you’ve changed the CurrentWeatherView initializer.

Now that you have most pieces in place, it’s time to wrap up your navigation. Open WeeklyWeatherBuilder.swift and add the following:

import SwiftUI

enum WeeklyWeatherBuilder {
  static func makeCurrentWeatherView(
    withCity city: String,
    weatherFetcher: WeatherFetchable
  ) -> some View {
    let viewModel = CurrentWeatherViewModel(
      city: city,
      weatherFetcher: weatherFetcher)
    return CurrentWeatherView(viewModel: viewModel)
  }
}

This entity will act as a factory to create screens that are needed when navigating from the WeeklyWeatherView.

Open WeeklyWeatherViewModel.swift and start using the builder by adding the following at the bottom of the file:

extension WeeklyWeatherViewModel {
  var currentWeatherView: some View {
    return WeeklyWeatherBuilder.makeCurrentWeatherView(
      withCity: city,
      weatherFetcher: weatherFetcher
    )
  }
}

Finally, open WeeklyWeatherView.swift and change the cityHourlyWeatherSection property implementation to the following:

var cityHourlyWeatherSection: some View {
  Section {
    NavigationLink(destination: viewModel.currentWeatherView) {
      VStack(alignment: .leading) {
        Text(viewModel.city)
        Text("Weather today")
          .font(.caption)
          .foregroundColor(.gray)
      }
    }
  }
}

The key piece here is viewModel.currentWeatherView. WeeklyWeatherView asks WeeklyWeatherViewModel which view it should navigate to next. WeeklyWeatherViewModel makes use of WeeklyWeatherBuilder to provide the necessary view. There is a nice separation between responsibilities while at the same time keeping the overall relationship between them easy to follow.

There are many other approaches to solve the navigation problem. Some developers will argue that the View layer shouldn’t be aware to where it’s navigating, or even how that navigation should happen (modally or pushed). If that’s the argument, then it no longer makes sense to use what Apple provides with NavigationLink. It’s important to strike a balance between pragmatism and scalability. This tutorial leans towards the former.

Build and run the project. Everything should work as expected! Congratulations on creating your weather app! :]

Where to Go From Here?

You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

You certainly covered a lot in this tutorial with MVVM, Combine and Swift. It’s important to mention that each of these topics deserves a tutorial on its own and that today’s goal was for you to get your feet wet and to have a glimpse on the future of iOS development.

An upcoming update to Advanced iOS App Architecture will go deeper into the topics covered today. Be on the lookout for the update!

And, to learn even more about using Combine, check out our new book Combine: Asynchronous Programming with Swift!

We hope you enjoyed this MVVM with Combine Tutorial. If you have any questions or comments, please join the forum discussion below!

Average Rating

4.7/5

Add a rating for this content

15 ratings

Contributors

Comments