RxSwift by Examples #3 – Networking
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:
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:
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:
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
:
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!
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
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!
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") } } |
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
:
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:
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:
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:
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 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.
As always you can find complete source code on Droids on Roids’s GitHub repository and here you can check other RxSwift examples!
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
About the author
Ready to take your business to the next level with a digital product?
We'll be with you every step of the way, from idea to launch and beyond!
Hi, Thanx for this awesome tutorial.
But I have one issue with this. Example project is not working in Latest Moya-ModelMapper . Can you please check after updating pod.
Hey! Thank you for your kind words! 🙂
There was a bug in the Mapper extension for Moya, there is a fix for it already on CocoaPods, just do “pod update” and you should be fine. Sorry for the inconvenience!
Hi,
Do you know modelmapper issues have been resolved recently, I cant get RxMoyaExample to compile on cocoapods 0.39.0, any help will be appreciated
Hey! Yeah, with version 2.0.1 of Moya-ModelMapper it should be alright, did you do “pod update”? If yes, can you tell me what is the error?
I did do pod update, I see this and many more in Result & ResultType, when I do clean build I get comp errors for ModelWrapper, building it again shows error for Result module
Can’t really think of what could it be, it is really strange. Can you try deleting the project and downloading it again? Also what kind of errors these are?
Hi Lukasz,
I will second that this is an awesome tutorial. It has given me the push to try and implement Moya for the first time in one of my own little applications.
However I am having problems when trying to convert the response from the server into objects. I was wondering if you could possibly help me see what is going wrong. I have put a question up on Stackoverflow here: http://stackoverflow.com/questions/36516940/mapping-json-response-to-objects-using-rx-programming
Cheers.
Hey!
Thank you so much for the kind words 🙂 It is always hard in the beginning, but I hope that with these tutorials you will get along with RxSwift.
About the problem, we solved it with @prestonspalding:disqus in the SO thread given in the comment. Basically the response array, which was supposed to be parsed, was under the key in dictionary. We can parse it easily as well replacing method mapArrayOptional(User.class) with mapArrayOptional(User.class, keyPath: “user”), if the key in dictionary is “user”. More about parsing here: https://github.com/sunshinejr/Moya-ModelMapper.
Cheers!
It’s really an eye opener to start the Rx marathon on swift.:) recommended one for all beginners.
Thank you so much! ☺️
Thanks again for the great tutorial series. Please continue this RxSwift series.
Thanks to YOU ?Really appreciate the support.
Although I have less time for articles right now, the series ain’t ending for sure. The last with Multithreading was released this week 🙂
Thanks for your great work!! Please do more of these tutorials!
Would you be able to do an JWT authentication wtih RxSwift + Moya Tutorial?
Would love to see a realm integration into that. They both have different mappable protocol for some reason and it blocking me at the moment
Hey @disqus_G9SL8VT66B:disqus, sorry for late answer. I believe you can use namespacing to use specific protocol/type. The same problem was with Error type that was in both Moya and Swift. The answer to that was to use Moya.Error or Swift.Error (now Moya has MoyaError instead). Hope it helps!
Many thanks Łukasz for your tutorials, and hello from Katowice, Sląskie. 🙂
I am not an expert in Rx, but I can suggest to move out Moya’s provider creation from VC. IssuesTrackerModel, should be initialized in AppDelegate, or in some coordinator, and propagated into VC. In such case the architecture of MVVM will be better supported.
But apparently that the project is more connected with Rx, rather than architecture… ))
Hey @siarheibrazil:disqus, thanks for the tip! And yeah, this blog post is not about architecture, but I’ve extracted the network logic from VC to keep it clean. There are many architectures good for Rx, one of them is indeed MVVM. I actually did a talk about MVVM+Coordinators with RxSwift on Mobilization, because it feels really natural!
Thanks a lot for this articles series… I have a question, Why you use ModelMapper instead ObjectMapper (https://github.com/Hearst-DD/ObjectMapper). Any particular reason for that? what advantages have ModelMapper over ObjectMaper or viceversa
Hey @disqus_tD8311u6Lu:disqus! Mostly because of immutability in models (I can use let instead of vars, which in models is really important, at least for me).
@disqus_Np0jHs7JmF:disqus thanks… Other question, i wanna build some kind of Rest SDK to communicate with various servers (all the servers expose the same endpoints)… In the login form, you need to type the homeserver, so the baseURL needs that parameter, how can i do that?
What do you mean by cancelling the request here?
> 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.
Do you mean that the MoyaProvider will cancel the request, and then proceed requesting the new one? Or will it just discard the result of the ongoing request and proceed with requesting the new one?
Hey @severusrazzmatazz:disqus. Inner Observable (this in the flatMapLatest) will be disposed, and because RxMoya provider is cancelling the request on dispose, if the request didn’t finish, it should be canceled. If it did finish, the results will be discarded.
This is a source code where Moya is cancelling request on dispose: https://github.com/Moya/Moya/blob/master/Sources/RxMoya/RxMoyaProvider.swift#L34-L36
Thanks for great tutorials, but I had a question? If I use Moya to make request like you did, how to write unit test for network ?
Hi, thanks for the tutorial.
I have a question for the GithubEndpoint, how can I pass parameters to it?
I want for example to pass it a ‘MyModel’ to be used for the request parameters, or I want to tell him to use ‘.post’ instead of ‘.get’. I don’t see how I can do it in you code.
Stuck with Swift 4 and Xcode 9 on:
return self.provider
.request(GitHub.issues(repositoryFullName: repository.fullName))
.debug()
.mapArrayOptional(Issue.self)
Error: Missing argument for parameter ‘completion’ in call
and with return self.provider.rx.request….
Error: Value of type ‘PrimitiveSequence’ has no member ‘mapArrayOptional’
@Uma@umang_bista:disqus
include asObservable() in your code. You’ll have something like this:
return self.provider.request(GitHub.issues(repositoryFullName: repository.fullName)).debug().asObservable().mapOptional(to: [Issue].self)
asObservable() doesn’t fix the problem. Were you able to fix it?
@umang_bista:disqus @adeyinkaadediji:disqus
@swiftl:disqus
Yes. I fixed it.
Could you upload the exact piece of code to gist?
@swiftl2:disqus
https://gist.github.com/tdscientist/0a0002a8a5f71f44c08da96ae50d0a8a
Looks like a great tutorial but it’s not possible to follow as is. The Moya pod denpedency appears to have been updated and doesn’t work as specified in this Podfile (XCode 9.1, Swift 4.0) :
[!] Unable to satisfy the following requirements:
–
RxCocoa (~> 3.0.0)
required byPodfile
–
RxCocoa (~> 4.0)
required byRxOptional (3.3.0)
It works for me on Xcode 9.1 with:
pod ‘RxSwift’, ‘~> 3.0.0’
pod ‘RxCocoa’, ‘~> 3.0.0’
pod ‘Moya-ModelMapper/RxSwift’, ‘~> 4.1.0’
pod ‘RxOptional’