Persistence 0.1.2

Persistence 0.1.2

Maintained by Cristian Barril.



  • By
  • Cristian Barril

Table of contents

Persistence

Framework to encapsulate persistence logic using Protocol Oriented Programming (POP) and Protocols with Associated Types (PATs). This is an EXAMPLE framework to show how to use:

  • Cocoapods
  • Core Data
  • Realm
  • Protocol Oriented Programming
  • Protocols with Associated Types
  • Dependency injection
  • Unit Testing

Supported databases

  • Core Data
  • Realm

Installation

Using Cocoapods, add in your Podfile:

All databases

pod 'Persistence'

Only Core Data database

pod 'Persistence/CoreData'

Only Realm database

pod 'Persistence/Realm'

How to use

To use this framework, you just have to know a one protocol. This makes easy to change from one database to another.

Protocol

DatabaseProtocol

This is the base protocol that any database must implement. The idea is to provide with the most used functionalities that a database should offer. For any other use, you can recover the database context an implement your custom code.

public protocol DatabaseProtocol {
	...
    func create<ReturnType: DatabaseObjectTypeProtocol>() -> ReturnType?
    func recover<ReturnType: DatabaseObjectTypeProtocol>(key: String, value: String) -> [ReturnType]?
    func delete(_ object: DatabaseObjectType) -> Bool
    func getContext() -> DatabaseContextType
}

Protocol extension

Saveable

Additional method added to DatabaseProtocol by the CoreDataManager:

extension DatabaseProtocol where Self == CoreDataManager {
    public func save() throws {
    	...
    }
}

Updatable

Additional method added to DatabaseProtocol by the RealmManager:

extension DatabaseProtocol where Self == RealmManager {
    public func update<T>(_ object: T) -> Bool where T : DatabaseObjectTypeProtocol {
    	...
    }
}

REMEMBER: This is just an example. Feel free to fork this repo and make your own version.

Builders

To create or recover a database, you have to use the DatabaseBuilder struct specific for the desired database. This struct required to be initialized with all the vital information for each case (Core Data need differents things than Realm)

Core Data Builder

Instantiate this struct to create a Core Data database. You can have as many as you want. Just be sure to use a different "name".

The information you need to provide is:

  • databaseName: String parameter with the name of the database.
  • bundle: Bundle where the NSManagedObjectModel file is located, needed in case of migration.
  • modelURL: URL with the path to the NSManagedObjectModel file.
public struct CoreDataBuilder: CoreDataBuilderProtocol {
    public typealias Database = CoreDataManager
    
    public let databaseName: String
    public let bundle: Bundle
    public let modelURL: URL
        
    public func create() throws -> CoreDataManager {
		...
    }
}

Use example:

let modelURL = Bundle.main.url(forResource: "MyModel", withExtension:"momd")
let databaseBuilder = CoreDataBuilder(databaseName: "CoreDataDatabaseName", bundle: Bundle.main, modelURL: modelURL)
let database: CoreDataManager = try? databaseBuilder.create()

Realm Builder

Instantiate this struct to create a Realm database. You can have as many as you want. Just be sure to use a different "name".

The information you need to provide is:

  • databaseName: String parameter with the name of the database.
  • passphrase: String with the key to encrypt the database. Cannot be an empty String.
  • schemaVersion: UInt64 with the current number of version. Default value is 0. (Optional parameter)
  • migrationBlock: MigrationBlock needed when the model is changed. Default is nil. (Optional parameter)
public struct RealmBuilder: RealmBuilderProtocol {
    public typealias Database = RealmManager
    
    public let databaseName: String
    public let passphrase: String
    public let schemaVersion: UInt64
    public let migrationBlock: MigrationBlock?
    
    public func create() throws -> RealmManager {
	    ...
    }
}

Use example:

let databaseBuilder = RealmBuilder(databaseName: "RealmDatabaseName", passphrase: "Passphrase")
let database: RealmManager = try? databaseBuilder.create()

REMEMBER: This is just an example. Feel free to fork this repo and make your own version.

Use

Common

Creation

Example to create a new User entity:

let newObject: User? = database.create()

Recover

Example to recover all User entities:

let recoveredObjects: [User]? = database.recover()

Example to recover a specific User entity. Can be used with any attribute, that is why it returns an Array. E.g.: return every User with name "John"

let recoveredObjects: [User]? = database.recover(key: "name", value: "John")

Delete

Example to delete a specific User entity. Returns true if the operation succeed or false if not:

let result = database.delete(objectToDelete)

Core Data

Save

Custom Core Data method to save the context:

try? database.save()

Realm

Update

Custom Realm method to update a specific User entity. Returns true if the operation succeed or false if not:

let result = database.update(newObject)

Add Custom Code

To make any other use of the databases, you can recover the context to use it:

let context = database.getContext()

Migrations

Core Data

  • Lightweight migration: just needs to initialize the database as always and the framework will migrate automatically.

  • Heavyweight migration: needs to create, in the same bundle of the database, a NSMappingModel to do it. If needed, in the NSMappingModel you can also setup a class of NSEntityMigrationPolicy type and add custom logic for the migration process.

Realm

When the model changes, you have to follow the next steps:

  • Raise the schema version
  • Create the migration block in the Builder struct. You can see some examples below:
migrationBlock: { migration, oldSchemaVersion in
                if (oldSchemaVersion < 1 && self.lastSchemaVersion >= 1) {
                    print("Automatic migration")
                    // Nothing to do!
                    // Realm will automatically detect new properties and removed properties
                    // And will update the schema on disk automatically
                }
                
                if (oldSchemaVersion < 2 && self.lastSchemaVersion >= 2) {
                    // The enumerateObjects(ofType:_:) method iterates
                    // over every Person object stored in the Realm file
                    migration.enumerateObjects(ofType: Person.className()) { oldObject, newObject in
                        // combine name fields into a single field
                        let firstName = oldObject!["firstName"] as! String
                        let lastName = oldObject!["lastName"] as! String
                        newObject!["fullName"] = "\(firstName) + \(lastName)"
                    }
                }
                
                if (oldSchemaVersion < 3 && self.lastSchemaVersion >= 3) {
                    // The renaming operation should be done outside of calls to `enumerateObjects(ofType: _:)`.
                    migration.renameProperty(onType: Person.className(), from: "age", to: "yearsSinceBirth")
                }
            }