MVVM Binding in UIKit project

Mike Haidan
4 min readApr 25, 2023

--

As I see for the last few years MVVM it’s one of the most popular design patterns and MVVM commonly used by software engineers.

In the SwiftUI app, mostly Published, properties are used to implement Bindings, but what about UIKit?
The most popular answer is the Delegate pattern and/or Closures.
It may work in most cases, but it has one huge disadvantage. It’s a one-to-one relationship.
For example with closures. If you have 2 UI elements, which use the same View Model, and if both of them have to be subscribed to the same closure, only one UI element can listen that event. Let’s see in an example:

The example below is very simple, but it shows the problem.

In this example we have a View Model, VM and two 2 ui elements/classes, MainUI and SubUIElement.

  1. Emulates an API call to the server.
  2. Emulate some UI changes.
  3. Emulate some UI changes.
  4. Main render/build function.

Let’s go to render function:

5. render function creates a new MainUI object.

6. fetchData call is made and “Update UI in MainUI: [“a”, “b”, “c”]” is printed.

7. MainUI object creates an UI element SubUIElement.

8. Another fetchData call is made, but only `Update UI in SubUIElement: [“a”, “b”, “c”]` is printed. And here is a problem. Closures nor Delegate pattern doesn’t give possibilities to have one-to-many connection.
As an alternative, we may use the Observer pattern. But, as ViewModel grows, it’s really hard to maintain and, from my experience, bugs from using NSNotificationCenter, really hard to detect, reproduce and fix.

So what can we do with it? Combine Framework can help us, particularly PassthroughSubject but with a few tweeks. This Subject is really easy to use and doesn’t require deep Combine knowledge.
So, what does this Subject do? It simply “sends” an event and subscribers “listen” to it. Kind of similar logic as with closures.

Let’s replace onChange variable with PassthroughSubject:

  1. To use PassthroughSubject, we need to import Combine of course.
  2. We replaced onChanged with onChangedSubject. “Never”, second parameter in subject, means that the subject should not send any errors.
  3. Both, in MainUI and SubUIElement classes, should use an AnyCancellable variable to store subscriptions (or AnyCancellable Set, as in gist above. Both AnyCancellable variable and Set work nice, but I prefer to use Set :)).

In both classes we call “sink” closure, to receive value and store subscription in the cancelable Set, by calling cancelable.insert(…).

After buildUIElement and fetchData are called, we see both messages:
Update UI in MainUI…” and “Update UI in SubUIElement…”

As PassthroughSubject will be used more commonly, what we can improve is to get rid of the error (“Never”) argument.
It can be done by wrapping PassthroughSubject into a custom Subject.

What is done is, that a new class is created, by conforming Subject protocol. In that class Failure was marked as Never and used PassthroughSubject under the hood. New PassSubject will work the same as PassthroughSubject, but now the second, error argument, is not needed.

Now, View Model looks like this:

Now, only the type that will be sent is required. All other code remains the same.
But there is one more problem. If you check the code above (line 2 and 3) there are 2 variables. One is to store an Array of String, and another is used to notify UI about changes. And the issue is, that, by mistake, in the function fetchData it’s easy to change the order of calls. Firstly we call onChangedSubject and then save data in arrayOfData, which is incorrect, because onChangedSubject will return an empty array. This bug will be noticed only during runtime.
Is this possible to improve? Actually, yes!
To fix this another subject is needed, which is called CurrentValueSubject. The difference is, that this subject not only “sends” events, but also stores value.

Let’s replace PassSubject with aCurrentValueSubject.

  1. onChangedSubject was removed and Array of Sting was replaced by a CurrentValueSubject.
  2. Instead of using “arrayOfData”, now need to call “arrayOfData.value” to save a new value (and to access the value, stored in the CurrentValueSubject, also need to use the value property).
  3. Similarly, need to call “arrayOfData.sink” to receive new values. But one minor update, I used “dropFirst” because every time when you subscribe to CurrentValueSubject for the first time, it sends a value stored in the subject. To avoid this behavior, use “dropFirst” before “sink”.
    And the last thing is to get rid of the error (“Never”) argument. Similarly as with PassthroughSubject, a new Subject class is required:

And finally, ViewModel after replacing CurrentValueSubject with ValueSubject:

And it’s finished. We moved away from Closures to Subjects with not so signigicat changes. This approach has a lot of benefits: Easy to implement, easy to understand, doesn’t require deep Combine knowledge and allows you to cover your code using unit tests.

To summarize, when to use ValueSubject and when to use PassSubject:

  • PassSubject — it’s a good candidate when you are interested in one time event and not interested in storing any data (e.g. need to present an error/message alert from api call).
  • ValueSubject — when you are interested not only in an event, but also in value (e.d. “dataSource” for “UITableView”, when dataSource is populated from an API).

That’s all. Hope this article will help you to build robust and flexible MVVM apps.
Thank you for reading ❤️.

--

--