Realm and Alamofire in effective harmony

We would like to introduce you to how we used the Swift libraries Alamofire, Realm and ObjectMapper in the Hyphe iOS application in order to develop an elegant and simple synchronization between the Hyphe API and our iOS client.

In the Hyphe iOS application, Realm plays a central role. It is important for us, that Hyphe as an address book, has an ideal offline capability. We want to deliver an application to our users, that is free of latency and (almost) available everywhere. Therefore all relevant data from a user that the app fetches from the Hyphe API, will be stored locally in a Realm database.

The following illustration shows a very rough idea of the Hyphe iOS application. We are using a VIPER architecture, which was adapted for our requirements.

As you can see in the illustration, the app is separated into two parts, the front-end and the back-end. In the front-end Alamofire is not used. All relevant data is available through the Realm database.

The back-end is responsible for the synchronization between the Realm database and our API. In addition, the back-end informs the restrictive presenter when the Realm database is modified and vice versa. Additionally, the back-end will be informed to keep the Hyphe-API up-to-date. Through this design the presenters are very small and simple.

In this post we would like to expand on the interaction between the libraries in the back-end through the following example. We start with a simple example and upgrade it step by step to the final code.

An elegant GET request

A GET request delivers the following data:

GET: /books

[
  {
    "id"    : 1,
    "title" : "Book A",
    "autor" : "Autor 1",
    "genre" : "Drama"
  },
  {
    "id"    : 2,
    "title" : "Book B",
    "autor" : "Autor 2",
    "genre" : "Myth"
  },
  {
    "id"    : 3,
    "title" : "Book C",
    "autor" : "Autor 3",
    "genre" : "Satire"
  }
]

The associated Alamofire call looks like this:

Alamofire.request(.GET,<url>)  
         .responseJSON { response in
             print(response.request)  // original URL request
             print(response.response) // URL response
             print(response.data)     // server data
             print(response.result)   // result of response serialization

             if let JSON = response.result.value {
                 print("JSON: \(JSON)")
             }
         }

Map Alamofire responses in objects

AlamofireObjectMapper is an extension for Alamofire. This library provides a number of response handlers, for mapping server responses into Swift objects. This library helps to map the responses from the Alamofire request into book objects. Therefore, the book class has to follow the Mappable protocol of the ObjectMapper library.

class Book:Mappable {  
    var id = 0
    var title = ""
    var autor = ""
    var genre = ""

    //Impl. of Mappable protocol
    required convenience init?(_ map: Map) {
        self.init()
    }

    func mapping(map: Map) {
        id    <- map["id"]
        title <- map["title"]
        autor <- map["autor"]
        genre <- map["genre"]
    }
}
Alamofire.request(.GET,url)  
         .responseArray { (response: Response<[Book], NSError>) in
             print(response.request)  // original URL request
             print(response.response) // URL response
             print(response.data)     // server data
             let books = response.result.value  // books array
             //...
         }

Before we can store these book objects into a Realm database, the book class has to become a Realm data model. Therefore, the book class has to inherit the Object class, which is provided by the Realm library. Because the Realm library is written in Objective-C, all non-generic properties of a Realm data model need a dynamic var attribute.

class Book:Object,Mappable{  
    dynamic var id = 0
    dynamic var title = ""
    dynamic var autor = ""
    dynamic var genre = ""

    override static func primaryKey() -> String? {
        return "id"
    }

    //Impl. of Mappable protocol
    required convenience init?(_ map: Map) {
        self.init()
    }

    func mapping(map: Map) {
        id    <- map["id"]
        title <- map["title"]
        autor <- map["autor"]
        genre <- map["genre"]
    }
}

Now it is possible to store all received book objects instantly into Realm. Setting the update option of the add write transition true, ensures that existing book items will simply be updated and new book items will be created and added to Realm.
Realm detects existing book objects with a primary key and in this case we use the server id of the book items.

Alamofire.request(.GET,url)  
         .responseArray { (response: Response<[Book], NSError>) in
         switch response.result {
         case .Success(let books):
            do {
               let realm = try Realm()
               try realm.write {
                  for book in books {
                     realm.add(book,update: true)
                  }
                }
             } catch let error as NSError {
               //TODO: Handle error
             }
         case .Failure(let error):
         //TODO: Handle error
     }
  }

Using Generics

If you look at the Get Request more closely, you can see that Alamofire only requires a URL string and the AlamofireObjectMapper response handler requires only a class type that follows the Mappable protocol, which is a perfect case for using generics. We can do multiple GET requests with different response Objects by changing a few lines to avoid redundant code. Therefore, all Realm object models have to follow the Meta protocol to provide the URL.

protocol Meta {  
    static func url()->String
}
//...
class Book:Object,Mappable,Meta {  
    dynamic var id = 0
    dynamic var title = ""
    dynamic var autor = ""
    dynamic var genre = ""

    override static func primaryKey() -> String? {
        return "id"
    }

    //Impl. of Mappable protocol
    required convenience init?(_ map: Map) {
        self.init()
    }

    func mapping(map: Map) {
        id    <- map["id"]
        title <- map["title"]
        autor <- map["autor"]
        genre <- map["genre"]
    }

    //Impl. of Meta protocol
    static func url()->String {
        return "https://bitbucket.org/hyphe/blog-examples/raw/master/fetchData/demoapi/books"
    }
}

The final GET request looks like this:

class FetchData {  
   static func get <T:Object where T:Mappable,T:Meta> (type:T.Type,success:()->Void,fail:(error:NSError)->Void)->Void {
      Alamofire.request(Method.GET, type.url())
        .responseArray { (response: Response<[T], NSError>) in
           switch response.result {
              case .Success(let item):
                 do {
                    let realm = try Realm()
                    try realm.write {
                    for item in items {
                       realm.add(item, update: true)
                    }
                 } catch let error as NSError {
                    fail(error:error)
                 }
                 success()
            case .Failure(let error):
               fail(error:error)
         }
      }
   }
}

This GET request can only create new or update existing items in the Realm database. It does not delete Realm objects that were deleted from the API.
This case must be separately treated depending on the API.

You can find a demo project on the following bitbucket repository.
Example

As always, feel free to leave suggestions in the comment section below.

comments powered by Disqus