Alamofire + AlamofireObjectMapper

Dec 27, 2018 · Follow on Twitter and Mastodon swiftrealm

This is an updated version of a talk I gave at CocoaHeads Sthlm in 2017, on how to use Alamofire to communcate with an api, AlamofireObjectMapper to map responses, the Alamofire RequestRetrier to automatically retry failing requests and the RequestAdapter to adapt all requests. I also demonstrated how to use Realm to seamlessly add offline support, using the decorator pattern.

In this post, I’ll recreate the entire app from scratch, with some modifications. I have updated the original post to Swift 4.2 and to use some new code conventions as well.

Disclaimer

Since I gave this talk, Codable has been released as a native part of Swift. I will update this blog post to Swift 4.2, but you should really be using Codable instead of Mappable, since it’s now SO easy to combine Alamofire and Codable.

Regarding the demo app structure, I normally prefer to extract as much logic and code as possible to separate libraries, which I then can use as decoupled blocks. For instance, I would keep my domain logic in a domain library that doesn’t know anything about the app. I’d also keep all api logic in an api library that knows about the domain, but not about the app. In this project, though, I will keep it simple. Think of the Api folder as a separate library, and Auth and Movies as part of a Domain library.

Update information

The original blog post was released in August 2017 and was updated about a week ago. The biggest differences between that post and this, is that this post changes the following:

  • I use struct instead of class for the models. Using structs simplifies how you can use and extend your model, collections etc. so using structs is something that I really recommend.
  • I no longer use model protocols. I instead use api-specific model structs that can be mapped to domain-specific models. This is much more flexible, although it requires more mapping code.
  • The Swift 4.2 demo app has much more comments, to guide developers and explain what the various parts do. The code is also a lot better ;)

Video

You can watch the original talk here. The talk focuses more on concepts than code, so that talk and this post complete eachother pretty well.

Prerequisites

For this article, I expect that you know how CocoaPods works. I will use terms like podfile, expecting you to know what it means.

Source Code

I recommend that you create an empty app project then work through this tutorial by coding. However, you can also download the source code for the Swift 4.2 demo app from GitHub. The master branch contains source code for the demo app and gh-pages contains source code for the static api.

Why use a static api?

In the demo, we will use a static api to fetch movies in different ways. The api is a static Jekyll web site with movies, that lets us grab single movies by id as well as top rated and top grossing movies.

If you want to have a look at the static api data model, you can use check out and run the gh-pages branch with Jekyll. The online api has been taken down since this post was written.

The limited api lets us focus on Alamofire and Realm instead of having to understand an external api, set up a developer account, handle auth logic etc.

Define the domain model

Start by creating a clean Xcode project. I went with a simple iOS storyboard app, but you can set it up in any way you like.

The app will fetch movie data from the api. A Movie has basic info and a cast list of MovieActors. For simplicity, MovieActor only has a name to show how easy deep mapping is with Alamofire.

Let’s define this domain-specific model as two structs. Create a Movies folder in the project root and add these two structs to it:

struct Movie {
    
    let id: Int
    let name: String
    let year: Int
    let releaseDate: Date
    let grossing: Int
    let rating: Double
    
    let cast: [MovieActor]
}
struct MovieActor {
    
    let name: String
}

As you’ll see later, we will convert the api-specific models we receive from the api to these structs. The app should only know about these structs and not know anything about the existence of an external api.

Define the domain logic

Now let’s define how the app should fetch movies. Add this protocol to Movies:

typealias MovieResult = (_ movie: Movie?, _ error: Error?) -> ()
typealias MoviesResult = (_ movies: [Movie], _ error: Error?) -> ()

protocol MovieService: class {
    
    func getMovie(id: Int, completion: @escaping MovieResult)
    func getTopGrossingMovies(year: Int, completion: @escaping MoviesResult)
    func getTopRatedMovies(year: Int, completion: @escaping MoviesResult)
}

The movie service lets us fetch fetch single movies as well as top grossing and top rated movies for a specific year. Having completion blocks open up for implementations to do this asynchronously.

