Data Binding Techniques in MVVM (Swift, UIKit)

Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, tvOS, and iPad OS that Apple released to the public in 2014. Since then, Apple and the open-source community have continued contributing to it. At the moment, Swift is the official programming language for Apple devices replacing Objective-C.

Through these years, many programmers have devised different design patterns to architect their apps. One of those design patterns is MVVM which stands for Model View View-Model. MVC, MVP, and VIPER are other design patterns that we will not cover in this post.

What is Data Binding?

Data Binding is how the app’s view and data are bound. In MVVM, we achieve this by binding the View Model (the data) and the View Controller (the view). Though we always connect the view model and the view in MVVM, there are several different techniques on how one can do this.

In this blog post, I will be going over 4 different techniques to bind the view model and the view controller in both ways.

  1. Delegate Pattern
  2. Observables
  3. Completion Handlers
  4. Function Returns

Before I start explaining them, let me give you the background for the code we will see. We are building a film repository where we will be displaying a list of film details on the home screen. The following is the model of the video data.

struct VideoData {
    let id: String
    let description: String
}

1. Delegate Pattern

The Delegate Pattern is a widely used design pattern in Swift. It is, in fact supported and used by Apple in many places in UIKit.

Delegation is a design pattern that enables a class or structure to hand off (or delegate) some of its responsibilities to an instance of another type. This design pattern is implemented by defining a protocol that encapsulates the delegated responsibilities, such that a conforming type (known as a delegate) is guaranteed to provide the functionality that has been delegated. Delegation can be used to respond to a particular action, or to retrieve data from an external source without needing to know the underlying type of that source.”

Swift Language Guide

In the words of a blogger John Sundell:

The core purpose of the delegate pattern is to allow an object to communicate back to its owner in a decoupled way. By not requiring an object to know the concrete type of its owner, we can write code that is much easier to reuse and maintain.

John Sundell’s article on “Delegation in Swift”

The following is an example of how the delegate pattern could be used in our arbitrary films repository app:

// HomeViewController.swift

class HomeViewController: UIViewController {
    // MARK: - Properties

    let homeViewModel: HomeViewModel

    init(homeViewModel: HomeViewModel) {
        self.homeViewModel = homeViewModel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        homeViewModel.delegate = self
        homeViewModel.viewLoaded()
    }
}

extension HomeViewController: HomeViewModelDelegate {
    func fetchedRecommendedVideos(videos: Result<[VideoData], Error>) {
        // Do something with the videos
    }
}

// HomeViewModel.swift

protocol HomeViewModelDelegate: AnyObject {
    func fetchedRecommendedVideos(videos: Result<[VideoData], Error>)
}

class HomeViewModel {
    weak var delegate: HomeViewModelDelegate?
    
    var videos: [VideoData]?
    
    func viewLoaded() {
        // Write logic to fetch videos from backend or local database here
        let videos: Result<[VideoData], Error> = .success([])
        
        delegate?.fetchedRecommendedVideos(videos: videos)
    }
}

This is a reasonably simple design pattern where we have a delegate property in the view model to which the view controller assigns itself. So once the videos are fetched from the backend, we send them to the view controller by invoking the fetchedRecommendedVideos delegate method.

2. Observables

An Observable is an entity that publishes its current value whenever its value changes. Anyone accessing the observable can subscribe to these changes and make the necessary operations based on the updated value.

Observables come under the concept of reactive programming, which is native to web frameworks like React. Apple, too has a native reactive programming library called Combine that supports iOS 13+. If your app needs to support iOS 12, RxSwift is another third-party library popularly used for reactive programming.

Let me use Combine in the same films repository app for this example.

// HomeViewController.swift

import Combine

class HomeViewController: UIViewController {
    // MARK: - Properties

    let homeViewModel: HomeViewModel
    
    var cancellables = Set<AnyCancellable>()
    
    // MARK: - Lifecycle

    init(homeViewModel: HomeViewModel) {
        self.homeViewModel = homeViewModel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        homeViewModel.viewLoaded()
        homeViewModel.videos.sink { videos in
            // Do something with the videos
        }.store(in: &cancellables)
    }
}

// HomeViewModel.swift

import Combine

class HomeViewModel {
    var videos = CurrentValueSubject<[VideoData], Never>([])
    
    func viewLoaded() {
        // Write logic to fetch videos from backend or local database here
        let videosResult: Result<[VideoData], Error> = .success([])
        
        switch videosResult {
        case let .success(videos):
            self.videos.send(videos)
        default:
            break
        }
    }
}

