Beyond MVC: how to use MVVM in iOS

Datetime:2016-08-23 01:23:56          Topic: MVVM Model           Share

Today we’re going to talk about MVVM pattern. We will discuss its advantages in comparison to MVC and take a look at two implementation examples, a small and a big one. You will be able to use the latter in your work as an example of a good architecture for almost any MVVM project. So, let’s start with the basics :)

The basics

One of the most widespread iOS patterns beginners normally start with is MVC (Model-View-Controller). Until some point we all use it in our projects. But time passes and your Controller grows bigger and bigger overloading itself.

Let me remind you here that MVC Controller can talk both to Model and View. MVVM has a little bit different structure and you should memorize it: User → View → ViewController → ViewModel → Model. It means that the user sees a button (View), touches it and then ViewController takes it from there performing actions with the UI, for example changes the color of this button. Then ViewModel sends a data request to the server, adds data to the Model and performs some other actions with the Model.

The main takeaway: we have a new ViewModel class that talks to the Model which means Controller is no longer responsible for it. Now the Controller does what it is supposed to do: works with Views and doesn’t even know that the Model exists.

Practice

Libraries working with MVVM pattern include ReactiveCocoa, SwiftBond/Bond and ReactiveX/RxSwift. Today we’re going to talk about the last framework — RxSwift. If you want to learn more, read about the difference between RxSwift and ReactiveCocoa . In short, RxSwift is a more modern solution written in Swift while ReactiveCocoa has been around for a little longer with its core being written in Objective-C. ReactiveCocoa has a lot of fans and a lot of tutorials are available for it.

Bond is somewhat smaller and it’s a good choice for beginners but we’re pros here, aren’t we? So let’s leave it aside. RxSwift is a an extension of ReactiveX and it is getting more and more fans. But the choice is yours, of course.

Simple example

Let’s start with a simple example (it is indeed very basic, maybe I shouldn’t have shown it to you, oh well :) The task is easy: we use UIPageControl to show some images.

We need only two elements for implementing it: UICollectionView and UIPageControl . Oh, and when on the app launch you need to demonstrate your user a logic of your app, you can use it too.

And here’s our “masterpiece”:

And one more thing. For our images to be centered right during scrolling we use CollectionViewFlowLayoutCenterItem and associate it with UICollectionViewFlowLayoutCenterItem.swift class (you can find it in the project folder). Here’s a GitHub link .

Our Podfile:

target 'PageControl_project' do
use_frameworks!
pod 'RxCocoa'
pod 'RxSwift'
end

RxCocoa is an extesion for all UIKit elements. So we can write: UIButton().rx_tap and receive ControlEvent that belongs to ObservableType . Let’s say we have UISearchBar . Without RxSwift we would normally write our Controller as a delegate and watch changes of the text property. With RxSwift we can write something like this:

searchBar
   .rx_text
   .subscribeNext { (text) in
 	print(text)
   }

And the key point here. For our task we don’t sign up Controller as a delegate for UICollectionView . We do the following instead:

override func viewDidLoad() {
        super.viewDidLoad()
        setup()
    }
 
    func setup() {
 
        //initialize viewModel
        viewModel = ViewModel()
 
        viewModel.getData()
            //set pageCtr.numberOfPages
            //images should not be nil
            .filter { [unowned self] (images) -> Bool in
                self.pageCtrl.numberOfPages = images.count
                return images.count > 0
            }
 
            //bind to collectionView
            //set pageCtrl.currentPage to selected row
            .bindTo(collView.rx_itemsWithCellIdentifier("Cell", cellType: Cell.self)) { [unowned self] (row, element, cell) in
                cell.cellImageView.image = element
                self.pageCtrl.currentPage = row
            }
 
            //add to disposeableBag, when system will call deinit - we`ll get rid of this connection
            .addDisposableTo(disposeBag)
    }

As a result, we write less code, our code becomes more readable and if we don’t have data ( ViewModel.getData() returns Observable<[UIImage?]> ), nothing happens, we wouldn’t even start the whole process.

Let’s take a closer look at the method of ViewModel getData() class. If we weren’t receiving data from the server (we will look at it a bit later), I would have added method for receiving data but since we are, I use private dataSource with images that I simply added to the project.

func getData() -> Observable<[UIImage?]> {
        let obsDataSource = Observable.just(dataSource)
 
        return obsDataSource
    }
 