Add Alamofire and AlamofireObjectMapper

Before we can add api-specific implementations to the app, we must use CocoaPods to specify that the app needs Alamofire and AlamofireObjectMapper.

Run pod init in the app root folder to make CocoaPods create a podfile. Then add Alamofire and AlamofireObjectMapper to the file and run pod install to download these libraries.

Once this is done, open the generated workspace instead of the project file.

Create an api specific domain model

With these dependencies in place, we can now add app-specific implememtations to the app. Create an Api folder in the project root, add a Movies folder to it and add these two types to it:

import ObjectMapper

class ApiMovie {
    
    required public init?(map: Map) {}
    
    var id = 0
    var name = ""
    var year = 0
    var releaseDate = Date(timeIntervalSince1970: 0)
    var grossing = 0
    var rating = 0.0
    var cast = [ApiMovieActor]()
    
    func convert() -> Movie {
        return Movie(
            id: id,
            name: name,
            year: year,
            releaseDate: releaseDate,
            grossing: grossing,
            rating: rating,
            cast: cast.map { $0.convert() }
        )
    }
}


// MARK: - Mappable

extension ApiMovie: Mappable {
    
    func mapping(map: Map) {
        id <- map["id"]
        name <- map["name"]
        year <- map["year"]
        releaseDate <- map["releaseDate"]
        grossing <- map["grossing"]
        rating <- map["rating"]
        cast <- map["cast"]
    }
}
import ObjectMapper

class ApiMovieActor {
    
    required public init?(map: Map) {}
    
    var name = ""
    
    func convert() -> MovieActor {
        return MovieActor(name: name)
    }
}


// MARK: - Mappable

extension ApiMovieActor: Mappable {
    
    func mapping(map: Map) {
        name <- map["name"]
    }
}

The api-specific types above have mapping logic that can be used by Alamofire to automatically map api responses to these types. They also have a convert() function to map them to app-specific types.

Besides this, ApiMovie uses a DateTransform and has an ApiMovieActor array that is easily converted using map/convert.

If we have set things up properly, we should now be able to point Alamofire to a valid url and recursively parse movie data with little effort.

Setup the core api logic

Before we create an api-specific MovieService implementation, let’s setup some core api logic in the Api folder, that our service implementation can use.

Managing api environments

Since real-world apps often have to switch between different api environments (e.g. test and production) I often use enums to specify available api environments.

I know we only have a single environment now, but I still prefer to have such an enum in place for later. Add this enum to the Api folder:

import Foundation

enum ApiEnvironment: String { case
    
    production = "http://danielsaidi.com/demo_Alamofire_AlamofireObjectMapper_Realm/api/"
    
    var url: String {
        return rawValue
    }
}

Managing api routes

With the ApiEnvironment enum in place, we can list available api routes in another enum. Add this enum to the Api folder:

import Foundation

enum ApiRoute { case
    
    auth,
    movie(id: Int),
    topGrossingMovies(year: Int),
    topRatedMovies(year: Int)
    
    var path: String {
        switch self {
        case .auth: return "auth"
        case .movie(let id): return "movies/\(id)"
        case .topGrossingMovies(let year): return "movies/topGrossing/\(year)"
        case .topRatedMovies(let year): return "movies/topRated/\(year)"
        }
    }
    
    func url(for environment: ApiEnvironment) -> String {
        return "\(environment.url)/\(path)"
    }
}

Since year and id are dynamic route segments, we use associated values. This is a really nice Swift enum feature. This enum can also provide complete route urls for specific api environments.

Managing api context

I usually have an ApiContext class that manages and persists api-specific information, such as the current environment, authentication tokens etc. This context can be used by services that need to communicate with the api. Using a singleton context ensures that all api specific services are properly affected whenever the context changes.

Let’s create an ApiContext protocol and as a non-persisted implementation. Add a Context folder to the Api folder, then add these files to it:

protocol ApiContext {
    
    var environment: ApiEnvironment { get set }
}
class NonPersistentApiContext: ApiContext {
    
