Core Data with Mantle in Swift

Today we are going to talk about how I used Core data in conjunction with Mantle wrapper to directly store and retrieve data out of database without directly interacting with Core data. This involves the significant portion of MTLManagedObjectAdapter object which is a wrapper around core data. All we have to do is take a Mantle object and pass to wrapper. Wrapper will take care of storing data into database. Same philosophy goes while retrieving data from the database.

Please be aware that I am still using Swift 2.3 for this project. If you are planning to use this example on your Swift 3.0 project, please make sure to update desired dependencies and code as needed

Let's start from the beginning. I am assuming you're following me with directions below

  • Create a new project say, CoreDataPractice.

  • Go to File -> New -> Project -> Single View Application -> Next -> Type CoreDataPractice

Make sure to check the box which says "Use Core Data"

use_core_data_option

On the next screen, tap the "Create" button.

  • Next step is to create a podfile to pull appropriate dependencies. Since this project will need Mantle and Object adapter to allow communication between Core data and Mantle, we will also add MTLManagedObjectAdapter as a dependency in our podfile. Our podfile will look like this

platform :ios, ‘8.0’
inhibit_all_warnings!
use_frameworks!

xcodeproj 'CoreDataPractice'

def shared_pods    
    pod 'Mantle', '~> 2.0'    
    pod 'MTLManagedObjectAdapter', '~> 1.0'
end

target 'CoreDataPractice' do
    shared_pods
end

Save the podfile and run pod install command from the directory where this podfile is stored. This will install all required dependencies. Now close the xcodeproj and open the newly generated CoreDataPractice.xcworkspace file.

  • Let's create a core data object and add it to pre-generated CoreDataPractice.xcdatamodeld file. Let's call the object as Product. Add few attributes to it as follows:

    • listPrice - Double
    • availability - String
    • averageOverallRating - Double
    • categoryIdentifier - String
    • imageURL - String

product_core_data_model

  • We want Product object to act as a Mantle object as well as core data model. We will use Mantle in conjunction with MTLManagedObjectAdapter to make it available for the core data storage

With this in mind, let's create a Product class which adheres to following 3 protocols.

  • MTLModel
  • MTLJSONSerializing
  • MTLManagedObjectSerializing

First 2 protocols are used for converting external JSON to Mantle object model. The last protocol is used for bridging Mantle object into Core data entity. The class will look like this.

For more clarity, I have sprinkled comments throughout the code instead of explaining the source outside code context


import CoreData
import Foundation
import MTLManagedObjectAdapter
import Mantle

// An enums to store the availability status for the given product. It gets converted from values true/false to Available/Unavailable.
enum Availability: String {
    case Available
    case Unavailable
}

// Product conforms to these 3 protocols required for converting JSON to Mantle object and then to core data entity.
class Product: MTLModel, MTLJSONSerializing, MTLManagedObjectSerializing {
    var categoryIdentifier: String = ""
    var averageOverallRating: NSNumber = 0
    var availability: String = Availability.Unavailable.rawValue
    var imageURL: NSURL? = nil
    var listPrice: NSNumber = 0
    
    // JSON keys paths for automatically converting incoming JSON into Mantle object with corresponding keys.
    static func JSONKeyPathsByPropertyKey() -> [NSObject : AnyObject]! {
        return ["averageOverallRating": "average_overall_rating",
        "availability": "has_stock",
        "imageURL": "image_url",
        "listPrice": "list_price",
        "categoryIdentifier": "category_id"]        
    }
    
    // A transformer function to convert incoming imageURL string into Mantle imageURL object which is of type NSURL.
    static func imageURLJSONTransformer() -> NSValueTransformer {
        return NSValueTransformer(forName: MTLURLValueTransformerName)!
    }
    
    // A transformer function to convert bool availability values into corresponding enum Available/Unavailable respectively.
    static func availabilityJSONTransformer() -> NSValueTransformer {
        return NSValueTransformer.mtl_valueMappingTransformerWithDictionary([true: "Available", false: "Unavailable"])
    }
    
    // MARK: MTLManagedObjectSerializing protocol method. This tells the Mantle the name of core data entity corresponding to Mantle object. Since we used the same entity name for both Mantle and Core data, we will return Product object back.
    static func managedObjectEntityName() -> String! {
        return "Product"
    }
    
    // For mapping Mantle keys to Core data object model keys.
    static func managedObjectKeysByPropertyKey() -> [NSObject : AnyObject]! {
        return ["categoryIdentifier": "categoryIdentifier",
                "availability": "availability",
                "averageOverallRating": "averageOverallRating",
                "imageURL": "imageURL",
                "listPrice": "listPrice"]
    }
    
    // An inverse transform to oncvert imageURL object which if of NSURL into Strign object.
    static func imageURLEntityAttributeTransform() -> NSValueTransformer {        
        return NSValueTransformer(forName: MTLURLValueTransformerName)!.mtl_invertedTransformer()
    }    
}
  • For the sake of server response I will not dig much into making and sending request since it falls outside the scope of this post. We will use the data fetched from local JSON file. This JSON will be converted to Mantle model and eventually to Core data object model with same name as Mantle object model - Product

This JSON file will looks like this


{
    "products": [{
                 "average_overall_rating": 4.5,
                 "has_stock": true,
                 "image_url": "http://www.sample.com/image1.jpg",
                 "list_price": 45.6,
                 "category_id": "3455"
                 }, {
                 "average_overall_rating": 5.0,
                 "has_stock": false,
                 "image_url": "http://www.sample.com/image2.jpg",
                 "list_price": 234.4,
                 "category_id": "4545"
                 }, {
                 "average_overall_rating": 3.8,
                 "has_stock": true,
                 "image_url": "http://www.sample.com/image2.jpg",
                 "list_price": 300.1,
                 "category_id": "1299"
                 }]
}

