How to update view in Swift MVVM?

Tonny
6 min readMar 18, 2021

--

There are two essential ideas in a well-designed MVVM: unidirectional data flow and weak reference to the view.

The flow of data in MVVM is unidirectional, the data was aroused from user or system event, and then it flows to viewModel, model, and view at last. You can implement the unidirectional by getting rid of having a controller reference in ViewModel. But how to update view without strong reference to view? In Android development, Jetpack provides LiveData to view owners(activity or fragment) for observation. In iOS development, without any official recommendation from Apple, there is a variety of ways in the community, it’s really up to the team or developer’s preference.

I totally agree with the idea: A reactive MVVM is a real MVVM. But there isn’t only one way to enjoy programming, especially when switching between pattern and anti-pattern unconsciously. In this story, let’s walk through a couple of different ways to update view, from stone age weak view, classical delegate, to future-oriented combine framework.

Before we create our ViewModel, let’s mock a User resource that can be fetched asynchronously. After users were fetched in ViewModel, the tableView will be updated.

public protocol Resource {
init()
static func fetch(completion: @escaping (Self) -> Void)
}
public extension Resource {
static func fetch(completion: @escaping (Self) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
completion(Self())
}
}
}
public struct User: Resource {
public init() {}
}
extension Array: Resource where Element: Resource {}

1. Weak View

Having the viewModel to hold a weak view is straightforward and easy to understand. If you have been used to use scalable but complicated architecture, you may dislike it, but it’s always beginner-friendly and time-tested. It represents the essential idea of MVVM: don’t hold view reference strongly when update view.

//ViewController.swift
class ViewController: UITableViewController {

let viewModel = ViewModel<[User]>()

func setup() {
viewModel.tableView = tableView
viewModel.fetch()
}
}
//ViewModel.swift
class ViewModel<R: Resource> {

var state: R?

weak var tableView: UITableView?

func fetch() {
R.fetch { [weak self] data in
print("fetched", data)

self?.state = data
self?.tableView?.reloadData()
}
}
}
let vc = ViewController()
vc.setup()

2. Weak Delegate

Delegate, an old friend from Objective-C, is like a carrier pigeon between components. It’s one of few programing features inherited by Swift, as protocol. It’s powerful enough to build any size of the project, from small to big.

//ViewController.swift
class ViewController: UITableViewController, Delegate {

let viewModel = ViewModel<[User]>()

func setup() {
viewModel.delegate = self
viewModel.fetch()
}

func didFetched() {
print("fetched", viewModel.state)

tableView.reloadData()
}
}
//ViewModel.swift
protocol Delegate: AnyObject {
func didFetched()
}
class ViewModel<R: Resource> {

weak var delegate: Delegate?

var state: R?

func fetch() {
R.fetch { [weak self] data in
self?.state = data
self?.delegate?.didFetched()
}
}
}
let vc = ViewController()
vc.setup()

3. didSet with Closure

Closure is often compared with delegate by misunderstood, but closure is reference type same as delegate, and with the help of weak capture list, it can stop strong reference too, so, from these two points, you can see closure as an anonymous delegate.

didSet is a strong observe mechanism, there is no easy way to unsubscribe the observe as Reactive does. But you can set viewModel.bind = nil to unsubscribe.

//ViewController.swift
class ViewController: UITableViewController {

let viewModel = ViewModel<[User]>()

func setup() {
viewModel.bind = { [unowned self] data in
print("notified", data)

self.tableView.reloadData()
}

viewModel.fetch()
}
}
//ViewModel.swift
class ViewModel<R: Resource> {

private var state: R? {
didSet {
bind?(state)
}
}

var bind: ((R?) -> Void)?

func fetch() {
R.fetch { [weak self] data in
self?.state = data
}
}
}
let vc = ViewController()
vc.setup()

4. Closure box

Wrapping value and closure into a box improves code readability. The wrapped closure gives the wrapped value power of behaviour. If the value changes, the behaviour will always be triggered accordingly.

//ViewController.swift
class ViewController: UITableViewController {

let viewModel = ViewModel<[User]>()

func setup() {
viewModel.bind { [unowned self] data in
print("notified", data)

self.tableView.reloadData()
}

viewModel.fetch()
}
}
//ViewModel.swift
class ViewModel<R: Resource> {

let state = Box<R>()

func bind(_ listener: @escaping (R?) -> ()) {
state.listener = listener
}

func fetch() {
R.fetch { [weak self] data in
self?.state.value = data
}
}
}
//Box.swift
class Box<T> {
var value: T? {
didSet {
listener?(value)
}
}

var listener: ((T?) -> ())?

init(_ v: T? = nil) {
value = v
}
}
let vc = ViewController()
vc.setup()