    init(environment: ApiEnvironment) {
        self.environment = environment
    }
    
    var environment: ApiEnvironment
}

We can now inject this context into our api-specific service implementations, and add more properties later if we want to, e.g. authentication tokens. If we later would like to create a persistent context, e.g. one that stores data in UserDefault, we just have to create another implementation and replace the implementation we use in our app.

Specifying basic api behavior

To simplify how the app communicates with the api, let’s create a base class for api-based services. Add an Alamofire folder to the Api folder, then add this file to it:

import Alamofire

class AlamofireService {
    
    init(context: ApiContext) {
        self.context = context
    }
    
    
    var context: ApiContext
    
    
    func get(at route: ApiRoute) -> DataRequest {
        return request(at: route, method: .get, encoding: URLEncoding.default)
    }
    
    func post(at route: ApiRoute) -> DataRequest {
        return request(at: route, method: .post, encoding: JSONEncoding.default)
    }
    
    func put(at route: ApiRoute) -> DataRequest {
        return request(at: route, method: .put, encoding: JSONEncoding.default)
    }
    
    func request(at route: ApiRoute, method: HTTPMethod, params: Parameters = [:], encoding: ParameterEncoding) -> DataRequest {
        let url = route.url(for: context.environment)
        return Alamofire
            .request(url, method: method, parameters: params, encoding: encoding)
            .validate()
    }
}

Forcing our services to only use ApiRoute ensures that the app can’t make any unspecified requests. If the app would have to call any custom URLs later on, we could just add a .custom(url: String) case to the ApiRoute enum.

This was a pretty long setup, but we are now ready to fetch movies from the api!

Create an api-based movie service

Let’s create an api-based movie service that loads movies from the api, by using the foundation that we have setup. Just add this file to the Api/Movies folder:

import Alamofire
import AlamofireObjectMapper

class AlamofireMovieService: AlamofireService, MovieService {
    
    func getMovie(id: Int, completion: @escaping MovieResult) {
        get(at: .movie(id: id)).responseObject { (response: DataResponse<ApiMovie>) in
            let result = response.result.value?.convert()
            completion(result, response.result.error)
        }
    }
    
    func getTopGrossingMovies(year: Int, completion: @escaping MoviesResult) {
        get(at: .topGrossingMovies(year: year)).responseArray { (response: DataResponse<[ApiMovie]>) in
            let result = response.result.value?.map { $0.convert() }
            completion(result ?? [], response.result.error)
        }
    }
    
    func getTopRatedMovies(year: Int, completion: @escaping MoviesResult) {
        get(at: .topRatedMovies(year: year)).responseArray { (response: DataResponse<[ApiMovie]>) in
            let result = response.result.value?.map { $0.convert() }
            completion(result ?? [], response.result.error)
        }
    }
}

As you can see, the service is super-simple. It basically just performs get requests and specifies api-specific return types that are automatically mapped to by Alamofire and AlamofireObjectMapper, then uses convert() to map the api-specific types to the domain-specific types that are used by the app.

getMovie uses responseObject, while the other functions use responseArray. This is because getMovie returns an optional object, while the other functions return an array of objects.

I only use arrays here to show both object and array mapping. Instead of having your api return arrays, I strongly recommend to add the arrays to a response object. This gives you more flexibility in the future, where you can add more information to the response if needed.

Fetch movies

We can now make our app fetch data from the api. Replace the boilerplate code in ViewController with this code:

override func viewDidLoad() {
    super.viewDidLoad()
    let env = ApiEnvironment.production
    let context = NonPersistentApiContext(environment: env)
    let service = AlamofireMovieService(context: context)
    service.getTopGrossingMovies(year: 2016) { (movies, error) in
        if let error = error { return print(error.localizedDescription) }
        print("Found \(movies.count) movies:")
        movies.forEach { print("   \($0.name)") }
    }
}

IMPORTANT For data fetching to work, you must allow the app to perform external requests. Just add this to Info.plist (in a real world app, you should specify an exact list of trusted domains):

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

Now run the app. If everything is correctly setup, it should print the following:

Found 10 movies:
   Finding Dory
   Rouge One - A Star Wars Story
   Captain America - Civil War
   The Secret Life of Pets
   The Jungle Book
   Deadpool
   Zootopia
   Batman v Superman - Dawn of Justice
   Suicide Squad
   Doctor Strange

If you see this in Xcode’s log, the app now fetches movie data from the api and maps it to domain-specific models. Well done!

Now change the print format for each movie to look like this:

movies.forEach { print("   \($0.name) (\($0.releaseDate))") }

The app should now output the following:

Found 10 movies:
   Finding Dory (1970-01-01 00:33:36 +0000)
   Rouge One - A Star Wars Story (1970-01-01 00:33:36 +0000)
   Captain America - Civil War (1970-01-01 00:33:36 +0000)
   The Secret Life of Pets (1970-01-01 00:33:36 +0000)
   The Jungle Book (1970-01-01 00:33:36 +0000)
   Deadpool (1970-01-01 00:33:36 +0000)
   Zootopia (1970-01-01 00:33:36 +0000)
   Batman v Superman - Dawn of Justice (1970-01-01 00:33:36 +0000)
   Suicide Squad (1970-01-01 00:33:36 +0000)
   Doctor Strange (1970-01-01 00:33:36 +0000)

Oooops! Seems like the date parsing doesn’t work. I told you that we would have fix this. Let’s do it.

Fix date parsing

The problem is that the api uses a different date format than Alamofire expected. This can be solved by replacing the DateTransform. Add a Date folder to Api and add this extension to it:

import ObjectMapper

public extension DateTransform {
    
    public static var custom: DateFormatterTransform {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        formatter.timeZone = TimeZone(secondsFromGMT: 0)
        return DateFormatterTransform(dateFormatter: formatter)
    }
}

Now change the releaseDate mapping in the ApiMovie class to look like this:

releaseDate <- (map["releaseDate"], DateTransform.custom)

Problem solved! The app should now output the following instead:

Found 10 movies:
   Finding Dory (2016-06-17 00:00:00 +0000)
   Rouge One - A Star Wars Story (2016-12-16 00:00:00 +0000)
   Captain America - Civil War (2016-05-06 00:00:00 +0000)
   The Secret Life of Pets (2016-07-08 00:00:00 +0000)
   The Jungle Book (2016-04-15 00:00:00 +0000)
   Deadpool (2016-02-12 00:00:00 +0000)
   Zootopia (2016-03-04 00:00:00 +0000)
   Batman v Superman - Dawn of Justice (2016-03-25 00:00:00 +0000)
   Suicide Squad (2016-08-05 00:00:00 +0000)
   Doctor Strange (2016-11-05 00:00:00 +0000)

If you inspect the other properties, you will see that they are correctly parsed. Time to celebrate! …then extend Alamofire with some additional functionality.

Retry failing requests

In the real world, a user most often has to authenticate her/himself in order to use some parts of an api. Authentication often returns a set of tokens, commonly an auth token and a refresh token.

If the auth and refresh token pattern is used, the authentication flow could look something like this:

  • If no tokens exist and a request fails with an HTTP 401, the user may have to login (if the request is mandatory). If so, show a login screen/prompt.
  • If tokens exist, the app should provide the auth token with each request.
  • If an auth token-based request fails with an HTTP 401, the auth token has most probably expired. The app should then save any requests that fail with 401 and use the refresh token to request new tokens from the api.
  • If the refresh succeeds, the app should parse the new tokens and retry failed requests with these new tokens. The app should use these new tokens from now on.
  • If the refresh request fails, the app should delete all tokens and logout the user. If the app requires a logged in user, the app should show a login screen.

Alamofire makes this kind of logic easy to implement, since it has a RequestRetrier protocol that we can implement and inject into Alamofire. It’s automatically notified about every failing request and lets you determine if a request should be retried or not.

Let’s demonstrate this by faking a failing request. First, add an auth route to ApiRoute, using auth as path. The static api will always give us the same auth token, but it’s good enough for demo purposes.

