RxSwift usage with MVVM pattern.

Kanan Abilzada
6 min readDec 16, 2021

How can we implement Rxswift with the MVVM pattern in swift?

Requirement: RxSwift basics

In this article, we will learn how we can use RxSwift in the MVVM pattern.

What will we build?

I’ll create an application that fetches images from API and later shows them in collectionView. I uploaded resources to GitHub you can get the link end of the page. Let’s get started.

Install RxSwift with CocoaPods

As a first step, we must install RxSwift with Cocoapods. I’ll pass explanation of this, hopefully, you already know that :)

I created a new project and set up all UI for us and you can see our project struct below:

Setup Model

I’ll use this API to that give me random photos’ URLs. As a first step, I’ll create a model file for this. Let’s open the PhotoListModel file and paste this code:

struct PhotoListModel: Codable, Identifiable {
let id: String?
let updated_at: String?
let urls: PhotoListItemModel?
}
struct PhotoListItemModel: Codable {
var id: String?
let small: String?
let raw: String?
let regular: String?
}

Setup ViewModel

Okay, our model file is ready. Let’s open PhotosViewModelActions file. This will be protocol and keep the functions that we’ll use in PhotosViewModel. Okay, copy this code and paste it:

import RxSwiftprotocol PhotoListViewModelActions: AnyObject {
func fetchImages(
currentPage: Int
)
var photoList : BehaviorRelay<[PhotoListModel]> { get }
var imageDownloaded : PublishRelay<(Int, UIImage?)> { get }
}

Now create our ViewModel file and call it a PhotoListViewModel and add this code inside it:

class PhotoListViewModel: PhotoListViewModelActions {
let imageLoaderService = StringImageLoader()
// MARK: - Variables
private
var currentPage = BehaviorRelay(value: 1)
private let photoService = PhotoService.shared
private let disposeBag = DisposeBag()
var photoList = BehaviorRelay<[PhotoListModel]>(value: [])
var imageDownloaded = PublishRelay<(Int, UIImage?)>()
init() {
self.fetchImages(currentPage: self.currentPage.value)
}
}

In here:
imageLoaderService — represents our image loader class
currentPage — keeps current page’s count
photoService — Gets all images from given API URL (if you download project file you can see it)
disposeBag — disposes all observable variables

When PhotoListModel is initialized fetchImages function will be called (you can call this function when you want) Let’s create it.

extension PhotoListViewModel {
/// Loading images from given api
func fetchImages(currentPage: Int) {
self.photoService.getPhotos(currentPage: "\(currentPage)") { [weak self] response in
switch response {
case .failure(let e):
print("error", e.localizedDescription)
self?.photoList.accept([])
case .success(let data):
self?.photoList.accept(data)
}
}
}
}

Setup Network

Create PhotoServiceActions protocol that it keep service functions:

protocol PhotoServiceActions: AnyObject {
func getPhotos(currentPage: String,
completion: @escaping (PhotoGetPhotosResponseType) -> Void)
}

It’s ready, now create PhotoService class and confirm it with PhotoServiceActions :

class PhotoService: PhotoServiceActions, Request {    private let dataLoader: DataLoader    // MARK: Request parameters
var
host: String
var path: String
var queryItems: [URLQueryItem]
var headers: [String : String]
static let shared = PhotoService()
// MARK: - Initialization
init
() {
dataLoader = DataLoader()
self.host = ApiURL.baseURL
self.path = ""
self.queryItems = []
self.headers = [:]
setupHeaders()
}
// MARK: Request header setup
private
func setupHeaders () {
self.headers = ["Authorization": "Client-ID \(ApiURL.apiKey)"]
}
}
extension PhotoService {
/// gets images from api
/// - Parameter completion: completion handler for function
/// - Parameter currentPage: represent current page's count
func getPhotos(currentPage: String,
completion: @escaping (PhotoGetPhotosResponseType) -> Void) {

self.path = "/photos"
self.queryItems = []
let perPage = URLQueryItem(name: "per_page", value: "30")
let currentPage = URLQueryItem(name: "page", value: currentPage)
self.queryItems = [perPage, currentPage]
dataLoader.request(self, decodable: [PhotoListModel].self) { response in
completion(response)
}
}
}

I’ll pass epxlanation of DataLoader class you can see it inside project.

Setup View

In now when photoList variable changes (in viewModel) we will listen to it here. In now open view class and. setup it

Add this variable to the head of code:

private var viewModel: PhotoListViewModel!
private let disposeBag = DisposeBag()
private var cachedImages: [Int: UIImage] = [:]

