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"
On the next screen, tap the "Create" button.
- Next step is to create a
podfile
to pull appropriate dependencies. Since this project will needMantle
and Object adapter to allow communication between Core data and Mantle, we will also add MTLManagedObjectAdapter as a dependency in ourpodfile
. 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 asProduct
. Add few attributes to it as follows:- listPrice -
Double
- availability -
String
- averageOverallRating -
Double
- categoryIdentifier -
String
- imageURL -
String
- listPrice -
- We want
Product
object to act as aMantle
object as well as core data model. We will useMantle
in conjunction withMTLManagedObjectAdapter
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 asMantle
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 fromviewController
'sviewDidLoad()
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
- Second time onwards, you can instead pass the category identifier and it will then load matching record directly from the 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 runpod install
before running it since I haven't committed localPods
directory to repository.
As usual, if you have any other questions feel free to reach to me through an email or over the Twitter