Second, add a new Auth folder and add this protocol to it:

typealias AuthResult = (_ token: String?, _ error: Error?) -> ()

protocol AuthService: class {
    
    func authorize(completion: @escaping AuthResult)
}

This is a very simple protocol that describes how the app authorizes itself. The app will be able to use this without having to care about how it’s implemented.

Before we implement it, we have to add a way to store any auth tokens we receive. Remember what I told you about the ApiContext? Well, I think it is the PERFECT place to store api tokens as well, so let’s do that. Add an authToken property to the ApiContext protocol:

var authToken: String? { get set }

Also, add this property to NonPersistentApiContext (if we had a persistent one, it would remember the token even if restarted the app, but that’s something that you could perhaps try to build yourself):

var authToken: String?

Now, let’s create an Alamofire-based AuthService. Add an Auth folder to Api and add this class:

import Alamofire
import AlamofireObjectMapper

class ApiAuthService: AlamofireService, AuthService {
    
    func authorize(completion: @escaping AuthResult) {
        get(at: .auth).responseString { (response: DataResponse<String>) in
            if let token = response.result.value {
                self.context.authToken = token
            }
            completion(response.result.value, response.result.error)
        }
    }
}

If the request above succeeds, the token will be saved in our api context, which makes it available to all future api requests.

Now, let’s (finally) retry some requests by creating a custom request retrier! Add this retrier code to the Api/Alamofire folder:

import Alamofire

class ApiRequestRetrier: RequestRetrier {
    
    
    // MARK: - Initialization
    
    init(context: ApiContext, authService: AuthService, statusCodeTrigger: Int = 404 /* 401 */) {
        self.context = context
        self.authService = authService
        self.statusCodeTrigger = statusCodeTrigger
    }
    
    
    // MARK: - Dependencies
    
    private let authService: AuthService
    private var context: ApiContext
    private let statusCodeTrigger: Int
    
    
    // MARK: - Properties
    private var isAuthorizing = false
    private var retryQueue = [RequestRetryCompletion]()
    
    
    // MARK: - RequestRetrier
    
    func should(
        _ manager: SessionManager,
        retry request: Request,
        with error: Error,
        completion: @escaping RequestRetryCompletion) {
        
        guard
            shouldRetryRequest(with: request.request?.url),
            shouldRetryResponse(with: request.response?.statusCode)
            else { return completion(false, 0) }
        
        authorize(with: completion)
    }
}


// MARK: - Private Functions

private extension ApiRequestRetrier {
    
    func authorize(with completion: @escaping RequestRetryCompletion) {
        print("Authorizing application...")
        retryQueue.append(completion)
        guard !isAuthorizing else { return }
        isAuthorizing = true
        authService.authorize { (token, error) in
            self.isAuthorizing = false
            self.printAuthResult(token, error)
            self.context.authToken = token
            let success = token != nil
            self.retryQueue.forEach { $0(success, 0) }
            self.retryQueue.removeAll()
        }
    }
    
    func printAuthResult(_ token: String?, _ error: Error?) {
        if let error = error {
            return print("Authorizing failed: \(error.localizedDescription)")
        }
        if let token = token {
            return print("Authorizing succeded: \(token)")
        }
        print("No token received - failing!")
    }
    
    func shouldRetryRequest(with url: URL?) -> Bool {
        guard let url = url?.absoluteString else { return false }
        let authPath = ApiRoute.auth.path
        return !url.contains(authPath)
    }
    
    func shouldRetryResponse(with statusCode: Int?) -> Bool {
        return statusCode == statusCodeTrigger
    }
}

Whenever a request fails, Alamofire will ask the retrier if it should be retried. The retrier will trigger a retry if the request is not a failing auth. If not, it just lets the request fail.

If a request should be retried, it’s added it to a retry queue. The retrier then triggers an authorization. Once it completes, the retrier checks if it succeeded. If so, all queued requests are retried. If not, they are made to fail. The retry queue is then cleared.