Here we create Observable object and use just method that tells: return sequence that contains only one element, UIImage elements array.

Note that ViewModel class is a structure. This way when using additional properties of this class we will have a ready initialization.

Complex example

I hope everything is clear with the first example. Now it’s time for the 2nd one. But first, some more tips.

When working with sequences, in the end of each call you need to add addDisposableTo(disposeBag) to the object.

let disposeBag = DisposeBag() — in example it is declared as property. Thanks to it when the system calls deinit resources are freed for Observable objects.

Next, in this project we will be using Moya. It is an abstract class above, for example, Alamofire that, in its turn, is an abstract class above NSURLSession and so on. Why do we need it? For even more abstraction and for our code to look professional and have no slightly different methods that are identical in practice.

Moya has an extension written for RxSwift. It is called Moya/RxSwift (yepp, pretty straightforward, isn’t it?).

Let’s start with Podfile:

platform :ios, '8.0'
use_frameworks!
 
target 'RxMoyaExample' do
 
pod 'Moya/RxSwift'
pod 'Moya-ModelMapper/RxSwift'
pod 'RxCocoa'
pod 'RxOptional'
 
end

To be able to work with Moya we need to create enum and put it under control of the TargetType protocol. In ReactX project folder this file is called GithubEndpoint.swift . We will be using api for github. We will only have four endpoints but you can add as many as you need in your own project.

enum GitHub {
    case UserProfile(username: String)
    case Repos(username: String)
    case Repo(fullName: String)
    case Issues(repositoryFullName: String)
}
 
private extension String {
    var URLEscapedString: String {
        return stringByAddingPercentEncodingWithAllowedCharacters(
            NSCharacterSet.URLHostAllowedCharacterSet())!
    }
}

We will need private extension for String later. Now let’s turn GithubEndpoint into a subordinate of the TargetType protocol:

extension GitHub: TargetType {
    var baseURL: NSURL { return NSURL(string: "https://api.github.com")! }
 
    var path: String {
        switch self {
        case .Repos(let name): return "/users/\(name.URLEscapedString)/repos"
        case .UserProfile(let name): return "/users/\(name.URLEscapedString)"
        case .Repo(let name): return "/repos/\(name)"
        case .Issues(let repositoryName): return "/repos/\(repositoryName)/issues"
        }
    }
 
    var method: Moya.Method {
        return .GET
    }
 
    var parameters: [String:AnyObject]? {
        return nil
    }
 
    var sampleData: NSData {
        switch self {
        case .Repos(_): return "{{\"id\": \"1\", \"language\": \"Swift\", \"url\": \"https://api.github.com/repos/mjacko/Router\", \"name\": \"Router\"}}}".dataUsingEncoding(NSUTF8StringEncoding)!
 
        case .UserProfile(let name): return "{\"login\": \"\(name)\", \"id\": 100}".dataUsingEncoding(NSUTF8StringEncoding)!
 
        case .Repo(_): return "{\"id\": \"1\", \"language\": \"Swift\", \"url\": \"https://api.github.com/repos/mjacko/Router\", \"name\": \"Router\"}".dataUsingEncoding(NSUTF8StringEncoding)!
 
        case .Issues(_): return "{\"id\": 132942471, \"number\": 405, \"title\": \"Updates example with fix to String extension by changing to Optional\", \"body\": \"Fix it pls.\"}".dataUsingEncoding(NSUTF8StringEncoding)!
        }
    }
}

If you’re using methods other than GET , you can use switch.parameters — since we aren’t transferring anything, we simply return nil . Using switch you can transfer additional information your server needs. sampleData — since Moya works with texts, this variable is a must.

Let’s start with our example. Here’s the storyboard:

Binding elements with our Controller:

@IBOutlet weak var searchBar: UISearchBar!
@IBOutlet weak var tableView: UITableView!

Adding several properties in our ViewController:

var provider: RxMoyaProvider<GitHub>!
var latestRepositoryName: Observable<String> {
        return searchBar
            .rx_text
            .filter { $0.characters.count > 2 }
            .throttle(0.5, scheduler: MainScheduler.instance)
            .distinctUntilChanged()
    }

provider — it’s a Moya object with our enum type.

latestRepositoryName — Observable<String>. Each time user starts writing something in the searchBar we watch the changes by subscribing to them. rx_text is from RxCocoa, category for UIKit elements we imported. You can take a look at other properties yourself.

