Mvvm swiftui

MVVM with Combine Tutorial for iOS

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 ViewModel 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 with your key inside , 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.

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 to decode the JSON from the OpenWeatherMap API. You’ll find out more about and 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 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 () 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 ().

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 from the . If this fails, return an error wrapped in a value. Then, erase its type to , since that’s the method’s return type.
  2. Uses the new method to fetch the data. This method takes an instance of and returns either a tuple or a .
  3. Because the method returns , you map the error from to .
  4. The uses of 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 as an auxiliary function to achieve this. Since you are only interested in the first value emitted by the network request, you set .
  5. If you don’t use you’ll have to carry over the full type returned by : . 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 . This is also useful because adding any new transformation (e.g. ) 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 conform to and . Conforming to these means that the ‘s properties can be used as bindings. You’ll see how to create them once you reach the View layer.]
  2. The properly delegate modifier makes it possible to observe the 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 , 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 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 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 ( object) to an array of 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 , 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 , you ensure the update you do in steps 5, 6 and 7 occurs in the right place.
  5. Start the publisher via . This is where you update 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 as an empty array.
  7. Update when a new forecast arrives.
  8. Finally, add the cancellable reference to the 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 property and an initializer inside the :

@ObservedObject var viewModel: WeeklyWeatherViewModel init(viewModel: WeeklyWeatherViewModel) { self.viewModel = viewModel }

The property delegate establishes a connection between the and the . This means that, when the ‘s property 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 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 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 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! establishes a connection between the values you’re typing in the and the ‘s property. Using allows you to turn the property into a . This is only possible because conforms to and is declared with the 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 properties without any fancy binds. This just displays the city name in a .

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 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:)) // 6 .store(in: &disposables) }

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

  1. Add a parameter, so you can specify which queue the HTTP request will use.
  2. The property uses the property delegate so it acts like any other . This means it can be observed and can also make use of any other method that is available to .
  3. As soon as you create the observation, 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 to provide a better user experience. Without it the would make a new HTTP request for every letter typed. works by waiting half a second () until the user stops typing and finally sending a value. You can find a great visualization of this behavior at RxMarbles. You also pass 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. You observe these events via and handle them with that you previously implemented.
  6. Finally, you store the cancelable as you did before.

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 , and, as such, this is what you’ll use in this tutorial.

If you look at ‘s most basic initializer: , you can see that it expects a 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 .

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 and . 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) } }

mimics what you did previously in :

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

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

@ObservedObject var viewModel: CurrentWeatherViewModel init(viewModel: CurrentWeatherViewModel) { self.viewModel = viewModel }

This follows the same pattern you applied in 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 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 method. This takes a function of type and executes it when the view appears. In this case, you call on the View Model so the 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 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 .

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 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 . asks which view it should navigate to next. makes use of 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 . 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!

raywenderlich.com Weekly

The raywenderlich.com newsletter is the easiest way to stay up-to-date on everything you need to know as a mobile developer.

Get a weekly digest of our tutorials and courses, and receive a free in-depth email course as a bonus!

Sours: https://www.raywenderlich.com/4161005-mvvm-with-combine-tutorial-for-ios

Your cart is empty.

instamobile-logo

Mega Bundle SALE is ON! Get ALL of our amazing iOS app codebases with 90% OFF discount 🔥

Get Bundle

In this article we are going to learn how to implement the powerful MVVM design pattern in SwiftUI applications that are leveraging the Combine framework. The combination of these 3 concepts will be the standard of iOS app architecture for the years to come, so it’s really important to set up a solid groundwork when it comes to designing Swift apps going forward.

mvvm combine swiftuiToday, we are discussing Combine and SwiftUI – the two latest frameworks that Apple has introduced recently. At the moment, tutorials simply introduce or analyze each framework individually. In this article, we’ll try to put them together to see how they fit together. Also, we will also use the MVVM pattern which is perfectly suitable for these two frameworks. Let’s go.

1. Why is MVVM The Most Popular Design Pattern in iOS?

If you’ve ever worked with RxSwift and RxCocoa, you’ll see the similarities between them and this new duo. But even with RxCocoa, we still need a place to bind data from ViewModel to View and vice versa – that was ViewController in the old UIKit world. Thus, MVVM has not really reflected its true nature: View – ViewModel – Model even when using RxSwift / RxCocoa. We have a more in-depth analysis of MVVM in our iOS MVVM Design Pattern article.

But with Combine and SwiftUI, MVVM truly shines. Because in SwiftUI, this iOS framework will actually re-render the component that needs to be updated. In other words, the binding steps have been done right on Views (sorry ViewController, but you have no more duties here). Let’s see how it works in practice. 

We are going to create a simple application that loads the list of breweries – which you can come to drink after this outbreak of COVID-19 disappears. So we need to create 3 main objects as follows:

  • Brewery: The Model
  • BreweriesViewModel
  • BreweriesView

2. Implementing The Model and The View in SwiftUI

The reason why we tackle both these objects in part one is that they are quite simple.

The URL we are going to use is `https://api.openbrewerydb.org/breweries`. Now let’s see how the JSON payload looks like.

[ { "id": 2, "name": "Avondale Brewing Co", "brewery_type": "micro", "street": "201 41st St S", "city": "Birmingham", "state": "Alabama", "postal_code": "35222-1932", "country": "United States", "longitude": "-86.774322", "latitude": "33.524521", "phone": "2057775456", "website_url": "http://www.avondalebrewing.com", "updated_at": "2018-08-23T23:19:57.825Z" }, {...} ]

