Combine-based MVVMs in UIKit and SwiftUI Applications for UIKit Developers



We know that ObservableObject classes with its @Publishedproperties are created in Combinespecially for View Modelin SwiftUI. But exactly the same View Modelcan be used in the UIKitimplementation of the architecture  MVVM, although in this case we will have to manually “bind” ( bind) the UIelements to the @Published properties of View Model. You will be surprised, but with the help of Combinethis a few lines of code are done. In addition, adhering to this ideology when designing UIKitapplications, you will subsequently painlessly switch to SwiftUI.

The purpose of this article is to show with a primitively simple example how you can elegantly implement an MVVMarchitecture UIKitwith  Combine. For contrast, we show the use of the sameView Model c SwiftUI.

The article will discuss two simple applications that allow you to select the latest weather information for a specific city from the OpenWeatherMa p site . But UIone of them will be created with application SwiftUI, and the other with help UIKit. For the user, these applications will look almost the same.



The code is on Github .

The user interface ( UI) will contain only 2 UI elements: a text field to enter the city and a label to display the temperature. The text box for entering the city is the active INPUT ( Input), and the temperature label is the passive EXIT ( Output).  

The role View Model in architecture MVVMis that it takes the INPUT (s) from View(or ViewControllerto UIKit), implements the business logic of the application and passes the OUTPUTS back to  View(or ViewControllerto UIKit), possibly presenting this data in the desired format.

Creating  View Modelwith Combineno matter what kind of business logic — synchronous or asynchronous — is very simple if you use a ObservableObject class with its @Publishedproperties.

API OpenWeatherMap


Although the OpenWeatherMap service    allows you to select very extensive weather information, the Model of the data we are interested in will be very simple, it provides detailed information  WeatherDetailabout the current weather in the selected city and is located in the Model.swift file  :



Although in this specific task we will be interested only in temperature temp, which is in the structure  Main, the Model provides full detailed information about the current weather as a root structure  WeatherDetail, believing that in the future you will want to expand the capabilities of this application. The structure WeatherDetail is Codable, this will allow us to literally decode the JSONdata into the Model with just two lines of code  .

The structure  WeatherDetail should also beIdentifiableif we want to make it easier for ourselves in the future to display an array of weather forecasts  [WeatherDetail] for several days in advance in the form of a list  List of SwiftUI. This is also a blank for a future more sophisticated current weather app. The protocol Identifiablerequires the presence of the property id,that we already have, so no additional efforts will be required from us.

Usually, services, including the OpenWeatherMap service  , offer all kinds of services URLs to get the resources we need. OpenWeatherMap service  offers us URLsto fetch detailed information about the current weather or forecast for 5 days in a certain city city. In this application, we will be interested only in current weather information and for this caseURLcalculated using the function absoluteURL (city: String):



API for the OpenWeatherMap service,  we will place it in the WeatherAPI.swift file . Its central part will be a method for selecting detailed weather information  WeatherDetailin a city  city:

  • fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>

In the context of the framework, Combine this method returns not just detailed weather information  WeatherDetail, but the corresponding "publisher" Publisher. Our "publisher" AnyPublisher<WeatherDetail, Never>does not return any error - Neverand if a sampling or coding error still occurred, then the deputy returns  WeatherDetail.placeholderwithout any additional messages about the cause of the error. 

Consider in more detail the method  fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>that selects  detailed weather information for the city from the OpenWeatherMap website  cityand does not return any error Never:



  1. based on the name of the city, we  city form URL using the function absoluteURL(city:city)to request detailed weather information  WeatherDetail,
  2. «» dataTaskPublisher(for:), Output (data: Data, response: URLResponse),   Failure - URLError,
  3. map { } (data: Data, response: URLResponse)  data
  4. JSON  data ,  WeatherDetail, ,
  5. - «»  catch (error ... )  «» WeatherDetail.placeholder,
  6. main , UI,
  7. «» «» eraseToAnyPublisher() AnyPublisher.

The asynchronous “publisher” thus obtained AnyPublisher“does not take off” by itself; it does not deliver anything until someone “subscribes” to it. We will use it in a  ObservableObject class that plays a role View Modelin  SwiftUIboth and UIKit

Create View Model


To View Modelcreate a very simple class  TempViewModelthat implements a protocol ObservableObject with two  @Published properties:  



  1. one  @Published var city: Stringis the city (you can conditionally call it an ENTRANCE, since its value is regulated by the user on View),  
  2. the second  @Published var currentWeather = WeatherDetail.placeholder is the weather in this city at the moment (we can conditionally call this property EXIT, as it is obtained by fetching data from the OpenWeatherMap website  ).

Once we have set a  @Published property  city, we can begin to use it both as a simple property  cityand as a "publisher"  $city.

In a class  TempViewModel, you can not only declare the properties of interest to us, but also prescribe the business logic of their interaction. To this end, when initializing a class instance  TempViewModel in, init?we can create a “subscription” that will operate throughout the entire “life cycle” of the class instance  TempViewModeland reproduce the dependence of the current weather  currentWeather on the city  city.

To do this, Combinewe stretch the chain from the input "publisher" $city to the output "publisher" AnyPublisher<WeatherDetail, Never>, whose value is the current weather. Subsequently, we “subscribe” to it with the help of a “subscriber” assign (to: \.currentWeather, on: self) and get the desired value of the current weather  currentWeather as an “output”  @Published property.

We must pull the chain NOT simply from the properties city, namely from the "publishers" $citywho will participate in the creation UI and it is there that we will change it.

How will we do this?