Note that this is completely hidden from the user as well as the app itself. The retrier works under the hood, tightly connected to Alamofire’s internal workings. It just notifies the app if the authorization fails, by failing all requests.

Inject the retrier into Alamofire by adding the following to our viewDidLoad (note that you have to add import Alamofire topmost as well):

let manager = SessionManager.default
manager.retrier = ApiRequestRetrier(context: context, authService: authService)

IMPORTANT In the real world, a 401 status code is an indication that tokens should be refreshed. If this refresh fails, a 401 indicates that the user has to log in, since the tokens are invalid. Here, however, we will never receive a 401, since we use a static api. We thus have to trigger these mechanisms by doing the following:

  • Kill your connection and perform a clean install, to remove all stored data.
  • Add a breakpoint to the retrier’s authService.authorizeApplication call.
  • Run the app. The app should now fail the request and activate this breakpoint.
  • Bring the connection back online and resume the app.
  • This should make the auth request succeed and have Alamofire retry the request.

That’s it! Alamofire should now retry any failing request that are not auth ones.

Adapt all api requests

Sometimes, you have to add custom headers to every request you make to an api. A common scenario is to add Accept information, auth tokens etc.

To adapt all requests before they are sent by an app, you just have to implement the RequestAdapter protocol and inject it into Alamofire.

Let’s give it a try! Add this file to the Api/Alamofire folder:

import Alamofire

class ApiRequestAdapter: RequestAdapter {
    
    public init(context: ApiContext) {
        self.context = context
    }
    
    private let context: ApiContext
    
    func adapt(_ request: URLRequest) throws -> URLRequest {
        guard let token = context.authToken else { return request }
        var request = request
        request.setValue(token, forHTTPHeaderField: "AUTH_TOKEN")
        return request
    }
}

As you can see, the adapter just adds any existing token to the request headers. Inject this adapter into Alamofire by adding the following to our viewDidLoad:

manager.adapter = ApiRequestAdapter(context: context)

That’s it! Alamofire should now add the auth token to all requests.

Adding offline support with Realm

We will now add offline support to our app, so that we can still fetch data when we are offline. There’s a million ways to do this, but we’ll do it by adding Realm to our app and building a new services that stores movies to a local database.

When we create this new service, we’ll use the decorator pattern, where the new service will use a base service to fetch data, then add the database logic on top of this. The decorator pattern is great when you want to compose services and let each service be responsible for its own scope. It makes it very easy to test each service and provides you with a flexible, composable code base.

Add Realm support

Before we can create a Realm-specific implementation of our service and domain model, we have to add Realm support to our app.

To do this, just add RealmSwift to podfile and run pod install. When Realm has been installed, we can create proceed with creating a Realm-specific model.

Create a Realm-specific model

Unlike the old Swift 3 implementation of this app, I no longer use protocols for the domain model. Instead, I use structs and map other representations to that domain model.

As such, the Realm-specific model will not inherit any other class nor implement any model protols. Instead, it will just define the properties it needs, as well as add some mapping functions.

Create a new Realm folder in the app root and these two Realm classes to it:

import RealmSwift

class RealmMovieActor: Object {
    
    convenience init(from actor: MovieActor) {
        self.init()
        self.name = actor.name
    }
    
    @objc dynamic var name = ""
    
    func convert() -> MovieActor {
        return MovieActor(name: name)
    }
}
import RealmSwift

class RealmMovie: Object {
    
    // MARK: - Initialization
    
    convenience init(from: Movie) {
        self.init()
        self.id = from.id
        self.name = from.name
        self.year = from.year
        self.releaseDate = from.releaseDate
        self.grossing = from.grossing
        self.rating = from.rating
        from.cast
            .map { RealmMovieActor(from: $0) }
            .forEach { self.cast.append($0) }
    }
    
    
    // MARK: - Properties
    
    @objc dynamic var id = 0
    @objc dynamic var name = ""
    @objc dynamic var year = 0
    @objc dynamic var releaseDate = Date(timeIntervalSince1970: 0)
    @objc dynamic var grossing = 0
    @objc dynamic var rating = 0.0
    let cast = List<RealmMovieActor>()
    
    
    // MARK: - Primary Key
    