So, with a response like this, the Model class will be Brewery, and it simply holds data we need. At this point, we just define three parameters to demo. As a challenge, you can get more parameters to show on your own UI.

struct Brewery { let name: String let street: String let city: String }

The View layer is next. We will have two views:

  • The first view is our main view – BreweriesView. It’s quite simple. It just contains a list inside a navigation view. The data is temporarily an empty array whose content type is Brewery.
  • A small note here is to let List be able to iterate on an array of Brewery, we need to make the Brewery model conform to the Hashable protocol.
struct BreweriesView: View { let breweries = [Brewery]() var body: some View { NavigationView { List(breweries, id: \.self) { BreweryView(brewery: $0) }.navigationBarTitle("Breweries") } } }

The item view of the list is a custom view – BreweryView. It takes a Brewery as a dependency – and uses this to render the UI. This is the place for you to unleash your creativity.

struct BreweryView: View { private let brewery: Brewery init(brewery: Brewery) { self.brewery = brewery } var body: some View { HStack { Image(uiImage: UIImage(named: "beer")!) .resizable() .scaledToFit() .frame(width: 80, height: 80) VStack(alignment: .leading, spacing: 15) { Text(brewery.name) .font(.system(size: 18)) .foregroundColor(Color.blue) Text("\(brewery.city) - \(brewery.street)") .font(.system(size: 14)) } } } }

3. Implementing The ViewModel with Combine

The main responsibility of this ViewModel is to fetch data from the server. After that, it will decode into our model types and then binding them to the View. Well, a lot of work, now let’s see how we deal with this:

class BreweriesViewModel: ObservableObject { private let url = "https://api.openbrewerydb.org/breweries" func fetchBreweries() { // To-do: implement here } }

From the iOS 13, URLSession supports a built-in publisher, which publishes data when the task completes or terminates if the task fails with an error. That is DataTaskPublisher.

As you can see here, the output will be data and URLRepsponse. So at this point, we just care about the Data. In the real world, you can check the URLResponse to validate more things from the server like `statusCode`. Now, let’s do it:

func fetchBreweries() { URLSession.shared.dataTaskPublisher(for: URL(string: url)!) .map { $0.data } }

After we had the data, we could decode it into an array of Breweries. Remember to make our model conform to Decodable

func fetchBreweries() { URLSession.shared.dataTaskPublisher(for: URL(string: url)!) .map { $0.data } .decode(type: [Brewery].self, decoder: JSONDecoder()) }

But what if the upstream publisher fails with an error? Or what if we have invalid data?
In such cases, we will use `replaceError`. It will replace any errors in the stream with the provided element. In our case, we would like to return an empty array. It’s fine for our tutorial.

func fetchBreweries() { URLSession.shared.dataTaskPublisher(for: URL(string: url)!) .map { $0.data } .decode(type: [Brewery].self, decoder: JSONDecoder()) .replaceError(with: []) }

Next, we need to erase the returned type to AnyPublisher then assign it to a property that can publish value to view. Since the data needs to be rendered on UI, we want to receive it on the MainThread. So, the final ViewModel class looks like this:

class BreweriesViewModel: ObservableObject { private let url = "https://api.openbrewerydb.org/breweries" private var task: AnyCancellable? @Published var breweries: [Brewery] = [] func fetchBreweries() { task = URLSession.shared.dataTaskPublisher(for: URL(string: url)!) .map { $0.data } .decode(type: [Brewery].self, decoder: JSONDecoder()) .replaceError(with: []) .eraseToAnyPublisher() .receive(on: RunLoop.main) .assign(to: \BreweriesViewModel.breweries, on: self) } }

4. MVVM Architecture with Combine in SwiftUI

Now, let’s combine all the pieces together, to fully understand how MVVM architecture can be achieved with Combine in SwiftUI apps. In the BreweriesView, we will call the ViewModel directly and the list which it needs comes from that ViewModel.
After setting up everything, we just need to invoke fetchBreweries() on the onAppear callback.

struct BreweriesView: View { @ObservedObject var viewModel = BreweriesViewModel() var body: some View { NavigationView { List(viewModel.breweries, id: \.self) { BreweryView(brewery: $0) }.navigationBarTitle("Breweries") .onAppear { self.viewModel.fetchBreweries() } } } }

Now, let’s run the iOS app. It should looks like this:

Conclusion

Congratulations, we’ve just built an iOS application that has a clean modern MVVM architecture and leverages the hottest Apple’s frameworks – Combine and SwiftUI.

Although simple, the app shows all the concepts related to SwiftUI, Combine, MVVM, and most importantly how they work together. Theoretically, MVVM is best suited for reactive programming frameworks, whereby we clearly divide the responsibilities of the modules (here, in particular, the presentation layer and UI layer). Also, as you can probably predict yourself, SwiftUI and Combine are likely to eliminate the legacy MVC architecture. Big changes are coming and we need to adapt, right?

Here is the full source code on Github. Please give us a star on that repository in case you find it useful. Don’t forget to share this article with your friends. Happy coding!

Share on FacebookShare on TwitterShare on Google+Share on LinkedinShare on Pinterest

Sours: https://iosapptemplates.com/blog/swiftui/mvvm-combine-swiftui
  1. Custom crewnecks etsy
  2. Large paper shades
  3. Dell mouse

The iOS Developer Community Survey shows that Model-View-ViewModel (MVVM) is the second most popular architectural pattern for designing iOS apps. It makes a good reason to study the state of modern MVVM with SwiftUI and Combine.

In this article we’ll cover:

  • The purpose of MVVM.
  • The components of the MVVM pattern.
  • The flow of data and dependencies in MVVM.
  • Why should we use unidirectional data flow instead of two-way bindings?
  • How to represent the UI as a finite-state machine?

And build an iOS app using the MVVM architecture pattern, Combine and SwiftUI frameworks.

History

MVVM has its roots in the Application Model pattern, invented in the Smalltalk engineering realm in 1988. The primary goal of the pattern was to split two kinds of logic, the presentation, and the business logic, into two separate objects: the Application Model and the Domain Model, respectively.

In 2004, Martin Fowler rebranded Application Model into Presentation Model (PM). The idea of PM is to create a UI-agnostic object Presentation Model that pulls all the state and behavior out of a View. This way, the view merely projects the state of the presentation model onto the screen.

Microsoft introduced MVVM in 2006 for designing and implementing desktop client applications with the Windows Presentation Foundation (WPF) UI framework. With MVVM, Microsoft pursued the goal of standardizing the way WPF applications are developed. The pattern intended to leverage the power of the WPF framework, such as data binding.

According to Microsoft, MVVM is a specialized version of Fowler’s Presentation Model. Martin Fowler even claims that they are the same.

The Purpose of MVVM

The goal of MVVM is to separate the business and presentation logic from the UI. It improves testability and maintainability, which are often the key success factors of an app.

To achieve its goal, MVVM minimizes decision-making in the views and moves view state and behavior into the view model. This way, the view becomes passive:

  • The view does not pull data from the view model.
  • The view is not responsible for updating itself from the view model.
  • The view has its state managed by the view model.

Such a design allows us to test presentation logic in isolation from the GUI stack.

The MVVM pattern

MVVM is the UI pattern. As with the most rich client systems, this is where a large part of your iOS app’s codebase sits. SwiftUI views, UIKit views and view controllers, storyboards and xibs all belong in here.

MVVM provides a set of guidelines on:

  • How to display information on the UI.
  • How to handle interactions between the user and the app.
  • How to interpret user inputs into actions upon business rules and data.

MVVM can be broken down into the three components that follow a strict dependency rule:

Modern MVVM iOS App Architecture with Combine and SwiftUI

Dependencies are organized in the following way:

  • The view depends on the view model.
  • The view model depends on the model.
  • Neither the model nor the view model depends on the view.

Depends on means code dependency, like imports, references, function calls.

Note that the flow of the data is different as compared with that of the dependency:

Modern MVVM iOS App Architecture with Combine and SwiftUI

That is, the data flows in both directions. It starts with a user interaction, which is handled by the view. Next, the view passes interaction events to the view model. Then the view model translates the events into CRUD (create, read, update and delete) operations upon model and data.

The reverse flow is also the case. The model fetches data from the backend, or a database, or any other source. Next, the model passes data to the view model. Then the view model prepares data in a form that is convenient for the view to consume. Lastly, the view renders data onto the screen.

Now let’s discover the roles of the MVVM components.

ViewModel

The ViewModel represents the data as it should be presented in the view, and contains presentation logic.

The responsibilities of the ViewModel are:

  • Manage UI behavior and state.
  • Interpret user inputs into actions upon business rules and data. Typically, a view model maintains a one-to-many relationship with model objects.
  • Prepare data from a model to be presented to a user. A view model structures data in a way that is convenient for a view to consume.

The ViewModel is independent of the UI frameworks. Think twice if you are about to import SwiftUI or UIKit inside your file.

View

The View renders the UI and passes user interactions forward. It has no state and does not contain any code-behind that interprets user actions.

The responsibilities of the View are:

  • Render the UI.
  • Perform animations.
  • Pass user interactions to a view model.

Model

The Model is the software representation of the business concepts that earn money or bring any other value to your customer. It is the primary reason why your iOS app is actually written.

Although MVVM has the Model as a part of its name, MVVM does not make any assumptions about its implementation. It can be Redux or a variation of Clean Architecture, like VIPER.

The Modern State of MVVM

The following techniques shape what I consider to be the modern state of MVVM in Swift.

FRP and Data Binding

The single most important aspect that makes MVVM viable in the first place is data binding.

Data binding is a technique that connects the data provider with consumers and synchronizes them.

Using the data binding technique, we can create streams of values that change over time. Functional Reactive Programming (FRP) is a programming paradigm concerned with data streams and the propagation of change. In FRP, streams of values are first-class citizens. It means that we can build them at runtime, pass around, and store in variables.

The Combine and SwiftUI frameworks provide first-party FRP support, which allows us to seamlessly reflect view model changes in a view, and remove the need for writing code in a view model that directly updates a view.

Unidirectional Data Flow Over Two-Way Bindings

Many applications of the MVVM pattern use two-way bindings to synchronize a view with a view model. Explanations of such an approach are often accompanied by an example of a counter app. Although it works fine for two data streams – counter increment and counter decrement – the two-way binding approach does not scale well when applied to production-like features.

Let’s demonstrate the problems by example. Here is a sign-up screen with just four states:

Modern MVVM iOS App Architecture with Combine and SwiftUI

If we wish to implement it with MVVM and connect a view and a view model with two-way bindings, it will likely look next:

Modern MVVM iOS App Architecture with Combine and SwiftUI

Every arrow represents a stream of values. The streams are also connected with each other:

Modern MVVM iOS App Architecture with Combine and SwiftUI

The figure still has some details missing. Typically, in a production app, you will send network requests, and allow your users to login with identity providers, like Google or Facebook. The two-way binding approach gets out of control very quickly and eventually ends up like this:

Modern MVVM iOS App Architecture with Combine and SwiftUI

The second problem with the two-way bindings is error handling. Out-of-the-box, the Combine framework does not provide a concept of a never-ending-stream-of-values, like RxSwift Relay. Therefore, when an error occurs, it will terminate the whole stream and potentially leave your app’s UI unresponsive. Although you can recreate Relays in Combine on top of or , it may not be the right way to go for the reasons explained in the next section.

UI as a State Machine

Another significant issue that falls out of the two-way bindings approach is state explosion.

What is a state? The state of an object means the combination of all values in its fields. Therefore, the combinatorial number of UI states grows with factorial complexity. However, most of such states are unwanted or even degenerate.

For instance, let’s take the and streams from the sign-up example. It’s unclear how to render the UI in case sends and sends a non- value. Should we show a loading indicator? Or an alert box with an error message? Or both?

There are even more problems with unexpected states:

  • They create lots of code paths that are very difficult to test exhaustively.
  • The complexity of adding new states keeps accumulating.

The solution is to work out all possible states and all possible actions that trigger state transitions and make them explicit. A finite-state machine (FSM) is the computational model that formalizes this idea.

The FSM can be in exactly one of a finite number of states at any given time. It can change from one state to another in response to external inputs; such change is called a transition [1].

The UI FSM will manage the state of a view and handle user inputs via a state-transitioning function that may include additional side-effects. A state machine is fully defined in terms of its [2]:

  • Set of inputs.
  • Set of outputs.
  • Set of states.
  • Initial state.
  • State transitioning function.
  • Output function.

In this article, we’ll be using the CombineFeedback library that lends itself to designing reactive state machines. With CombineFeedback, the structure of the app components looks next:

Modern MVVM iOS App Architecture with Combine and SwiftUI

Let’s describe the core components of the above figure.

State represents the state of your finite-state machine.

Event describes what has happened in a system.

Reduce specifies how the state changes in response to an event.

Feedback is the extension point between the code that generates events and the code that reduces events into a new state. All your side effects will sit in here. Feedback allows us to separate side effects from the pure structure of the state machine itself (see it in green).

ViewModel fully initializes a UI state machine.

To set up a state machine, we’ll need the operator and the type. The operator creates a feedback loop and bootstraps all dependencies:

The snippet below is based on the implementation from RxFeedback.

produces a stream of events in response to state changes. It allows us to perform side effects, like IO, between the moments when an event has been sent, and when it reaches the reduce function:

The snippet below is based on the implementation from CombineFeedback.

Finally, let’s discover how to put everything together.

Building an App with MVVM

To follow the code samples you’ll need some basic knowledge of the SwiftUI and Combine frameworks. Getting Started with Combine and Apple SwiftUI tutorials will get you up to speed.

Let’s explore the MVVM iOS app architecture by building a movie app from scratch. Here is how the final result will look:

Modern MVVM iOS App Architecture with Combine and SwiftUI

Implementing Movies List ViewModel

The app has two screens:

  • A list of trending movies.
  • Details of a movie.

We begin with the movies list. Before writing any code, we must design the state machine:

Modern MVVM iOS App Architecture with Combine and SwiftUI

Based on the figure, it’s trivial to represent a list of states and events in code. I prefer declaring them as inner types of a view model:

Note that implements the protocol. This allows us to bind a view to the view model. SwiftUI will automatically update the view whenever the view model updates its state.

Some of the states have associated values in order to draw them on the UI or to pass to the next state. Similarly, events carry data, which is the only source of information when we produce a new state inside a function.

Now we can implement a function that defines all possible state-to-state transitions:

On the state machine figure, you can see all the events except for . The reason for that is because is sent as a result of user interaction with an app. User input is a side effect that needs to be handled inside feedback:

Then we initialize the state machine using the operator:

The takeaways are:

  1. is the entry point of the feature. It connects all the dependencies and starts the state machine.
  2. The feedback handles networking. We’ll implement it in a moment.
  3. The method provides a way of passing user input and view lifecycle events. Using the subject, we propagate the events into the feedback loop for processing.

The two pieces that are missing are the feedback and the type. Both of them are related to loading movies from the network.

When the system enters the state, we initiate a network request:

Here is what we are doing:

  1. Check that the system is currently in the state.
  2. Fire a network request.
  3. In case the request succeeds, the feedback sends an event with a list of movies.
  4. In case of a failure, the feedback sends an event with an error.

The network client talks to TMDB API in order to fetch trending movies. I am skipping some implementation details to keep focus on the main subject:

You can learn how to build a promise-based networking layer with Combine here.

A list entry is represented with an object:

The DTO suffix means that we are using the Domain Transfer Object pattern.

is a mapping of for the purpose of presentation:

Implementing Movies List View

After designing the view model, now we can start with the implementation of the view.

First, bind the view to the view model state updates by means of the property wrapper:

Next, in the , we want to send a lifecycle event to the view model:

State rendering takes place in the variable:

Here how the method is implemented:

represents details of a movie. If a user taps a list row, it will be pushed onto the navigation stack. is initialized with a view model, which, in its turn, accepts a movie identifier.

represents a list row. Note that it accepts a view model of type rather than . It is important not to mix the infrastructure details, i.e. , with the presentation, i.e. the view models. I am skipping the implementation of since it is not directly relevant to our subject.

Implementing Movie Details

Movie details state machine is identical as compared with that of the list of trending movies:

Modern MVVM iOS App Architecture with Combine and SwiftUI

Here is how we represent the movie details state machine in code:

In order to pass user events, we create a feedback:

Next, we declare one more feedback that fires a network request:

It calls from that fetches movie details provided movie identifier:

is created for the purpose of parsing network response. It shouldn’t leak into the UI layer. When the view model receives a successful network response, it maps into . The latter is the representation of the same data that is convenient for a view to consume.

Then we initialize the state machine:

Now we can implement a view:

Source Code

You can find the final project here. It is published under the “Unlicense”, which allows you to do whatever you want with it.

References


Thanks for reading!

If you enjoyed this post, be sure to follow me on Twitter to keep up with the new content. There I write daily on iOS development, programming, and Swift.

Sours: https://www.vadimbulavin.com/modern-mvvm-ios-app-architecture-with-combine-and-swiftui/

MVVM in iOS with SwiftUI (Detailed Example + Pitfalls)

Since the introduction of SwiftUI, the MVVM pattern has seen a new renaissance. Many developers believe that this particular pattern fits well with the SwiftUI data flow.

MVVM certainly has some good ideas, but it also brings along problems because of the various discording interpretations of the pattern and its rigidity.

In this article, we will see how MVVM fits in iOS apps written in SwiftUI, how to take advantage of its benefits, and how to avoid its problems.

Contents


Chapter 1

How MVVM improves the structure of iOS apps


Chapter 2

The model layer is the foundation of an app’s architecture


Chapter 3

Organizing the view layer and simplifying view models


Chapter 4

The networking infrastructure of an MVVM app


Chapter 5

Bringing an app together using view models

The structure of the MVVM pattern and the roles of its layers

If I had to define MVVM quickly, I would say that it’s a variation of the MVC pattern.

But let’s proceed in order.

Like MVC, the Model-View-ViewModel pattern, or MVVM in short, is an architectural pattern that guides how you structure the code in your iOS apps. MVVM consists of three layers, from which it takes its name.

Diagram for the MVVM pattern in iOS apps

Each layer has a well-defined role in the app’s structure, helping you respect the separation of concerns design principle.

  • In the model layer, we find the Swift types representing your app’s data and its domain business logic.
  • The view layer is what the user sees on the screen. In iOS, it contains the SwiftUI views for your app, which display information and allow user interaction.
  • Finally, a view model connects a view to the app’s model. A view model contains the current state of a view, links to other parts of an app’s architecture (e.g., data storage or the network), and handles the user’s interaction.

The MVVM pattern is not unique to iOS. In fact, it was invented by Microsoft architects (of all people). It found its way into iOS apps only years after the release of the first iPhone.

Traditionally, Apple has followed the MVC pattern for both macOS and iOS apps. That changed with the introduction of SwiftUI.

MVVM uses binders to connect views and view models

Looking at the MVC pattern diagram, it does not take a genius to see that MVVM is, practically speaking, the same pattern. For that reason, I usually talk about architecture in terms of MVC, although I use ideas from both patterns.

The most crucial difference with MVC is that MVVM connects the view and the view model layers using a binder. This synchronizes the data between the two layers, removing boilerplate code.

That’s a natural part of SwiftUI.

In UIKit, developers used an FRP framework like RxSwift. SwiftUI instead uses, behind the scenes, Combine, Apple’s native reactive framework. But you don’t need to learn Combine to make SwiftUI apps.

So, in SwiftUI, the most significant difference between MVVM and MVC has disappeared. While SwiftUI offer several data flow mechanisms, there is only “one” way to connect objects and views:

  • An object must conform to the ObservableObject protocol.
  • Such an object must expose any property that affects the user interface using the @Published property wrapper.
  • Views connect to observed objects through the @StateObject, @ObservedObject, and @EnvironmentObjects property wrappers.

MVVM vs. MVC: local view models and of global controllers

There is another fundamental difference between MVC and MVVM.

In MVC, the emphasis has always been on making controllers globally shared objects. It was not uncommon to use singletons for that, although dependency injection became, with time, the preferred alternative.

In MVVM, instead, each view gets its separate view model. Objects, then, move from global to local.

In UIKit, both interpretations were possible because there was a fourth, not-so-hidden layer: view controllers. These sat squarely between views and controllers/view models. Since they were required by the framework, it was impossible to get rid of them.

That does not happen in SwiftUI. If you make an iOS app with more than one screen, you soon conclude that both local and global states are needed.

That’s why SwiftUI offers three property wrappers instead of just one. So, again the difference between MVC and MVVM disappears. We can have both controllers (global) and view models (local) in a full app.

Creating model types to represent the data of an app

As an example, we will build a small app for Hacker News, a news website for developers like Reddit, known for its (debatable) quality.

We will use its simple web API to fetch the top 10 news stories from the best stories page.

Mockup for the Hacker News app to illustrate the MVVM pattern in SwiftUI

You can find the complete Xcode project on GitHub.

As it’s often the case, we will start creating the model layer of our app. These are the easiest to implement and the foundation for the whole app’s architecture.

Model types only contain data and the code that manipulates it. They should not know anything about data storage, networking, or how data is presented to the user. In short, they should not know anything about the other layers of the MVVM pattern.

The Hacker News API uses a single item entity to represent all its data. Stories, comments, jobs, etc. are all items. So, creating a corresponding Swift type is straightforward:

structItem: Identifiable{

letid:Int

letcommentCount:Int

letscore:Int

letauthor:String

lettitle:String

letdate:Date

leturl:URL

}

JSON decoding goes inside the model layer

Since the Hacker News API returns data in JSON format, our Item struct must conform to the Decodable protocol and provide some coding keys to map the JSON fields to our properties. You can read more about JSON decoding in my Codable article.

extensionItem: Decodable{

enumCodingKeys:String,CodingKey{

caseid,score,title,url

casecommentCount="descendants"

casedate="time"

caseauthor="by"

}

}

This is an example of code that manipulates data. Model types are not just empty containers. Data transformation code goes into model types together with the domain business logic.

Granted, our Item type has no business logic. That often happens for data we fetch from a web API. You can find an example in my free guide on MVC and MVVM in SwiftUI.

While we are at it, we can also create some test data we will use later for our SwiftUI previews. First, we need to grab the information for a story from the API, which we can save in a .json file in our Xcode project.

{

"by":"theafh",

"descendants":312,

"id":24777268,

"kids":[24778001,24788825,24777957,24778210,24778026,24779203,24778283,24780437,24778385,24779634,24779048,24777921,24779934,24787940,24781384,24779512,24784656,24782672,24779975,24779050,24787912,24785350,24784794,24785141,24780574,24778706,24778457,24780008,24780571,24779757,24785643,24778735,24779575,24778254,24777996,24778341,24777945,24777875,24779393,24780180,24782796,24779270,24780626,24779061,24778700,24781183,24779653,24779074,24778439,24777831,24779329,24778009],

"score":1082,

"time":1602687710,

"title":"Room-Temperature Superconductivity Achieved for the First Time",

"type":"story",

"url":"https://www.quantamagazine.org/physicists-discover-first-room-temperature-superconductor-20201014/"

}

Then, we decode it inside a TestData structure to make it easy to use in any SwiftUI preview.

structTestData{

staticletstory:Item={

leturl=Bundle.main.url(forResource:"Story",withExtension:"json")!

letdata=try!Data(contentsOf:url)

letdecoder=JSONDecoder()

decoder.dateDecodingStrategy=.secondsSince1970

returntry!decoder.decode(Item.self,from:data)

}()

}

Again, refer to my Codable article linked above for more details about JSON and test data.

Views should be independent of model types

We will now jump to the other edge of the MVVM pattern and create our views.

I am skipping the view model layer because it’s the most complicated. Moreover, it’s hard to write a view model’s code when we don’t have its related view.

We can start with some simple views to represent the position, upvotes, and comments for each row in our News screen.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

structBadge: View{

lettext:String

letimageName:String

varbody:someView{

HStack{

Image(systemName:imageName)

Text(text)

}

}

}

 

structPosition: View{

letposition:Int

varbody:someView{

ZStack{

Circle()

.frame(width:32.0,height:32.0)

.foregroundColor(.teal)

Text("\(position)")

.font(.callout)

.bold()

.foregroundColor(.white)

}

}

}

 

structNewsView_Previews: PreviewProvider{

staticvarpreviews:someView{

Group{

Position(position:1)

Badge(text:"1.234",imageName:"paperplane")

}

.previewLayout(.sizeThatFits)

}

}

The Xcode preview for the components of the News screen

I could have used the Label type of SwiftUI instead of creating the Badge view, but the former has a spacing that does not fit my mockup.

What is important here is that our two views are entirely independent of our Item structure. Instead, they use simple Swift types like Int and String. This is an excellent practice to keep your types as loosely coupled as possible so that changes break as little code as possible.

Formatting code should be kept out of views and view models

The view for a single story also looks straightforward. But here we have to pause for a moment.

Our UI requires a lot of data formatting:

  • The numbers for upvotes and comments need decimal separators;
  • We only need to show the domain name for a story, not its full URL;
  • A story doesn’t have a date but shows how much time has passed since its submission.

Formatting is related to the visual representation of data, so its code does not belong to the Item structure.

At the same time, we want to keep views independent and reusable by using simple Swift types. So, where do we put our formatting code?

Some proponents of MVVM think that formatting code should go into view models. But that’s a mistake.

First of all, this would overload view models with responsibilities. Moreover, the formatting code needs to be reusable and not tied to a single view or view model.

While we only have a single screen in our sample app, that rarely happens. A separate screen showing each story’s comments, as it happens on the Hacker News website, would need to format data in the same way.

Luckily, Swift offers a perfect solution: extensions.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

extensionURL{

varformatted: String{

(host??"").replacingOccurrences(of:"www.",with:"")

}

}

 

extensionInt{

varformatted: String{

letformatter=NumberFormatter()

formatter.numberStyle=.decimal

returnformatter.string(from:NSNumber(value:self))??""

}

}

 

extensionDate{

vartimeAgo: String{

letformatter=RelativeDateTimeFormatter()

formatter.unitsStyle=.short

returnformatter.localizedString(for:self,relativeTo:Date())

}

}

Don’t follow the MVVM pattern too strictly

Now, I have a question for you.

To which layer of MVVM does the formatting code above belong?

To be honest, I don’t think that’s a question that makes much sense. This shows you the dangers of adhering too strictly to a particular design pattern.

It looks like the code above belongs to the model layer since we are extending data types like Int, URL, and Date. At the same time, our extensions do not affect those types. We own that code, and we only use it in the view layer.

We can even place it inside the same files as our views. If I had to find a definite answer, I would say that our extensions are still part of the view layer.

Which brings me to the next point.

Keeping the view and model layers decoupled using Swift extensions

This could be a possible implementation for the view representing a story.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

structStory: View{

letposition:Int

letitem:Item

varbody:someView{

HStack(alignment:.top,spacing:16.0){

Position(position:position)

VStack(alignment:.leading,spacing:8.0){

Text(item.title)

.font(.headline)

Text(footnote)

.font(.footnote)

.foregroundColor(.secondary)

ZStack(alignment:Alignment(horizontal:.leading,vertical:.center)){

Badge(text:item.score.formatted,imageName:"arrowtriangle.up.circle")

.foregroundColor(.teal)

Badge(text:item.commentCount.formatted,imageName:"ellipses.bubble")

.padding(.leading,96.0)

.foregroundColor(.orange)

}

.font(.callout)

.padding(.bottom)

}

}

.padding(.top,16.0)

}

varfootnote: String{

item.url.formatted

+" - \(item.date.timeAgo)"

+" - by \(item.author)"

}

}

This view is coupled with our Item structure.

In a simple app, I would consider that to be acceptable. There is no need to complicate your code and keep it as generic as possible when that’s not needed.

In more complex apps, though, you often need to reuse UI components with different data types. Moreover, the Story view above embeds formatting code that might not apply to all data.

In that case, we need to decouple the Story view from the Item model type.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

structStory: View{

letposition:Int

lettitle:String

letfootnote:String

letscore:String

letcommentCount:String

varbody:someView{

HStack(alignment:.top,spacing:16.0){

Position(position:position)

VStack(alignment:.leading,spacing:8.0){

Text(title)

.font(.headline)

Text(footnote)

.font(.footnote)

.foregroundColor(.secondary)

ZStack(alignment:Alignment(horizontal:.leading,vertical:.center)){

Badge(text:score,imageName:"arrowtriangle.up.circle")

.foregroundColor(.teal)

Badge(text:commentCount,imageName:"ellipses.bubble")

.padding(.leading,96.0)

.foregroundColor(.orange)

}

.font(.callout)

.padding(.bottom)

}

}

.padding(.top,16.0)

}

}

This does not solve our problem, though. It only moves it up the view hierarchy. The view that contains the Story type will need to use the code we just removed.

Here, Swift extensions come to our rescue again.

extensionStory{

init(position:Int,item:Item){

self.position=position

title=item.title

score=item.score.formatted

commentCount=item.commentCount.formatted

footnote=item.url.formatted

+" - \(item.date.timeAgo)"

+" - by \(item.author)"

}

}

We can now initialize our view using an Item value, while its internal code uses only simple Swift types.

We can even export our Story view to another app without the Item type. All we need is a new extension using the model types of the destination project.

We can now use this new initializer in two places. The first is inside the SwiftUI preview.

structNewsView_Previews: PreviewProvider{

staticvarpreviews:someView{

Group{

Story(position:1,item:TestData.story)

Position(position:1)

Badge(text:"1.234",imageName:"paperplane")

}

.previewLayout(.sizeThatFits)

}

}

Xcode preview for the row view

The second is in the view for the whole News screen.

structNewsView: View{

letstories:[Item]

varbody:someView{

List(stories.indices){index in

Story(position:index+1,item:stories[index])

}

.navigationTitle("News")

}

}

Xcode preview for the complete News view

Should networking code go into view models?

Our app needs to fetch data through the Hacker News API. Networking code clearly does not go into either the model or the view layers.

This is another long-standing debate about MVVM, where some proponents insist that networking code goes inside a view model?

I beg to differ.

If you look at it from the point of view of reusing code, that’s a mistake. While a view model clearly needs to trigger network requests to fetch data, networking code is filled with boilerplate.

That’s code that is clearly going to be repeated in every view model that performs network requests.

Here, I think the culprit is the use of reactive frameworks like RxSwift and Combine. All that boilerplate code gets lost into the glue code that connects streams of data.

But that’s just replacing some boilerplate with some other boilerplate.

Creating a separate networking layer to keep boilerplate code out of view models

A better approach to iOS networking in Swift is to build a separate infrastructure for API requests.

Our sample app doesn’t need a networking hierarchy as complex as the one in that article. Still, we can use a generic network request class, which simplifies our view model code and is easy to extend.

To fetch the best stories from the API, we need to perform two types of requests.

