If you’re here, you’re problably an iOS developer and chances are you needed to implement some REST calls in your apps.
Unless you’re a fan of DIY, you’ve probably used the most popular networking library that is Alamofire. I’ve done this too.
It can easily get messy with Alamofire so today we're going to try Moya, an Alamofire wrapper that encourages you to split your service definition in a different place from the actual implementation.
And we're going to do this with RxSwift to make our code cleaner.
We’ll keep this basic and use some stub API as our endpoint and we’re going to GET and DELETE some posts.
Let’s get started, shall we?
Installation
First of all, we need to install Moya+RxSwift with Cocoapods:
In your Podfile add:
pod 'Moya/RxSwift', '~> 12.0'
And then, in the terminal run:
pod install
and we’re done! You can find other installation methods on the repo’s README here.
We’ll start by defining the endpoints that we’re going to use.
To do so, we need an enum with a case for each operation.
If we need to pass parameters to our call, we can pass them to the enum case.
enum ForumService { | |
case getPosts | |
case deletePost(id: Int) | |
} |
Let’s shape up our service by implementing Moya’s TargetType.
This protocol will require us to define how each call should be built by Moya.
import Moya | |
extension ForumService: TargetType { | |
// This is the base URL we'll be using, typically our server. | |
var baseURL: URL { | |
return URL(string: "https://jsonplaceholder.typicode.com")! | |
} | |
// This is the path of each operation that will be appended to our base URL. | |
var path: String { | |
switch self { | |
case .getPosts: | |
return "/posts" | |
case .deletePost(let id): | |
return "/posts/\(id)" | |
} | |
} | |
// Here we specify which method our calls should use. | |
var method: Method { | |
switch self { | |
case .getPosts: | |
return .get | |
case .deletePost: | |
return .delete | |
} | |
} | |
// Here we specify body parameters, objects, files etc. | |
// or just do a plain request without a body. | |
// In this example we will not pass anything in the body of the request. | |
var task: Task { | |
return .requestPlain | |
} | |
// These are the headers that our service requires. | |
// Usually you would pass auth tokens here. | |
var headers: [String: String]? { | |
return ["Content-type": "application/json"] | |
} | |
// This is sample return data that you can use to mock and test your services, | |
// but we won't be covering this. | |
var sampleData: Data { | |
return Data() | |
} | |
} |
Ok, we’re done with our service specifications and now we just need to implement the calls. Now it’s RxSwift’s turn.
import RxSwift | |
import Moya | |
struct ForumNetworkManager { | |
// I'm using a singleton for the sake of demonstration and other lies I tell myself | |
private static let shared = ForumNetworkManager() | |
// This is the provider for the service we defined earlier | |
private let provider = MoyaProvider<ForumService>() | |
private init() {} | |
// We're returning a Single response with just an array with the retrieved posts. | |
// You could return an Observable<PostJSON> if you need to, this is just an example. | |
func getPosts() -> Single<[PostJSON]> { | |
return provider.rx // we use the Reactive component for our provider | |
.request(.getPosts) // we specify the call | |
.filterSuccessfulStatusAndRedirectCodes() // we tell it to only complete the call if the operation is successful, otherwise it will give us an error | |
.map([PostJSON].self) // we map the response to our Codable objects | |
} | |
// Here we return a Completable because we only need to know if the call is done or if there was an error. | |
func deletePost(with id: Int) -> Completable { | |
return provider.rx | |
.request(.deletePost(id: id)) | |
.filterSuccessfulStatusAndRedirectCodes() | |
.asObservable().ignoreElements() // we're converting to Observable and ignoring events in order to return a Completable, which skips onNext and only maps to onCompleted | |
} | |
} | |
We need to actually consume the calls right? We’ll do that right now.
For this example we’ll return a Completable, to keep it simple, and in the onSuccess
we manage the data parsed from the server response.
import RxSwift | |
class PostsViewModel { | |
func fetchRemotePosts() -> Completable { | |
return .create { observer in | |
ForumNetworkManager.getPosts() | |
.subscribe(onSuccess: { jsonPosts: [PostJSON] in | |
// we fetched the posts | |
observer(.completed) | |
}, onError: { error in | |
// there was an error fetching the posts | |
observer(.error(error)) | |
}) | |
} | |
} | |
func deletePost(with id: Int) -> Completable { | |
return .create { observer in | |
ForumNetworkManager.deletePost(with: id) | |
.subscribe(onCompleted: { | |
// we successfully deleted the post | |
observer(.completed) | |
}, onError: { error in | |
// there was an error deleting the post | |
observer(.error(error)) | |
}) | |
} | |
} | |
} |
We’re done with our network part and now you just need to handle the results of the calls.
How was it? Setting up your network layer with Moya is a bit more verbose than with Alamofire but investing a little more time will pay back in the long run, and adding some RxSwift only makes it better.
Let’s spice up our networking layer.
We’re about to add every developer’s two favourite things: logging and error handling.
Logging
This is the best thing ever and it’s also the easiest since Moya comes with a built-in logger, which can be setup like this:
MoyaProvider<ForumService>(plugins: [NetworkLoggerPlugin(verbose: true)])
After this you will start seeing calls being logged in the console. You can also make your own plugin! Find more about it here.
Error handling
Last but not least, error handling. You know how to handle your errors so I’m not going to teach you that, but since you probably use custom enums for error representation, you have to figure out what happened during the call and return the appropriate info in order to tell the user what went wrong.
Let’s see how we can handle errors in our network layer:
enum ExampleError: Error { | |
case somethingHappened | |
} | |
static func getPosts() -> Single<[PostJSON]> { | |
return provider.rx | |
.request(.getPosts) | |
.filterSuccessfulStatusAndRedirectCodes() | |
.map([PostJSON].self) | |
.catchError { error in | |
// this function catches any error that happens, | |
// you can recover and continue the sequence with another observable, | |
// but we're not doing this right now | |
// todo parse error and figure out what happened | |
throw ExampleError.somethingHappened | |
} | |
} |
When handling errors, you will just need to cast the error you receive to the one(s) you expect.
Conclusions
I hope you enjoyed this different solution. Some developers are against adding another layer to their network stack, but I find this a good compromise between having a messy network manager and having to architecture, test and maintain your own layer.
If you have comments or suggestions please don't be shy!
Top comments (0)