5. KVO

didSet + closure is a strong observation, while KVO provides an official way to remove the observer. KVO takes full advantage of Objective-C message dispatching capability, but it’s not convenient if you want to stick with pure Swift.

//ViewController.swift
class ViewController: UITableViewController {

let viewModel = ViewModel()

func setup() {
viewModel.addObserver(
self,
forKeyPath: #keyPath(ViewModel.state),
options: [.new],
context: nil
)

viewModel.fetch()
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

if keyPath == #keyPath(ViewModel.state) {
let state = change?[.newKey] as? [Admin]
print("notified", state)

tableView.reloadData()
}
}
}
//ViewModel.swift
class ViewModel: NSObject {

@objc dynamic var state: [Admin]?

func fetch() {
[Admin].fetch { [weak self] data in
self?.state = data
}
}
}
final class Admin: NSObject, Resource {
required override init() {}
}
let vc = ViewController()
vc.setup()

6. KeyPath

Swift KeyPath, different from the old keypath, is a pure swift entity with strong type information, it reduces bug from string keypath, it’s not getter/setter, it’s getter/setter’s description. KeyPath observation is preferable to Key-Value observation.

//ViewController.swift
class ViewController: UITableViewController {

@objc let viewModel = ViewModel()

var observer: NSKeyValueObservation?

func setup() {
observer = observe(\.viewModel.state, options: [.new]) { [unowned self] (_, change) in
print("notified", change.newValue)

self.tableView.reloadData()
}

viewModel.fetch()
}
}
//ViewModel.swift
class ViewModel: NSObject {

@objc dynamic var state: [Admin]?

func fetch() {
[Admin].fetch { [weak self] data in
self?.state = data
}
}
}
final class Admin: NSObject, Resource {
required override init() {}
}
let vc = ViewController()
vc.setup()

7. RxSwift

RxSwift introduces reactive programming to iOS development first time, it not only provides disposing of subscriptions but also provides abundant stream operators, like merge, debounce, etc. If you see the weak view, weak delegate, and closure are light-weight ways in view updating of MVVM, RxSwift is absolutely heavy-weight. It is very flexible for subject observation and stream manipulation, but it’s so invasive that you have to rebuild the whole architecture in a reactive way.

import RxSwift//ViewController.swift
class ViewController: UITableViewController {

let viewModel = ViewModel<[User]>()

let disposeBag = DisposeBag()

func setup() {
viewModel
.state
.subscribe({ [unowned self] newValue in
print("notified", newValue)

self.tableView.reloadData()
})
.disposed(by: disposeBag)

viewModel.fetch()
}
}
//ViewModel.swift
class ViewModel<R: Resource> {

let state = PublishSubject<R>()

func fetch() {
R.fetch { [weak self] data in
self?.state.onNext(data)
}
}
}
let vc = ViewController()
vc.setup()

8. Combine framework

If you are ready to experience reactive, consider Combine(iOS 13+), rather than RxSwift. Asynchronous events programming officially comes to iOS platform when Combine framework released. From the code above using RxSwift and below using Combine, you may find the coding styles are very similar. Apple learns from rx community and has some adjustment and simplification in Combine.

import Combine//ViewController.swift
class ViewController: UITableViewController {

let viewModel = ViewModel<[User]>()

var cancellables = Set<AnyCancellable>()

func setup() {
viewModel
.$state
.sink { [weak self] data in
print("notified", data)

self?.tableView.reloadData()
}
.store(in: &cancellables)

viewModel.fetch()
}
}
//ViewModel.swift
class ViewModel<R: Resource> {

@Published var state: R?

func fetch() {
R.fetch { [weak self] data in
self?.state = data
}
}
}
let vc = ViewController()
vc.setup()

Each way of above listed has its own characteristic, they appear, become popular, and discarded as iOS system and programming language evolving year after year, it’s pointless to tell one is better than the other. It’s really up to your preference and your project size. Probably this is what we said it’s fun to code.

If you enjoyed my opinion, please give it a clap (50) and share to help others find it!

--

--

Tonny
Tonny

Written by Tonny

Mobile developer@NZ. iOS, Android, React

Responses (1)