DEV Community

Mattia
Mattia

Posted on

4 2

Better networking with Moya + RxSwift

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)
}
view raw MyService.swift hosted with ❤ by GitHub

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)])


Enter fullscreen mode Exit fullscreen mode

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!

Image of Datadog

How to Diagram Your Cloud Architecture

Cloud architecture diagrams provide critical visibility into the resources in your environment and how they’re connected. In our latest eBook, AWS Solution Architects Jason Mimick and James Wenzel walk through best practices on how to build effective and professional diagrams.

Download the Free eBook

Top comments (0)

Image of Datadog

The Essential Toolkit for Front-end Developers

Take a user-centric approach to front-end monitoring that evolves alongside increasingly complex frameworks and single-page applications.

Get The Kit

👋 Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay