Edit 18.01.2017: This post was updated to Swift 3.0, RxSwift 3.1 and Moya 8.0

As we dive more and more into the wild world of functional reactive programming, today we will talk about networking and connecting our data with UI. We will also make sure everything is (as always!) simple, smooth and nice (guaranteed)! To feel comfortable with pace of the tutorial, make sure you’ve checked out part #1 and #2 from our series!

For Rx there are many networking extensions, including RxAlamofire and Moya. In this tutorial we will focus on Moya, which I really like.

Moya



Moya is an abstract layer above all the networking stuff you would normally need to take care of by yourself. Basically, by using this library we will make our connection with API in no-time, and with extensions to it that consists of RxSwift and ModelMapper , we will have full package for our journey.

Setup

To setup Moya, we need a Provider , which consists of setup for stubbing, endpoint closure etc. (more of it when we will do testing). For our simple case we actually don’t need anything at all, so this point is just initializing the Provider with RxSwift. Then there is the second thing we need to do, which is the Endpoint configuration – an enum with our possible endpoint targets. It is really simple! We just need to create enum that conforms to TargetType and we are done. But what is this TargetType ? It is a protocol that has url, method, task(is a request/upload/download), parameters and parameterEncoding (so really basic stuff for URL requests), but there is also one more thing! The last parameter we need to specify is something called sampleData . Moya is really heavy relying on tests. It treats test stubs as first-class citizens. But more about testing in Moya and RxSwift in the next chapters. The only thing you need to know for now is that for every request we should specify sample response from the server.

Example

That’s right, straight to the example app! We skipped definitions because there isn’t much theory to tell before the example, but we will learn much step by step during the coding part. In the example we will try to get issues for specific repository using GitHub API. But to complicate things a little bit, first we will get the repository object, check if it exists, and then by chaining the requests we will get issues for that repository. And we will map everything from JSON to objects. And we will have to take care of errors, duplicating requests, spamming API and so on.

Don’t you worry, most of it we already covered in first part of the series! Here we will need to understand the chaining and error handling, plus how to connect the chained operation to table view. Not that hard, right? Right?

Never mind, let’s dive in. Our Issue Tracker at the end should looks like this one:

We type full repository name (with repository owner and slash), like in example: apple/swift , apple/cups , moya/moya and so on. When the repository is found (which is one URL request), we then search for issues of this repository (second URL request). That’s our main target, so let’s start coding!

First, we create a project and we install CocoaPods in it. We would need a few more pods this time. We will use RxSwift, Moya, RxCocoa, RxOptional and Moya’s extension for RxSwift and ModelMapper, to map objects, Moya-ModelMapper. Quite a lot! We can reduce the Podfile to 3 pods, which are:

platform :ios, '8.0' use_frameworks! target 'RxMoyaExample' do pod 'RxCocoa', '~> 3.0.0' pod 'Moya-ModelMapper/RxSwift', '~> 4.1.0' pod 'RxOptional' end post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['ENABLE_TESTABILITY'] = 'YES' config.build_settings['SWIFT_VERSION'] = '3.0' end end end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 platform : ios , '8.0' use_frameworks ! target 'RxMoyaExample' do pod 'RxCocoa' , '~> 3.0.0' pod 'Moya-ModelMapper/RxSwift' , '~> 4.1.0' pod 'RxOptional' end post_install do | installer | installer . pods_project . targets . each do | target | target . build_configurations . each do | config | config . build_settings [ 'ENABLE_TESTABILITY' ] = 'YES' config . build_settings [ 'SWIFT_VERSION' ] = '3.0' end end end

These are really helpful and you’ll see that our task is really simple with the help we’ve got from them.

Step 1 – Controller and Moya setup.

We will start with UI, which is just a UITableView and UISearchBar . Really simple one. You can follow up the layout in gif above, or create your own design – what you like the most!

Then we would need a controller to manage everything. We can try to describe role of our controller before we create the architecture.

So what should our controller really do? It should get the data from search bar, pass it to model, get issues from model and pass it to table view. Quite simple! Let’s start with our IssueListViewController . Create file IssueListViewController.swift , and prepare our controller with importing modules and basic configuration:

import Moya import Moya_ModelMapper import UIKit import RxCocoa import RxSwift class IssueListViewController: UIViewController { @IBOutlet weak var tableView: UITableView! @IBOutlet weak var searchBar: UISearchBar! override func viewDidLoad() { super.viewDidLoad() setupRx() } func setupRx() { } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import Moya import Moya_ModelMapper import UIKit import RxCocoa import RxSwift class IssueListViewController : UIViewController { @ IBOutlet weak var tableView : UITableView ! @ IBOutlet weak var searchBar : UISearchBar ! override func viewDidLoad ( ) { super . viewDidLoad ( ) setupRx ( ) } func setupRx ( ) { } }

As you can see I have prepared the setupRx() method already, because of course we will have to setup our bindings! But before that, let’s setup Moya’s Endpoint . Remember that I’ve told you that we need two steps: step one, which is Provider and step two, which is the Endpoint. Let’s start with the Endpoint.

We will create new file, let’s call it GithubEndpoint.swift and we will create an enum with few possible targets in it:

import Foundation import Moya enum GitHub { case userProfile(username: String) case repos(username: String) case repo(fullName: String) case issues(repositoryFullName: String) } 1 2 3 4 5 6 7 8 9 import Foundation import Moya enum GitHub { case userProfile ( username : String ) case repos ( username : String ) case repo ( fullName : String ) case issues ( repositoryFullName : String ) }

Alright, alright, but you’ve told us that it needs to conform to the TargetType and this one is just an enum. And you are right! We will make an extension to the GitHub enum, which will have all the needed properties. As I told you, we need 7 (7, not 6, because URL is a baseURL + path ). Besides baseURL and path and task , we also have method , which is just a request method like .get , .post , etc. There is also parameters and parametersEncoding , which should be self-explanatory, and sampleData , which we covered at the start of the tutorial.

So let’s implement it! In the same file, we will create an extension for GitHub to conform to TargetType :

import Foundation import Moya private extension String { var URLEscapedString: String { return self.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlHostAllowed)! } } enum GitHub { case userProfile(username: String) case repos(username: String) case repo(fullName: String) case issues(repositoryFullName: String) } extension GitHub: TargetType { var baseURL: URL { return URL(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: Any]? { return nil } var sampleData: Data { switch self { case .repos(_): return "{{\"id\": \"1\", \"language\": \"Swift\", \"url\": \"https://api.github.com/repos/mjacko/Router\", \"name\": \"Router\"}}}".data(using: .utf8)! case .userProfile(let name): return "{\"login\": \"\(name)\", \"id\": 100}".data(using: .utf8)! case .repo(_): return "{\"id\": \"1\", \"language\": \"Swift\", \"url\": \"https://api.github.com/repos/mjacko/Router\", \"name\": \"Router\"}".data(using: .utf8)! case .issues(_): return "{\"id\": 132942471, \"number\": 405, \"title\": \"Updates example with fix to String extension by changing to Optional\", \"body\": \"Fix it pls.\"}".data(using: .utf8)! } } var task: Task { return .request } var parameterEncoding: ParameterEncoding { return JSONEncoding.default } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import Foundation import Moya private extension String { var URLEscapedString : String { return self . addingPercentEncoding ( withAllowedCharacters : CharacterSet . urlHostAllowed ) ! } } enum GitHub { case userProfile ( username : String ) case repos ( username : String ) case repo ( fullName : String ) case issues ( repositoryFullName : String ) } extension GitHub : TargetType { var baseURL : URL { return URL ( 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 : Any ] ? { return nil } var sampleData : Data { switch self { case . repos ( _ ) : return "{{\"id\": \"1\", \"language\": \"Swift\", \"url\": \"https://api.github.com/repos/mjacko/Router\", \"name\": \"Router\"}}}" . data ( using : . utf8 ) ! case . userProfile ( let name ) : return "{\"login\": \" \ ( name ) \", \"id\": 100}" . data ( using : . utf8 ) ! case . repo ( _ ) : return "{\"id\": \"1\", \"language\": \"Swift\", \"url\": \"https://api.github.com/repos/mjacko/Router\", \"name\": \"Router\"}" . data ( using : . utf8 ) ! case . issues ( _ ) : return "{\"id\": 132942471, \"number\": 405, \"title\": \"Updates example with fix to String extension by changing to Optional\", \"body\": \"Fix it pls.\"}" . data ( using : . utf8 ) ! } } var task : Task { return . request } var parameterEncoding : ParameterEncoding { return JSONEncoding . default } }

Whole GithubEndpoint.swift should be fine now! It looks scary, but if you read it, it really isn’t! We won’t really need any parameters sent in here, so we return nil, method is always .get in our case, baseURL is also the same, just sampleData and path need to be put in a switch.

If you would want to add another target, you would need to check if this request needs .get or maybe .post method, maybe it needs parameters, then you would need to add there switches as well. What we’ve also added is the function URLEscapedString , which is really helpful with encoding characters in URL. Other than that, everything else should be clear. Back to the controller!

We now have to implement Moya’s Provider . We will also implement hiding keyboard on cell click, which will be done of course with RxSwift, and for that we will also need DisposeBag . Additionally we will create new Observable, that will be our text from search bar, but filtered (remove duplicates, wait for changes, everything from #1 of our tutorials).

In summary we will have 3 properties to add and setupRx() method to implement. Let’s do this!

class IssueListViewController: UIViewController { ... let disposeBag = DisposeBag() var provider: RxMoyaProvider! var latestRepositoryName: Observable { return searchBar .rx.text .orEmpty .debounce(0.5, scheduler: MainScheduler.instance) .distinctUntilChanged() } ... func setupRx() { // First part of the puzzle, create our Provider provider = RxMoyaProvider() // Here we tell table view that if user clicks on a cell, // and the keyboard is still visible, hide it tableView .rx.itemSelected .subscribe(onNext: { indexPath in if self.searchBar.isFirstResponder() == true { self.view.endEditing(true) } }) .addDisposableTo(disposeBag) } ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class IssueListViewController : UIViewController { . . . let disposeBag = DisposeBag ( ) var provider : RxMoyaProvider ! var latestRepositoryName : Observable { return searchBar . rx . text . orEmpty . debounce ( 0.5 , scheduler : MainScheduler . instance ) . distinctUntilChanged ( ) } . . . func setupRx ( ) { // First part of the puzzle, create our Provider provider = RxMoyaProvider ( ) // Here we tell table view that if user clicks on a cell, // and the keyboard is still visible, hide it tableView . rx . itemSelected . subscribe ( onNext : { indexPath in if self . searchBar . isFirstResponder ( ) == true { self . view . endEditing ( true ) } } ) . addDisposableTo ( disposeBag ) } . . . }

And wow, first magic happened. I hope that latestRepositoryName variable code looks familiar to you, because it was in first part of the series and was deeply discussed there. Let’s move onto the more interesting things. First we have that mystery Provider setup we were talking about. As you can see – nothing special, just initializer. And because we are using Moya with RxSwift, we have to use RxMoyaProvider . If you would ever want to write API using Moya and ReactiveCocoa or using just Moya, there are different providers for each of them ( MoyaProvider for pure Moya and ReactiveCocoaMoyaProvider for ReactiveCocoa + Moya).

LET’S TALK ABOUT YOUR APP We’re 100% office based team with 7-years’ experience

in mobile & web app development Estimate project

Then we have the keyboard hiding setup. Thanks to RxCocoa, we have access to tableView.rx.itemSelected , which emits signal every time someone taps on table view cell. We can of course subscribe to it, and do our thing (hiding keyboard). We are checking if search bar is our first responder (if the keyboard is shown), and we are hiding it.

That would be it for basic View Controller and Moya setup. Step 2, we are coming!

Step 2 – Network model and mapping objects

Now we need our model that will give us the data based on the text. But first, we will also need to parse the objects before we send any info at all. That will be done thanks to our friends at ModelMapper. We will need 2 entities, one for Repository, and one for Issue. These are really easy to create, we need to conform to Mappable protocol and just try to parse objects. Let’s create them!

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") } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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" ) } }

import Mapper 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") } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import Mapper 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" ) } }