  • Usually, REST APIs return arrays of JSON objects with the full data. The best stories endpoint, instead, only returns an array of IDs.
  • After getting those IDs, we need to call the story endpoint several times to fetch each story’s data.

So, our API request class needs to handle different types of JSON data.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

classAPIRequest{

leturl:URL

init(url:URL){

self.url=url

}

funcperform<T:Decodable>(with completion:@escaping(T?)->Void){

letsession=URLSession(configuration:.default,delegate:nil,delegateQueue:.main)

lettask=session.dataTask(with:url){(data,_,_)in

guard letdata=dataelse{

completion(nil)

return

}

letdecoder=JSONDecoder()

decoder.dateDecodingStrategy=.secondsSince1970

completion(try?decoder.decode(T.self,from:data))

}

task.resume()

}

}

In short, the perform(with:) method uses a URLSession to fetch data and then decodes it with a JSONDecoder. Since it uses a Swift generic, we can use this method for any Decodable data.

Again, refer to the article about REST APIs I linked above for a full explanation.

A view model contains the app’s business logic that drives a single app screen

We can finally focus on the core layer of MVVM: the view model layer.

A view model is limited only to the logic that drives a specific screen in an app. In our example, we only need one for the NewsView type, representing the whole News screen.

Our view model needs to perform all the network requests to retrieve the top 10 best stories from Hacker News.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

classNewsViewModel: ObservableObject{

@Published varstories:[Item?]=Array(repeating:nil,count:10)

funcfetchTopStories(){

leturl=URL(string:"https://hacker-news.firebaseio.com/v0/beststories.json")!

letrequest=APIRequest(url:url)

request.perform{[weakself](ids:[Int]?)->Voidin

guard letids=ids?.prefix(10)else{return}

for(index,id)inids.enumerated(){

self?.fetchStory(withID:id){story in

self?.stories[index]=story

}

}

}

}

funcfetchStory(withID id:Int,completion:@escaping(Item?)->Void){

leturl=URL(string:"https://hacker-news.firebaseio.com/v0/item/\(id).json")!

letrequest=APIRequest(url:url)

request.perform(with:completion)

}

}

First of all, the NewsViewModel class conforms to the ObservableObject protocol and publishes its stories property using the @Published property wrapper. This is the “binder” part of MVVM we discussed at the beginning of the article.

While the NewsViewModel class contains the logic to retrieve the stories, it does not know anything about the NewsView. This leaves the view model decoupled from its view, making it easier to write unit tests.

I divided the fetching of data into two methods to avoid callback hell. This is the simplest way to achieve that without using a reactive framework like Combine.

The fetchTopStories() method fetches the IDs of the best stories. Then, it calls the fetchStory(withID:completion:) method for each of these. This, in turn, fetches the details of each story.

All these requests happen in parallel, and their callbacks might occur in any order. Appending the stories to the stories array might then change their order.

To avoid complex synchronization code, I first populate the stories array with 10 nil values and replace them with the corresponding story using indexes.

Creating view models in SwiftUI views and triggering events

All we have left is connecting our view to its view model.

structNewsView: View{

@StateObject private varmodel=NewsViewModel()

varbody:someView{

List(model.stories.indices){index in

ifletstory=model.stories[index]{

Story(position:index+1,item:story)

}

}

.navigationTitle("News")

.onAppear(perform:model.fetchTopStories)

}

}

The NewsView type creates the NewsViewModel instance using the @StateObject property wrapper. Then, it triggers the network requests by calling the fetchTopStories() method in its .onAppear modifier.

The @ObservedObject wrapper would be a mistake. The view would recreate its view model every at every refresh of the view hierarchy, with the effect of losing the callbacks of all the network requests in progress.

We could also use the @EnvironmentObject wrapper in this specific case, but that’s not a common practice. View models often need to be initialized with particular values, which is only possible in a view’s initializer.

The @EnvironmentObject is more appropriate for shared instances of global objects. The ones that are usually called controllers in the MVC pattern.

To complete our app, we only need to add a navigation view to our app’s entry point.

structHackerNewsApp: App{

varbody:someScene{

WindowGroup{

NavigationView{

NewsView()

}

}

}

}

The running Hacker News app

Conclusions

The MVVM pattern provides a useful architectural idea: the views in an iOS app often need dedicated objects. These are the view models of MVVM.

In this article, we have seen one of the most common uses for view models: performing a sequence of network requests to fetch data for a single view.

We also saw how to solve many of the problems of sticking to the pattern definition too strictly. Making formatting and networking code reusable simplifies all the view models in an app.

There is only so much this article can cover about an app’s full architecture. As I mentioned, complex apps often need global objects.

The MVVM pattern alone is too limited. Trying to shoehorn all your code into view models is only going to create problems.

There are also some problems that a strict approach to MVVM does not address. For example, why does the NavigationView in the example above go into the main app structure? And what happens in apps with tricky navigation?

Moreover, adding objects to views breaks Xcode previews. After adding the NewsViewModel instance to the NewsView, you will notice that its preview becomes empty.

To solve all these problems, we need an extra layer, which I call the root layer. To know more about it, get my free guide below.

Architecting SwiftUI apps with MVC and MVVM

It’s easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.

GET THE FREE BOOK NOW
This entry was posted in Uncategorized on by Matteo Manferdini. Sours: https://matteomanferdini.com/mvvm-pattern-ios-swift/

Swiftui mvvm

.

.

Now discussing:

.



1315 1316 1317 1318 1319