For the sake of space and content length, I will not enlist the function used to read data from local JSON file. This will be included in the Github project instead. In this post I will mainly concentrate of Core data and Mantle models generation part

  • Once JSON is ready and our Mantle scaffold model is created, now it's the time to fetch data from local JSON file, convert it into Mantle objects and store in the Core data using a wrapper. I have also sprinkled the code with intermittent comments.

import UIKit
import Mantle
// An adapter which acts as a bridge between Mantle and Core Data.
import MTLManagedObjectAdapter

enum ProductStorageIndicatorKey: String {
    case ProductStored
}

class ProductsFetcher: NSObject {
    // We will fetch the list of products with given category identifier.
    func fetchProducts() -> [Product] {
        // For the sake of this example project, we will read the data from local JSON file.        
        if let products = JSONReader.readJSONFromFileWith("Products") as? [String: AnyObject] {
            if let listOfProducts = products["products"] as? [[String: AnyObject]] {
                do {
                    // First off, convert JSON dictionaries into Mantle model objects.
                    if let productsCollection = try MTLJSONAdapter.modelsOfClass(Product.self, fromJSONArray: listOfProducts) as? [Product] {
                        // Take Mantle objects as an input and store it into Core data as NSManagedObject models.
                        return self.objectsStoredToDatabaseWithProducts(productsCollection)
                    }
                } catch let error as NSError {
                    print("Failed to fetch and create models from products from local JSON resource. Failed with error \(error.localizedDescription)")
                }
            }
        }
        return []
    }

    func objectsStoredToDatabaseWithProducts(products: [Product]) -> [Product] {

        // This is a shared ManagedObjectContext taken directly from AppDelegate. Instead of using it as a global variable, you might want to do dependency injection.
        let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate
        let managedContext = appDelegate!.managedObjectContext

        for product in products {
            do {
                // Conversion from Mantle to Core Data realm.
                try MTLManagedObjectAdapter.managedObjectFromModel(product, insertingIntoContext: managedContext)
            } catch let error as NSError {
                print(error.localizedDescription)
            }
        }
        do {
            // Save the managedobject context for persistence.
            try managedContext.save()
            NSUserDefaults.standardUserDefaults().setBool(true, forKey: ProductStorageIndicatorKey.ProductStored.rawValue)
        } catch let error as  NSError {
            print("Error in saving the managed context \(error.localizedDescription)")
        }
        return self.fetchProductsWith("")
    }

    // We will take catgory identifier as an input and output all products matching with that category identifier.
    func fetchProductsWith(categoryIdentifier: String) -> [Product] {
        let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate
        let managedContext = appDelegate!.managedObjectContext
        // Specify the entity name which records will be extracted from.
        let fetchRequest   = NSFetchRequest(entityName: "Product")
        // Predicate to output only those products matching input categoryIdentifier.
        if categoryIdentifier.characters.count > 0 {
            let predicate = NSPredicate(format: "categoryIdentifier == %@", categoryIdentifier)
            fetchRequest.predicate = predicate
        }

        do {
            let fetchedResult = try managedContext.executeFetchRequest(fetchRequest) as? [NSManagedObject]

            var records: [NSManagedObject] = []
            if let results = fetchedResult {
                records = results
            }


            var tempProducts: [Product] = []

            // Convert Core data models into Mantle counterparts.
            for record in records {
                do {
                    if let productModel = try MTLManagedObjectAdapter.modelOfClass(Product.self, fromManagedObject: record) as? Product {
                        tempProducts.append(productModel)
                    }
                } catch let error as NSError {
                    print("Failed to convert NSManagedobject to Model Object. Failed with error \(error.localizedDescription)")
                }
            }
            return tempProducts
        } catch let error as NSError {
            print("Error occurred while fetching products with category identifier \(categoryIdentifier). Failed with error \(error.localizedDescription)")

        }
        // Return an empty array in case error occurs while retriving records.
        return []
    }
}

This should be enough code as long as fetching external JSON and storing it into Mantle and Core data models is concerned. The beauty of this approach is ManagedObjectAdapter takes care of most of the complexity associated with core data and client, once the wrapper is written has little to worry about how core data works under the hood.

  • Now let's run the project, store some records and verify everything works as expected. As mentioned before we will read the data from local file and store it into Core data storage. This will happen for the first time. Next time onwards, data will be read from local Core data resource instead of going back to reading local JSON file

    We will call this method a loadData() and it will be called from viewController's viewDidLoad() method


override func viewDidLoad() {
    super.viewDidLoad()
    // Load the data either from file or Core database storage.
    loadData()
}

func loadData() {
    let productsFetcher = ProductsFetcher()
    var products: [Product] = []
    // Check if data has already been stored in the database. If yes, retrieve the specific record
    if NSUserDefaults.standardUserDefaults().boolForKey(ProductStorageIndicatorKey.ProductStored.rawValue) == true {
        products = productsFetcher.fetchProductsWith("3455")
    } else {
        // If data is not present, read if from the file.
        products = productsFetcher.fetchProducts()
    }
    // Print the Debug information.
    print("Products Count \(products.count)")
    print(products)
}
  • As you will see, the first time it will go to fetch products from local file

fetching_from_local_file

  • Second time onwards, you can instead pass the category identifier and it will then load matching record directly from the core data

fetching_from_core_data

This is indeed a simplistic example to prove the point of integration of Core data with Mantle. You can of course do more complex things with Mantle framework and Core data as it is suitable to your project.

I have uploaded a fully functional sample project CoreDataPractice and it is hosted on the Github. Please make sure to run pod install before running it since I haven't committed local Pods directory to repository.

As usual, if you have any other questions feel free to reach to me through an email or over the Twitter