We won’t need many properties, you can add more based on this GitHub API docs.

Okay, that was quick, now we move to the most interesting thing in this tutorial, IssueTrackerModel – the core of our Networking. First, our model should have Provider property that we will pass in init . Then we should have a property for our observable text, which is Observable type, that will be our source of repositoryNames , that our view controller will pass. And from methods, we for sure will need one method to return our observable sequence of issues array, Observable<[Issue]> , that view controller will use to bind the table view. And we don’t need to implement init , because swift backs us up with memberwise initializer.

Let’s create the IssueTrackerModel.swift :

import Foundation import Moya import Mapper import Moya_ModelMapper import RxOptional import RxSwift struct IssueTrackerModel { let provider: RxMoyaProvider let repositoryName: Observable func trackIssues() -> Observable<[Issue]> { } internal func findIssues(repository: Repository) -> Observable<[Issue]?> { } internal func findRepository(name: String) -> Observable<Repository?> { } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import Foundation import Moya import Mapper import Moya_ModelMapper import RxOptional import RxSwift struct IssueTrackerModel { let provider : RxMoyaProvider let repositoryName : Observable func trackIssues ( ) - > Observable < [ Issue ] > { } internal func findIssues ( repository : Repository ) - > Observable < [ Issue ] ? > { } internal func findRepository ( name : String ) - > Observable < Repository ? > { } }

As you can see I’ve added two more functions. One, findRepository(_:) will return optional repository (nil if it can’t map the object from response, Repository object if it can), and findIssues(_:) (the same logic with optional), that will of course search for repositories based on a given repository object. First let’s implement those two methods. What you think is really tricky, in fact is so amazingly simple to do with our setup:

internal func findIssues(repository: Repository) -> Observable<[Issue]?> { return self.provider .request(GitHub.issues(repositoryFullName: repository.fullName)) .debug() .mapArrayOptional(Issue.self) } internal func findRepository(name: String) -> Observable<Repository?> { return self.provider .request(GitHub.repo(fullName: name)) .debug() .mapObjectOptional(Repository.self) } 1 2 3 4 5 6 7 8 9 10 11 12 13 internal func findIssues ( repository : Repository ) - > Observable < [ Issue ] ? > { return self . provider . request ( GitHub . issues ( repositoryFullName : repository . fullName ) ) . debug ( ) . mapArrayOptional ( Issue . self ) } internal func findRepository ( name : String ) - > Observable < Repository ? > { return self . provider . request ( GitHub . repo ( fullName : name ) ) . debug ( ) . mapObjectOptional ( Repository . self ) }

Step by step:

1. We have provider, on which we can perform request with a given enum case.

2. We then pass GitHub.repo or GitHub.issues and voila, request done!

3. We use the debug() operator, which prints for us some valuable info from the request – it’s really useful in development/testing stage.

4. We could then try to parse and map the response manually, but thanks to our extension we have access to methods like mapObject() , mapArray() , mapObjectOptional() or mapArrayOptional() . What is the difference? With optional methods, when the object can’t be parsed, function returns nil. In normal methods it throws errors and we have to catch them using catch() functions or retry() . But in our case optionals are perfectly fine. We can clear our table view if the request was a failure.

Okay, okay. We have two methods that give us something based on something. How do I connect them though? For this task we will need to learn new operator, flatMap() and especially flatMapLatest() . What these operators do is, from one sequence they create another one. Why would you need that? Let’s say we have a sequence of strings, that you want to convert into sequence of repositories. Or sequence of repositories into sequence of issues. So… exactly like in our case! We will transform it in a chaining operation.

And when we got nil (while getting Repository or Issues for the Repository object), we will return empty array to clear our table view. But what’s the difference between flatMap() and flatMapLatest() ? Well, flatMap() gets one value, then performs long task, and when it gets the next value, previous task will still finish even when the new value arrives in the middle of the current task. It isn’t really what we need because when we get a new text in the search bar, we want to cancel the previous request and start another. That’s what flatMapLatest() does.

Our trackIssues method should look like the one below:

func trackIssues() -> Observable<[Issue]> { return repositoryName .observeOn(MainScheduler.instance) .flatMapLatest { name -> Observable<Repository?> in print("Name: \(name)") return self .findRepository(name) } .flatMapLatest { repository -> Observable<[Issue]?> in guard let repository = repository else { return Observable.just(nil) } print("Repository: \(repository.fullName)") return self.findIssues(repository) } .replaceNilWith([]) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func trackIssues ( ) - > Observable < [ Issue ] > { return repositoryName . observeOn ( MainScheduler . instance ) . flatMapLatest { name - > Observable < Repository ? > in print ( "Name: \ ( name ) " ) return self . findRepository ( name ) } . flatMapLatest { repository - > Observable < [ Issue ] ? > in guard let repository = repository else { return Observable . just ( nil ) } print ( "Repository: \ ( repository . fullName ) " ) return self . findIssues ( repository ) } . replaceNilWith ( [ ] ) }

Step by step:

1. We wanna make sure it is observed on MainScheduler, because the purpose of this model is to bind it to UI, in our case table view.

2. We transform our text (repository name) into observable repository sequence, that can be nil in case it doesn’t map our object correctly.

3. We check if the repository we’ve mapped is nil or not.

If it is nil, just return observable nil sequence (which in case the repository is nil, next flatMapLatest() guarantees the empty array as a response). Observable.just(nil) means that we will send one item as an observable (in our case that item will be nil ).

If it isn’t nil, we want to transform it to an array of issues (if the repository has any issues). It can also return nil or array with issues, so we still have observable with optional array.

4. .replaceNilWith([]) is RxOptional extension that helps us with nil, in our case we transform nil to empty array to clear table view.

And that’s it for our model! Really easy if you think about it. Just read the code few times, try to move operators, change them, replace them. Try it yourself.

Step 3 – Bind issues to table view

The last step would be to just connect data from the model we created to the table view. Which means we have to bind the observable to our table view.

Normally, you would need to conform our view controller to UITableViewDataSource , implement a few methods like number of rows, cell for row, and so on, then assign dataSource to the view controller.

With RxSwift, we can setup our UITableViewDataSource with only one closure! That’s right, RxCocoa provides us with another great utility, called rx.itemsWithCellFactory , which in closure takes the cell that we want to show. It simultaneously does everything for us, just based on our observable and the closure we provide. Magic! And the code looks really good too!

Now we go back to our IssueListViewController , and we implement the full setupRx() method:

class IssueListViewController: UIViewController { ... var issueTrackerModel: IssueTrackerModel! ... func setupRx() { // First part of the puzzle, create our Provider provider = RxMoyaProvider() // Now we will setup our model issueTrackerModel = IssueTrackerModel(provider: provider, repositoryName: latestRepositoryName) // And bind issues to table view // Here is where the magic happens, with only one binding // we have filled up about 3 table view data source methods issueTrackerModel .trackIssues() .bindTo(tableView.rx.items) { tableView, row, item in let cell = tableView.dequeueReusableCell(withIdentifier: "issueCell", for: IndexPath(row: row, section: 0)) cell.textLabel?.text = item.title return cell } .addDisposableTo(disposeBag) // Here we tell table view that if user clicks on a cell, // and the keyboard is still visible, hide it tableView .rx.itemSelected .subscribe(onNext: { indexPath in if self.searchBar.isFirstResponder == true { self.view.endEditing(true) } }) .addDisposableTo(disposeBag) } ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class IssueListViewController : UIViewController { . . . var issueTrackerModel : IssueTrackerModel ! . . . func setupRx ( ) { // First part of the puzzle, create our Provider provider = RxMoyaProvider ( ) // Now we will setup our model issueTrackerModel = IssueTrackerModel ( provider : provider , repositoryName : latestRepositoryName ) // And bind issues to table view // Here is where the magic happens, with only one binding // we have filled up about 3 table view data source methods issueTrackerModel . trackIssues ( ) . bindTo ( tableView . rx . items ) { tableView , row , item in let cell = tableView . dequeueReusableCell ( withIdentifier : "issueCell" , for : IndexPath ( row : row , section : 0 ) ) cell . textLabel ? . text = item . title return cell } . addDisposableTo ( disposeBag ) // Here we tell table view that if user clicks on a cell, // and the keyboard is still visible, hide it tableView . rx . itemSelected . subscribe ( onNext : { indexPath in if self . searchBar . isFirstResponder == true { self . view . endEditing ( true ) } } ) . addDisposableTo ( disposeBag ) } . . . }

What is new here, is our new property for our IssueTrackerModel (which we also initialize in setupRx() ), and also new binding: from the model’s trackIssues() method, to rx.itemsWithCellFactory property. Also don’t forget to change the cellIndentifier in dequeueReusableCell() method.

And that’s it! Everything we wanted to implement is implemented! Run the project and be happy with the results!

It was really long run and I’m really proud that we’ve come so far. I hope that everything was clear, but if you have any questions or feedback: message me on Twitter, send me an e-mail, or just comment here. I really love your messages and I’d love to hear what can be improved or what would you see in the next episodes ✌️

Also I’ve improved the resources for RxSwift in our repository, so be sure to check them out. And subscribe(?) to get the latest info about our series or RxSwift in general.

Read more articles about RxSwift

RxSwift by Examples #1 – The Basics

RxSwift by Examples #2 – Observable and the Bind

RxSwift by Examples #3 – Networking

RxSwift by Examples #4 – Multithreading