    override class func primaryKey() -> String? {
        return "id"
    }
    
    
    // MARK: - Functions
    
    func convert() -> Movie {
        return Movie(
            id: id,
            name: name,
            year: year,
            releaseDate: releaseDate,
            grossing: grossing,
            rating: rating,
            cast: cast.map { $0.convert() }
        )
    }
}

As you can see, just as the api-specific models, these are regular Realm objects that can be mapped to our domain model. Both inherit the Realm Object class and have a convenience initializer that copies a domain model instance, which will be needed when we create our Realm-based movie service.

Create a Realm-specific movie service

Let’s add a Realm-specific movie service that lets us store movies and movie actors from the api into Realm. Add this file to the Realm folder:

import RealmSwift

class RealmMovieService: MovieService {

    
    // MARK: - Initialization
    
    init(baseService: MovieService) {
        self.baseService = baseService
    }
    
    
    // MARK: - Dependencies
    
    private let baseService: MovieService
    private var realm: Realm { return try! Realm() }
    
    
    // MARK: - Functions
    
    func getMovie(id: Int, completion: @escaping MovieResult) {
        getMovieFromDb(id: id, completion: completion)
        getMovieFromBaseService(id: id, completion: completion)
    }
    
    func getTopGrossingMovies(year: Int, completion: @escaping MoviesResult) {
        getTopGrossingMoviesFromDb(year: year, completion: completion)
        getTopGrossingMoviesFromBaseService(year: year, completion: completion)
    }
    
    func getTopRatedMovies(year: Int, completion: @escaping MoviesResult) {
        getTopRatedMoviesFromDb(year: year, completion: completion)
        getTopRatedMoviesFromBaseService(year: year, completion: completion)
    }
}


// MARK: - Database Functions

private extension RealmMovieService {
    
    func getMovieFromDb(id: Int, completion: @escaping MovieResult) {
        let obj = realm.object(ofType: RealmMovie.self, forPrimaryKey: id)
        guard let movie = obj?.convert() else { return }
        completion(movie, nil)
    }
    
    func getTopGrossingMoviesFromDb(year: Int, completion: @escaping MoviesResult) {
        let objs = realm.objects(RealmMovie.self).filter("year == \(year)")
        let sorted = objs.sorted { $0.grossing > $1.grossing }.map { $0.convert() }
        completion(sorted, nil)
    }
    
    func getTopRatedMoviesFromDb(year: Int, completion: @escaping MoviesResult) {
        let objs = realm.objects(RealmMovie.self).filter("year == \(year)")
        let sorted = objs.sorted { $0.rating > $1.rating }.map { $0.convert() }
        completion(sorted, nil)
    }
    
    func persist(_ movie: Movie?) {
        persist([movie].compactMap { $0 })
    }
    
    func persist(_ movies: [Movie]) {
        let objs = movies.map { RealmMovie(from: $0) }
        try! realm.write {
            realm.add(objs, update: true)
        }
    }
}


// MARK: - Base Service Functions

private extension RealmMovieService {
    
    func getMovieFromBaseService(id: Int, completion: @escaping MovieResult) {
        baseService.getMovie(id: id) { [weak self] (movie, error) in
            self?.persist(movie)
            completion(movie, error)
        }
    }
    
    func getTopGrossingMoviesFromBaseService(year: Int, completion: @escaping MoviesResult) {
        baseService.getTopGrossingMovies(year: year) { [weak self] (movies, error) in
            self?.persist(movies)
            completion(movies, error)
        }
    }
    
    func getTopRatedMoviesFromBaseService(year: Int, completion: @escaping MoviesResult) {
        baseService.getTopRatedMovies(year: year) { [weak self] (movies, error) in
            self?.persist(movies)
            completion(movies, error)
        }
    }
}

As you can see, the RealmMovieService initializer requires another MovieService instance, as I described earlier. This is the decorator pattern in action, where RealmMovieService uses another implementation of the same a protocol to extend the base implementation with Realm-specific logic.