Here instead of getting the videos from a delegate method, the view controller directly observes the view model’s videos property.

3. Completion Handlers

Completion Handlers is one of the easiest ways to receive a response from a functional call. A completion handler is a function that calls back when a task completes. For this reason, it is also called a callback function.

A closure is passed as an argument to a function. When this function completes performing the task, it executes the closure.

The following is an example of using a completion handler in the films repository app:

// HomeViewController.swift

class HomeViewController: UIViewController {
    // MARK: - Properties

    let homeViewModel: HomeViewModel
    
    var cancellables = Set<AnyCancellable>()
    
    // MARK: - Lifecycle

    init(homeViewModel: HomeViewModel) {
        self.homeViewModel = homeViewModel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        homeViewModel.loadVideos {
            // Do something with the videos
            print(self.homeViewModel.videos)
        }
    }
}

// HomeViewModel.swift

class HomeViewModel {
    var videos: [VideoData]?
    
    func loadVideos(completionHandler: @escaping () -> Void) {
        // Write logic to fetch videos from backend or local database here
        let videosResult: Result<[VideoData], Error> = .success([])
        
        switch videosResult {
        case let .success(videos):
            self.videos = videos
            completionHandler()
        default:
            break
        }
    }
}

Completion Handlers and Function Returns have a slightly different approach when compared to the first 2 techniques since, in this case, we are explicitly asking the view model for the data right when we make the function call. This is why we name the method to be invoked loadVideos instead of viewLoaded.

4. Function Returns

A classical and primitive technique to get a response from a function call is through its return type. This is easy to understand for types like Bool, String, or Int, where we append the function definition with -> Bool, -> String, -> Int. However, this might seem tricky when dealing with asynchronous operations inside the function.

One way to do this is to use Combine to return an AnyPublisher and sink it later in the view controller. Here is an example of doing in our very own films repository app:

// HomeViewController.swift

class HomeViewController: UIViewController {
    // MARK: - Properties

    let homeViewModel: HomeViewModel
    
    var cancellables = Set<AnyCancellable>()
    
    // MARK: - Lifecycle

    init(homeViewModel: HomeViewModel) {
        self.homeViewModel = homeViewModel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        homeViewModel.fetchVideos().sink { complete in
            switch complete {
            case let .failure(error):
                print(error)
            default:
                break
            }
        } receiveValue: { videos in
            // Do something with the videos
            print(videos)
        }.store(in: &cancellables)
    }
}

// HomeViewModel.swift

class HomeViewModel {
    var videos: [VideoData]?
    
    func fetchVideos() -> AnyPublisher<[VideoData], Never> {
        // Write logic to fetch videos from backend or local database here
        let request: AnyPublisher<[VideoData], Never> = Just<[VideoData]>([])
            .map { [weak self] videos -> [VideoData] in
                self?.videos = videos
                return videos
            }
            .eraseToAnyPublisher()
        
        return request
    }
}

Since Swift 5.5, we also have the new async/await method. We would use the keyword async to denote that a function needs to be run asynchronously and the keyword await before the asynchronous function call to indicate that the thread must wait for the operation to complete.

Finally, this is what it would look like in our films repository:

// HomeViewController.swift

class HomeViewController: UIViewController {
    // MARK: - Properties

    let homeViewModel: HomeViewModel
    
    // MARK: - Lifecycle

    init(homeViewModel: HomeViewModel) {
        self.homeViewModel = homeViewModel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        Task {
            await loadVideos()
        }
    }
    
    func loadVideos() async {
        let videos = await homeViewModel.fetchVideos()
        print(videos)
    }
}

// HomeViewModel.swift

class HomeViewModel {
    var videos: [VideoData]?
    
    func fetchVideos() async -> [VideoData]? {
        // Write logic to fetch videos from backend or local database here
        let videos = await videoRepository.fetchVideosFromBackend()
        
        return videos
    }
}

If I were honest, this last one looks way cleaner to me than the other techniques, but that could be just me. Also, one thing to remember is that this only works with iOS 15 and newer, so it might not be an option if you are supporting older iOS versions (which is highly likely).

I hope these techniques helped you get an idea of the different techniques that exist for data binding in MVVM, and I’m sure there are several other techniques I might have missed.

Please leave what you think is the best technique for data binding and why in the comments below.

Until then, see you all in the next post!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.