viewModelrepresents our PhotoListViewModel class
disposeBag — disposes all observable variables
cachedImagesit will keep our downloaded images

Okay, let’s add CollectionView’s data source and delegate methods with RxSwift.

As a first step, we will bind photoList to the collectionView that we created in the PhotosViewModel file.

private func bindCollectionView() {
// MARK: Bind photoList to collectionView
viewModel.photoList
.filter({ !$0.isEmpty })
.bind(to: collectionView.rx
.items(cellIdentifier: PhotoCollectionViewCell.identifier,
cellType: PhotoCollectionViewCell.self)) { _, _, _ in}
.disposed(by: disposeBag)
}

Run the application you will see that we got data successfully:

As you see we get images’ URLs, I’ll download the images in the willDisplayCell method. Without forgetting we also must create an array that will keep our downloaded images so as not to reload them again. Add this line of codes inside it:

collectionView.rx.willDisplayCell
.observeOn(MainScheduler.instance)
.map({ ($0.cell as! PhotoCollectionViewCell, $0.at.item) })
.subscribe { [weak self] cell, indexPath in
guard
let self = self else {return}
if let cachedImage = self.cachedImages[indexPath] {
/// use image from cached images
cell.imageView.image = cachedImage
} else {
/// start animation for download image
cell.imageView.image = nil
cell.activityIndicator.startAnimating()
/// download image
self.viewModel.loadImageFromGivenItem(with: indexPath)
}
}
.disposed(by: disposeBag)

Let’s create loadImageFromGivenItem function inside PhotoListViewModel class:

var imageDownloaded = PublishRelay<(Int, UIImage?)>()/// loading image from given stringfunc loadImageFromGivenItem(with index: Int) {
let givenElementString = photoList.value[index].urls?.regular ?? ""
imageLoaderService.loadRemoteImageFrom(urlString: givenElementString) { [weak self] image in
print("image downloaded: \(index): ", image?.description ?? "")
self?.imageDownloaded.accept((index, image))
}
}

It’s ready. Handle it inside View:

/// bind loaded image to cell
private func bindImageLoader() {
viewModel.imageDownloaded
.observeOn(MainScheduler.instance)
.filter({ $0.1 != nil })
.map({ ($0.0, $0.1!) })
.subscribe(onNext: { [unowned self] index, image in
guard let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? PhotoCollectionViewCell
else {
return
}
cell.animateCellWithImage(cell, image)
self.cachedImages[index] = image
})
.disposed(by: disposeBag)
}

I added animateCellWithImage function inside PhotoCollectionViewCell class:

/// show image when image downloaded successfully
func animateCellWithImage(_ cell: PhotoCollectionViewCell, _ image: UIImage) {
cell.activityIndicator.stopAnimating()
cell.transform = CGAffineTransform(rotationAngle: 60)

UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: .curveEaseInOut, animations: {
cell.transform = .identity
}, completion: nil)
cell.imageView.image = image
}

Well done :) Now we can run our application and see the result would be as below:

Okay, the last thing I want to change currentPage and load more photos when the scroll ended. Add this line of code inside PhotosViewController:

// MARK: Trigger scroll view when ended
collectionView.rx.willDisplayCell
.filter({
let currentSection = $0.at.section
let currentItem = $0.at.row
let allCellSection = self.collectionView.numberOfSections
let numberOfItem = self.collectionView.numberOfItems(inSection: currentSection)
return currentSection == allCellSection - 1
&&
currentItem >= numberOfItem - 1
})
.map({ _ in () })
.bind(to: viewModel.scrollEnded)
.disposed(by: disposeBag)

with this line of code will trigger when the last cell is displayed on the screen:

return currentSection == allCellSection - 1 && currentItem >= numberOfItem - 1

And I’ll trigger my new created variable that is called it scrollEnded in the ViewModel file and call its bind function when ViewModel is initialized. (Also, I added this variable to PhotoListViewModelActions protocol)

var scrollEnded = PublishRelay<Void>()func bindScrollEnded() {
scrollEnded
.subscribe { [weak self] _ in
if
let currentPage = self?.currentPage.value {
self?.currentPage.accept(currentPage + 1)
self?.fetchImages(currentPage: s elf?.currentPage.value ?? 0)
}
}
.disposed(by: disposeBag)
}

As a next step, you can stop some image download tasks, because when we scroll to the bottom, the top of collectionView’s images continues to download images still that is not true.

Thanks for reading. You also check out my other articles. Don’t forget to smile :)

--

--