In this case, baseService will be an AlamofireMovieService, but the decorator most not know anything about the base service, only what the protocol promises.

In this case, RealmMovieService will try to get data from the database, but at the same time, it will also try to get data from the base service. When the base service completes, RealmMovieService saves any data it receives, then calls the completion block with the data.

Disclaimer: This is an intentionally simple design. RealmMovieService always loads data from the database and from the base service. In a real app, you’d probably have some logic to determine if calling the base service is needed.

Put Realm into action

Let’s give the new movie service a try. Modify viewDidLoad to look like this:

override func viewDidLoad() {
    super.viewDidLoad()
    let env = ApiEnvironment.production
    let context = NonPersistentApiContext(environment: env)
    let baseService = AlamofireMovieService(context: context)
    let service = RealmMovieService(baseService: baseService)
    var invokeCount = 0
    service.getTopGrossingMovies(year: 2016) { (movies, error) in
        invokeCount += 1
        if let error = error { return print(error.localizedDescription) }
        print("Found \(movies.count) movies (callback #\(invokeCount))")
    }
}

We rename the AlamofireMovieService to baseService and create a RealmMovieService instance, into which we inject the baseService. The app is still loading top grossing movies using a service, but the instance will now first check the database then call the api.

An important thing here is that the app doesn’t care about any of this. Just as the decorator doesn’t care about the internal workings of its base service, the app only uses the protocol, not the implementation. In the code above, it actually knows about the implementation, but we’ll fix that later.

The output will be the following, the first time we run the app with this setup:

Found 0 movies  (callback #1)
Found 10 movies (callback #2)

This happens because the database has no data, while the api will load 10 movies. If you run the app again, the output should now be:

Found 10 movies (callback #1)
Found 10 movies (callback #2)

This happens because the database now has data, which means that both completions will return 10 movies.

It’s worth repeating that having multiple callbacks for a single function call is not good. We only have it here for simplicity and to visualize what’s going on. You should adjust the service implementation to only call the completion block once.

Now bring the app offline and call getTopRatedMovies instead (Alamofire caches the previous result, so we have to fetch previously unfetched data). If you then run the app again, the output should be:

Found 10 movies (callback #1)
ERROR: The Internet connection appears to be offline.

This happens because the database data can still be loaded, while the api cannot be called since the Internet connection is dead.

We now have an app with offline support, that only refreshes its data whenever a call to the api provides new data. All we had to do was to change two lines that determine which service implementation we use.

Add Dependency Injection to the app

I won’t show the specifics here, since it just add even more complexity to an already long post. In the demo app, however, I have an IoC folder, in which I use a library called Dip to resolve dependencies.

By adding Dip to podfile and running pod install, we can make the app much cleaner and more robust, since we’ll register all dependencies when the app launches, then resolve dependencies with constructor injection, or by calling IoC.resolve(...), which is required if you use storyboards.

Take a look at the demo app if you are interested in the specifics. In short, it lets us remove a lot of code from our view controller, which then looks like this:

import UIKit
import Alamofire

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        reloadData(self)
    }
    
    lazy var movieService: MovieService = IoC.resolve()
    
    ...
}

With dependency injection in place, the app no longer knows anything about which implementations we use. The only part of the app that now knows about the api, a database etc. is the IoC. This makes it easy to change implementations later, since we just have to change implementations at a single place.

Conclusion

Well done! You have created an app that uses Alamofire to fetch data from an api and that also injects a RequestRetrier and a RequestAdapter to Alamofire to change how it adapts all outgoing requests and handles any failing ones. We also use Realm to implement an offline layer, as well as IoC to get a flexible architecture.

I hope this was helpful. Don’t hesistate to connect with me on Twitter if you want to discuss further.

Discussions & More

Please share any ideas, feedback or comments you may have in the Disqus section below, or by replying on Twitter or Mastodon..

If you found this text interesting, make sure to follow me on Twitter and Mastodon for more content like this, and to be notified when new content is published.

If you like & want to support my work, please consider sponsoring me on GitHub Sponsors.