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.
- Delegate Pattern
- Observables
- Completion Handlers
- 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!