Then we filter text and use only the one that has over 2 symbols.

throttle — a very useful property. If a user is typing too fast, we create a small timeout to prevent “bothering” the server.

distinctUntilChanged — checks the previous text and if the text was changed, it lets it go further.

Creating a model:

import Mapper
 
struct Repository: Mappable {
    let identifier: Int
    let language: String
    let name: String
    let fullName: String
 
    init(map: Mapper) throws {
        try identifier = map.from("id")
        try language = map.from("language")
        try name = map.from("name")
        try fullName = map.from("full_name")
    }
}
 
struct Issue: Mappable {
    let identifier: Int
    let number: Int
    let title: String
    let body: String
 
    init(map: Mapper) throws {
        try identifier = map.from("id")
        try number = map.from("number")
        try title = map.from("title")
        try body = map.from("body")
    }
}

And now we create ViewModel(IssueTrackerModel) :

import Foundation
import Moya
import Mapper
import Moya_ModelMapper
import RxOptional
import RxSwift
 
struct IssueTrackerModel {
    let provider: RxMoyaProvider<GitHub>
    let repositoryName: Observable<String>
 
    func trackIssues() -> Observable<[Issue]> {
        return repositoryName
            .observeOn(MainScheduler.instance)
            .flatMapLatest { name -> Observable<Repository?> in
 
                return self.findRepository(name)
            }
            .flatMapLatest { repository -> Observable<[Issue]?> in
                guard let repository = repository else { return Observable.just(nil) }
 
                return self.findIssues(repository)
            }
            .replaceNilWith([])
    }
 
    private func findIssues(repository: Repository) -> Observable<[Issue]?> {
        return self.provider
        .request(GitHub.Issues(repositoryFullName: repository.fullName))
        .debug()
        .mapArrayOptional(Issue.self)
    }
 
    private func findRepository(name: String) -> Observable<Repository?> {
        return self.provider
        .request(GitHub.Repo(fullName: name))
        .debug()
        .mapObjectOptional(Repository.self)
    }
}

Firstly, we have created two methods: findRepository and findIssues . The first one will be returning optional Repository object, the second — Observable[Issue] according to the same logic.

mapObjectOptional() method will return optional object in case nothing is found and mapArrayOptional() will return optional array . debug() — will display debug info in the console.

Next, trackIssues() joins these two methods. flatMapLatest() is an important element here creating one sequence after another. Its fundamental difference from flatMap() is that when flatMap() receives a value, it starts a long task and when receiving the next value it is completing the previous operation. And it’s not what we need since the user might start entering another text. We need to cancel the previous operation and start a new one — flatMapLatest() will help us here.

Observable.just(nil) — simply returns nil that will be further replaced with an empty array with the next method. replaceNilWith([]) — replaces nil in an empty array.

Now we need to bind this data with UITableView . Remember that we don’t need to subscribe to UITableViewDataSource , RxSwift has rx_itemsWithCellFactory method for it. Here’s a setup() method within viewDidLoad() :

func setup() {
 
        provider = RxMoyaProvider<GitHub>()
 
        issueTrackerModel = IssueTrackerModel(provider: provider, repositoryName: latestRepositoryName)
 
        issueTrackerModel
            .trackIssues()
            .bindTo(tableView.rx_itemsWithCellFactory) { (tableView, row, item) in
                print(item)
                let cell = tableView.dequeueReusableCellWithIdentifier("Cell")
                cell!.textLabel?.text = item.title
 
                return cell!
            }
            .addDisposableTo(disposeBag)
 
        //if tableView item is selected - deselect it)
        tableView
            .rx_itemSelected
            .subscribeNext { indexPath in
                if self.searchBar.isFirstResponder() == true {
                    self.view.endEditing(true)
                }
            }.addDisposableTo(disposeBag)
    }

Another thing to remember is in which stream operations will be carried. We have two methods: subscribeOn() and observeOn() . What’s the difference between them? subscribeOn() indicates in which stream the chain of events will be started while observeOn() — where the next one should be started (see the image below).

Here’s an example:

.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
 
.subscribeOn(MainScheduler.instance)

So, looking through all the code you can see that with MVVM we need much less lines of code for writing this app than with MVC pattern. But when you start using MVVM, it doesn’t mean that it should go everywhere — pattern choice should be logical and consistent. Don’t forget that your top priority is writing good code that is easy to change and understand.

Code from this article is available on GitHub .





About List