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{returnNSURL(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]? {returnnil} 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 inif 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.
Need MVP development, iOS and Android apps or prototyping? Check out our portfolio and make an order today!