Even though you might be an iOS expert, almost every application requires some kind of backend storage. Baasic provides a standard set of backend features found in various BaaS solutions, content management systems and modern application frameworks. This time we are going to focus on Dynamic Resources.

Let’s see how to use Dynamic Resources as the backend storage for our Todo items which we’ll be able to query, create, edit, and delete.

Before we get started, we need to setup a new Baasic application.

Dynamic Resources

Dynamic resources are a storage mechanism for your custom data objects, enabling you to make them first-class citizens in your app. This means you can generate any number of different data objects on which you can perform any CRUD operation while including support for filtering, sorting, paging, fine-grained validation using HTTP calls or directly in the Baasic dashboard. You also get fully-featured authorization infrastructure that provides role and user based access control for all resources. More about Dynamic Resources can be found in this post.

Now that we know what Dynamic Resources can do for us, let’s use them for our Todo data model. Data object is created using a specified Schema (a regular JSON schema). Navigate to the dashboard (by simply clicking on the application you’ve created) and click Add New Schema. Name your schema Todo. You can generate JSON schema using object designer or by directly entering it in the schema editor. To save you some time, scroll down to schema definition and paste this schema:

{
  "type": "object",
  "properties": {
    "id": {
        "type": "string",
        "title": "Unique Identifier",
        "hidden": true,
        "readonly": true
    },
    "title": {
        "type": "string",
    },
    "isComplete": {
        "type": "boolean"
    },
    "description": {
        "type": "string"
    },
    "scheduledDate": {
        "type": "string"
    }
  }
}

The great thing about Dynamic Resources is that they can be added not only via REST API but also directly from the Baasic dashboard. Click on Dynamic Resources, select Todo scheme from the dropdown and click Add New Resource. For the sake of simplicity, we won’t be using authentication, so we need to enable access to Baasic application end-points to everyone. Click on Resource Permissions:

Dynamic Resource Permissions

and check all Anonymous role permissions.

That’s basically it; we have our Todo Baasic Dynamic Resource Schema ready and we can start building our Todo iOS application.


iOS Application in Swift

Our main goal is to setup a generic service in Swift that will send HTTP calls to Baasic REST API Dynamic Resource end-point and to be able to perform CRUD operations as well as filtering, sorting and paging. You’ll be able to reuse the code from this post for any custom schema that you create in the future.

Third party libraries being used:

  • Alamofire - HTTP networking library
  • ObjectMapper - provides a way to convert your model objects (classes and structures) to and from JSON

Alamofire is the most commonly used networking library for making HTTP calls, so we wanted to make the code as much familiar as possible, while ObjectMapper makes it easier to convert JSON objects to models in a generic way.

Let’s start off by creating our base definition and implementation of a Dynamic Resource model:

import ObjectMapper

public protocol ResponseSerializable : Mappable {
    init()
}

public protocol DynamicModel : ResponseSerializable {
    init()
    
    var id: String { get set }
    
    static var schemaName: String { get }
}

public class DynamicModelBase : DynamicModel {
    
    public var id: String = ""
    
    public required init() {
        
    }
    
    required public init?(map: Map) {
        
    }
    
    public func mapping(map: Map) {
        id <- map["id"]
    }
    
    public class var schemaName: String {
        fatalError("override in subclass")
    }
}

ResponseSerializable is just a wrapper around Mappable protocol of ObjectMapper, so we don’t depend directly on ObjectMapper - if we would replace it with some other library or our own code.

Now that we have the base resource model, we can create our Todo model.

import ObjectMapper

public class TodoModel : DynamicModelBase {
    public var title: String = ""
    public var description: String = ""
    public var isComplete: Bool = false
    public var scheduledDate: Date = Date()
    
    required public init() {
        super.init()
    }
    
    public required init?(map: Map) {
        super.init(map: map)
    }
    
    public override func mapping(map: Map) {
        super.mapping(map: map)
        
        title <- map["title"]
        description <- map["description"]
        isComplete <- map["isComplete"]
        scheduledDate <- (map["scheduledDate"], DateFormatTransform())
    }

    public override class var schemaName: String {
        return "todo"
    }
}

Very important thing to note here is the override of schemaName which has to be the same as the name of our Dynamic Resource Schema that we want to use (todo in our case).

Swift Dynamic Resource Client

The most important part of this application is the dynamic resource client. Once we implement this client, we’ll only need to create a new model that implements DynamicModelBase (just like we did with TodoModel) and everything will be setup for making HTTP calls to the Baasic REST API.

First, we need to define how our response will look. BaasicResponse will be generic enum - it returns a value of the specified generic type. If an error occurs it will return error with appropriate status code.

public enum BaasicResponse<T> {
    case success(T)
    case failure(Error, Int?)
}

Baasic end-point returns requested collections in this format:

{
    "item": [],
    "page": 1,
    "recordsPerPage": 10,
    "searchQuery": null,
    "sort": null,
    "totalRecords": 2
}
  • item - array of requested objects
  • page - current page that is requested
  • recordsPerPage - number of records per page
  • searchQuery - search query string used for filtering
  • sort - field by which we are sorting the data
  • totalRecords - total number of records based on the filter