We already have a function in our arsenal fetchWeather (for city: String)that is in the class  WeatherAPIand returns the “publisher” AnyPublisher<WeatherDetail, Never> with detailed weather information depending on the city  city, and we can only somehow use the value of the “publisher”  $cityto turn it into an argument of this function.

 Go to the right publisher  fetchWeather (for city: String) to  Combinehelp us operator  flatMap:



OperatorflatMapcreates a new “publisher” based on data received from the previous “publisher”.

Next, we “subscribe” to this newly received “publisher” with the help of a very simple “subscriber”  assign (to: \.currentWeather, on: self)and assign the value received from the “publisher” to the @Publishedproperty  currentWeather:



We just created an init( )ASYNCHRONOUS “publisher” and “subscribed” to it, resulting in a AnyCancellable“subscription” ".

AnyCancellable The “subscription” allows the caller to cancel the “subscription” at any time and no longer receive values ​​from the “publisher”, but moreover, as soon as the  AnyCancellable“subscription” leaves its scope, the memory occupied by the “publisher” is freed. Therefore, as soon as it is init( ) completed, this “subscription” will be deleted by the system ARC, and not having time to assign the asynchronous information about the current weather received with a time delay  currentWeather. To save such a “subscription”, it is necessary to create an OUTSIDE init()variable var cancellableSetthat will keep our AnyCancellable“subscription” in this variable throughout the entire “life cycle” of the class instance  TempViewMode

The AnyCancellable“subscription” in the variable is stored cancellableSetusing the operator  store ( in: &self.cancellableSet):



As a result, the “subscription” will be preserved throughout the entire “life cycle” of the class instance  TempViewModel. We can change the value of the publisher as desired $city, and the current weather currentWeather for this city will always be at our disposal .

In order to reduce the number of server calls when typing a city city, we should use not directly the “publisher” of the line with the name of the city  $city, but its modified version with the operators debounceand removeDuplicates:



The operator is  debounce used to wait until the user finishes typing the necessary information on the keyboard, and only then perform the resource-intensive task once.

Similarly, an operator removeDuplicateswill publish values ​​only if they differ from any previous values. For example, if the user first enters john, then joe, and then again john, we will receive johnonly once. This helps make ours UImore efficient.

Creating a UI with SwiftUI


Now that we have it View Model, let's get started UI. First at SwiftUI, and then at UIKit.

In we Xcodecreate a new project with SwiftUIand in the resulting structure we ContentView  place ours  View Modelas a @ObservedObject variable model. Replace  Text ("Hello, World!") with the title  Text ("WeatherApp"), add a text box to enter the city  TextField ("City", text: self.$model.city) and a label to display the temperature:



We directly used the values ​​of our variable model: TempViewModel(). We used in the text box to enter the city $model.city, and in the label to display the temperature - model.currentWeather.main?.temp.

Now, any changes to the  @Published properties will lead to "redrawing" View:



This is ensured by the fact that ours View Model is@ObservedObject, that is, AUTOMATIC “binding” ( binding) @Publishedof our properties View Modeland user interface elements ( UI) is carried out . Such AUTOMATIC “binding” is possible only in SwiftUI.

Creating a UI with UIKit


What to do with this in UIKit? It’s not there  @ObservedObject. In  UIKit we will perform the "binding" ( binding) manually. There are many ways to do this “manual binding”:

  • Key-Value Observing or KVO: a mechanism for using  key pathsto monitor a property and receive notification that it has changed.
  • Functional reactive programming or FRP: use of a framework Combine.
  • Delegation: Using delegate methods to send a notification that a property value has changed.
  • Boxing: didSet { } , .

Given the title of the article, we will naturally work in the field Combine. In the UIKitapplication, we will show how easy it is to make “manual binding” with Combine.

In the UIKitapplication, we will also have two UI elements: UITextFieldfor entering the city and UILabelfor displaying the temperature. In ViewControllerwe naturally will have Outletthese elements:





In the form of an ordinary variable viewModel, we have the same one as View Modelin the previous section:



Before doing the “manual binding” with Combine, let's make the text field UITextFieldour ally and the “publisher” of our content text:



This will allow us to very easily viewDidLoadimplement “manual binding” using the functionbinding ():



Indeed, we “subscribe” to the “publisher” cityTextField.textPublisherusing a very simple “subscriber”  assign (to: \.city, on: viewModel)and assign the text typed by the user in the text box cityTextFieldto our “input”  @Publishedproperty of cityours View Model.

In addition, we make changes in another direction: we “subscribe” to the “output”  @Publishedproperty  $currentWeather with the help of the “subscriber” sink and its closure receiveValue, form the temperature value and assign it to the label temperatureLabel.

Received in the  viewDidLoad â€śsubscription” is stored in a variable var cancellableSet. Having created them once, we allow them to act throughout the entire “life cycle” of the class instance ViewControllerand together with the “subscription” in our View Modelimplement all the business logic of the application.

By the way, the protocol ObservableObjectdoes not work with UIKit, but it does not interfere. UIKit completely indifferent to the protocol ObservableObjectand, in principle, it could be removed  View Modelin UIKit applications:



But we will not do this, because we want to keep it unchanged View Modelfor both the current application on UIKitand possibly future applications on SwiftUI.

That's all. The code is on Github .

Conclusion

A functional reactive framework Combineallows you to very simply and concisely implement the MVVMarchitecture both SwiftUIin UIKitand in the form of understandable and readable code.

Links:

Combine + UIKit + MVVM
Using Combine
iOS MVVM Tutorial: Refactoring from MVC
MVVM with Combine Tutorial for iOS

PS If you want to see some weather information, you need to register on OpenWeatherMap  and get it API key. This process will take you no more than 2 minutes.

All Articles