so we need to define the corresponding Swift model:

import ObjectMapper

public class CollectionModelBase<T: DynamicModel> : ResponseSerializable {
    public var item: [T] = []
    public var page: Int = 0
    public var recordsPerPage: Int = 0
    public var searchQuery: String = ""
    public var sort: String = ""
    public var totalRecords: Int = 0

    public required init() { }
    
    public required init?(map: Map) { }
    
    public func mapping(map: Map) {
        item <- map["item"]
        links <- map["links"]
        page <- map["page"]
        recordsPerPage <- map["recordsPerPage"]
        searchQuery <- map["searchQuery"]
        sort <- map["sort"]
        totalRecords <- map["totalRecords"]
    }
}

The last thing before we create our client is to define a filtering object that will help us generate query parameters for filtering:

public class FilterParameters : ResourceFilterable {
    
    private let DefaultSearchQuery = ""
    private let DefaultPage = 1
    private let DefaultRpp = 10
    private let DefaultSort = ""
    private let DefaultEmbed = ""
    private let DefaultFields = ""
    
    public var searchQuery: String    
    public var page: Int    
    public var rpp: Int    
    public var sort: String    
    public var fields: String
    
    init() {
        self.searchQuery = DefaultSearchQuery
        self.page = DefaultPage
        self.rpp = DefaultRpp
        self.sort = DefaultSort
        self.fields = DefaultFields
    }
    
    public func getQueryParameters() -> [String: Any] {
        var query: [String : Any] = [
            "page" : page,
            "rpp" : rpp,
            ]
        
        if searchQuery != DefaultSearchQuery {
            query["searchQuery"] = searchQuery
        }
        
        if sort != DefaultSort {
            query["sort"] = sort
        }
        
        if fields != DefaultFields {
            query["fields"] = fields
        }
        
        return query
    }
}

Everything is ready to setup the main client contract:

public protocol DynamicResourceRequestable {
    associatedtype T : DynamicModel
    
    func delete(id: String, completion: @escaping (BaasicResponse<Bool>) -> Void)
    
    func find(filter: ResourceFilterable, completion: @escaping (BaasicResponse<CollectionModelBase<T>>) -> Void)    
    func find(schemaName: String, filter: ResourceFilterable, completion: @escaping (BaasicResponse<CollectionModelBase<T>>) -> Void)
    
    func get(id: String, fields: String, completion: @escaping (BaasicResponse<T>) -> Void)    
    func get(schemaName: String, id: String, fields: String, completion: @escaping (BaasicResponse<T>) -> Void)
    
    func insert(resource: T, completion: @escaping (BaasicResponse<T>) -> Void)    
    func insert(schemaName: String, resource: T, completion: @escaping (BaasicResponse<T>) -> Void)
    
    func update(resource: T, completion: @escaping (BaasicResponse<Bool>) -> Void)    
    func update(schemaName: String, resource: T, completion: @escaping (BaasicResponse<Bool>) -> Void)
}

Note that every object that is set as a generic argument of this protocol must implement DynamicModel protocol which we defined earlier.

Finally, here is the DynamicResourceRequestable implementation that can manage any resource you define.

import Alamofire
import ObjectMapper

public class DynamicResourceClient<T : DynamicModel> : DynamicResourceRequestable {
    
    private let moduleRelativePath = "resources"
    
    private let sessionManager: SessionManager
    private let configuration: BaasicConfigurable
    
    convenience init(configuration: BaasicConfigurable) {
        self.init(sessionManager: SessionManager.default, configuration: configuration)
    }
    
    init(sessionManager: SessionManager,
         configuration: BaasicConfigurable)
    {
        self.sessionManager = sessionManager
        self.configuration = configuration
    }
 
    public func delete(id: String, completion: @escaping (BaasicResponse<Bool>) -> Void) {
        let url = self.getDynamicResourceApiUrl(T.schemaName, id: id)
        
        self.sessionManager.request(url, method: .delete)
            .validate()
            .responseData { response in
                switch response.result {
                case .success(_):
                    completion(.success(true))
                    break
                case .failure(let error):
                    completion(.failure(error, response.response?.statusCode))
                    break
                }
            }
    }
    
    public func find(filter: ResourceFilterable, completion: @escaping (BaasicResponse<CollectionModelBase<T>>) -> Void) {
        self.find(schemaName: T.schemaName, filter: filter, completion: completion)
    }
    
    public func find(schemaName: String, filter: ResourceFilterable, completion: @escaping (BaasicResponse<CollectionModelBase<T>>) -> Void) {
        let url = self.getDynamicResourceApiUrl(schemaName)
        let parameters = filter.getQueryParameters()
        
        self.sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default)
            .validate()
            .responseObject { (response: DataResponse<CollectionModelBase<T>>) in
                switch response.result {
                case .success(let result):
                    completion(.success(result))
                    break
                case .failure(let error):
                    completion(.failure(error, response.response?.statusCode))
                    break
                }
            }
    }
    
    public func get(id: String, fields: String, completion: @escaping (BaasicResponse<T>) -> Void) {
        self.get(schemaName: T.schemaName, id: id, fields: fields, completion: completion)
    }
    
    public func get(schemaName: String, id: String, fields: String, completion: @escaping (BaasicResponse<T>) -> Void) {
        let url = self.getDynamicResourceApiUrl(T.schemaName, id: id)
        
        self.sessionManager.request(url, method: .get)
            .validate()
            .responseObject(completionHandler: { (response: DataResponse<T>) in
                switch response.result {
                case .success(let value):
                    completion(.success(value))
                    break
                case .failure(let error):
                    completion(.failure(error, response.response?.statusCode))
                    break
                }
            })
    }
    
    public func update(resource: T, completion: @escaping (BaasicResponse<Bool>) -> Void) {
        self.update(schemaName: T.schemaName, resource: resource, completion: completion)
    }
    
    public func update(schemaName: String, resource: T, completion: @escaping (BaasicResponse<Bool>) -> Void) {
        let url = self.getDynamicResourceApiUrl(T.schemaName, id: resource.id)
        
        let json = resource.toJSON()
        
        self.sessionManager.request(url, method: .put, parameters: json, encoding: JSONEncoding.default)
            .validate()
            .responseData(completionHandler: { response in
                switch response.result {
                case .success(_):
                    completion(.success(true))
                    break
                case .failure(let error):
                    completion(.failure(error, response.response?.statusCode))
                    break
                }
            })
    }
    
    public func insert(resource: T, completion: @escaping (BaasicResponse<T>) -> Void) {
        self.insert(schemaName: T.schemaName, resource: resource, completion: completion)
    }
    
    public func insert(schemaName: String, resource: T, completion: @escaping (BaasicResponse<T>) -> Void) {
        let url = self.getDynamicResourceApiUrl(schemaName)
        let json = resource.toJSON()
        
        self.sessionManager.request(url, method: .post, parameters: json, encoding: JSONEncoding.default)
            .validate()
            .responseObject(completionHandler: { (response: DataResponse<T>) in
                switch response.result {
                case .success(let result):
                    completion(.success(result))
                    break
                case .failure(let error):
                    completion(.failure(error, response.response?.statusCode))
                    break
                }
            })
    }
    
    private func getDynamicResourceApiUrl(_ schemaName: String, id: String) -> String {
        let url = self.getApiUrl(ssl: true, relativeUrl: "\(moduleRelativePath)/\(schemaName)/")
        return url + id
    }
    
    private func getDynamicResourceApiUrl(_ schemaName: String) -> String {
        return self.getDynamicResourceApiUrl(schemaName, id: "")
    }
    
    private func getApiUrl(relativeUrl: String) -> String {
        return getApiUrl(ssl: true, relativeUrl: relativeUrl)
    }
    
    private func getApiUrl(ssl: Bool, relativeUrl: String) -> String {
        let baseAddress = ssl ? self.configuration.secureBaseAddress.trimEnd("/") : self.configuration.baseAddress.trimEnd("/")
        let relative = self.configuration.applicationIdentifier.trimEnd("/") + "/" + relativeUrl
        return baseAddress + "/" + relative
    }
}

As you can see, our DynamicResourceClient<T> is dependent on SessionManager and BaasicConfigurable which is Baasic configuration that specifies things like baseAddress, requestTimeout, applicationIdentifier and other things. Currently, the client is using only end-point address and application identifier.

How do we use it?

You can use it directly like this:

private let dynamicResourceClient: DynamicResourceClient<TodoModel> = DynamicResourceClient(configuration: BaasicConfiguration(applicationIdentifier: "todo"))

public func fetchData() {
    let params = FilterParameters()
    params.sort = "title"
    params.page = self.currentPage
    params.rpp = self.recordsPerPage
    
    self.dynamicResourceClient.find(filter: params, completion: { (response: BaasicResponse<CollectionModelBase<TodoModel>>) in
        switch response {
        case .success(let value):
            self.todos = value.item
            self.refreshTableData()
            break
        case .failure(let error, let statusCode):
            self.displayErrorMessage(errorType: .response(error, statusCode))
            break
        }
    })
}

We just had to inject configuration with applicationIdentifier - the Baasic application identifier which we defined when we created the application (note that this is not the Dynamic Resource Schema name, they just happened to be the same in our case). That way, if we want to use another Baasic application that we’ve created, we would just have to change this identifier to the specified value.

A better practice would be to create a specific service for each schema that would wrap DynamicResourceClient with the specified model, so that you can implement any custom business logic related to your resource.

We didn’t want to put any unnecessary code that’s related only to views and project setup. Here are a few screenshots of the finished application which you can find here:

Todo Example 1

And that’s about it. We’ve created full Swift implementation of Baasic Dynamic Resources. Now you can use this code for any schema you create in the future. Please let us know how you like it.

Feel free to leave a comment

comments powered by Disqus