Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Saving array to CoreData Swift

I would like to save this kind of arrays with Core Data:

let crypto1 = Cryptos(name: "Bitcoin", code: "bitcoin", symbol: "BTC", placeholder: "BTC Amount", amount: "0.0")
let crypto2 = Cryptos(name: "Bitcoin Cash", code: "bitcoinCash", symbol: "BCH", placeholder: "BCH Amount", amount: "0.0")

Is that even possible?

I know I can create an array to save like that...

let name = "Bitcoin"
let code = "bitcoin"
let symbol = "BTC"
let placeholder = "BTC Amount"
let amount = "0.0"
let cryptos = CryptoArray(context: PersistenceService.context)
cryptos.name = name
cryptos.code = code
cryptos.symbol = symbol
cryptos.placeholder = placeholder
cryptos.amount = amount
crypto.append(cryptos)
PersistenceService.saveContext()

...but this seems pretty inconvenient when a theoretical infinite number of arrays will be created by the user.

What would be the best way for me to save data, load it, edit it and delete it?

like image 413
Wizzardzz Avatar asked Dec 11 '22 07:12

Wizzardzz


1 Answers

This is a question for a tutorial rather than a straight forward answer. I suggest you give some time to read about CoreData. Having said that, your question sounds generic, "Saving array to CoreData in Swift", so I guess it doesn't hurt to explain a simple implementation step by step:

Step 1: Create your model file (.xcdatamodeld)

In Xcode, file - new - file - iOS and choose Data Model

Step 2: Add entities

Select the file in Xcode, find and click on Add Entity, name your entity (CryptosMO to follow along), click on Add Attribute and add the fields you like to store. (name, code, symbol... all of type String in this case). I'll ignore everything else but name for ease.

Step 3 Generate Object representation of those entities (NSManagedObject)

In Xcode, Editor - Create NSManagedObject subclass and follow the steps.

Step 4 Lets create a clone of this subclass

NSManagedObject is not thread-safe so lets create a struct that can be passed around safely:

struct Cryptos {
    var reference: NSManagedObjectID! // ID on the other-hand is thread safe. 

    var name: String // and the rest of your properties
} 

Step 5: CoreDataStore

Lets create a store that gives us access to NSManagedObjectContexts:

class Store {
    private init() {}
    private static let shared: Store = Store()

    lazy var container: NSPersistentContainer = {

        // The name of your .xcdatamodeld file.
        guard let url = Bundle().url(forResource: "ModelFile", withExtension: "momd") else {
            fatalError("Create the .xcdatamodeld file with the correct name !!!")
            // If you're setting up this container in a different bundle than the app,
            // Use Bundle(for: Store.self) assuming `CoreDataStore` is in that bundle.
        }
        let container = NSPersistentContainer(name: "ModelFile")
        container.loadPersistentStores { _, _ in }
        container.viewContext.automaticallyMergesChangesFromParent = true

        return container
    }()

    // MARK: APIs

    /// For main queue use only, simple rule is don't access it from any queue other than main!!!
    static var viewContext: NSManagedObjectContext { return shared.container.viewContext }

    /// Context for use in background.
    static var newContext: NSManagedObjectContext { return shared.container.newBackgroundContext() }
}

Store sets up a persistent container using your .xcdatamodeld file.

Step 6: Data source to fetch these entities

Core Data comes with NSFetchedResultsController to fetch entities from a context that allows extensive configuration, here is a simple implementation of a data source support using this controller.

class CryptosDataSource {

    let controller: NSFetchedResultsController<NSFetchRequestResult>
    let request: NSFetchRequest<NSFetchRequestResult> = CryptosMO.fetchRequest()

    let defaultSort: NSSortDescriptor = NSSortDescriptor(key: #keyPath(CryptosMO.name), ascending: false)

    init(context: NSManagedObjectContext, sortDescriptors: [NSSortDescriptor] = []) {
        var sort: [NSSortDescriptor] = sortDescriptors
        if sort.isEmpty { sort = [defaultSort] }

        request.sortDescriptors = sort

        controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
    }

    // MARK: DataSource APIs

    func fetch(completion: ((Result) -> ())?) {
        do {
            try controller.performFetch()
            completion?(.success)
        } catch let error {
            completion?(.fail(error))
        }
    }

    var count: Int { return controller.fetchedObjects?.count ?? 0 }

    func anyCryptos(at indexPath: IndexPath) -> Cryptos {
        let c: CryptosMO = controller.object(at: indexPath) as! CryptosMO
        return Cryptos(reference: c.objectID, name: c.name)
    }
}

All we need from an instance of this class is, number of objects, count and item at a given indexPath. Note that the data source returns the struct Cryptos and not an instance of NSManagedObject.

Step 7: APIs for add, edit and delete

Lets add this apis as an extension to NSManagedObjectContext: But before that, these actions may succeed or fail so lets create an enum to reflect that:

enum Result {
    case success, fail(Error)
} 

The APIs:

extension NSManagedObjectContext {

    // MARK: Load data

    var dataSource: CryptosDataSource { return CryptosDataSource(context: self) }

    // MARK: Data manupulation

    func add(cryptos: Cryptos, completion: ((Result) -> ())?) {
        perform {
            let entity: CryptosMO = CryptosMO(context: self)
            entity.name = cryptos.name
            self.save(completion: completion)
        }
    }

    func edit(cryptos: Cryptos, completion: ((Result) -> ())?) {
        guard cryptos.reference != nil else { 
            print("No reference")
            return 
        }
        perform {
            let entity: CryptosMO? = self.object(with: cryptos.reference) as? CryptosMO
            entity?.name = cryptos.name
            self.save(completion: completion)
        }
    }

    func delete(cryptos: Cryptos, completion: ((Result) -> ())?) {
        guard cryptos.reference != nil else { 
            print("No reference")
            return 
        }
        perform {
            let entity: CryptosMO? = self.object(with: cryptos.reference) as? CryptosMO
            self.delete(entity!)
            self.save(completion: completion)
        }
    }

    func save(completion: ((Result) -> ())?) {
        do {
            try self.save()
            completion?(.success)
        } catch let error {
            self.rollback()
            completion?(.fail(error))
        }
    }
}

Step 8: Last step, use case

To fetch the stored data in main queue, use Store.viewContext.dataSource. To add, edit or delete an item, decide if you'd like to do on main queue using viewContext, or from any arbitrary queue (even main queue) using newContext or a temporary background context provided by Store container using Store.container.performInBackground... which will expose a context. e.g. adding a cryptos:

let cryptos: Cryptos = Cryptos(reference: nil, name: "SomeName")
Store.viewContext.add(cryptos: cryptos) { result in
   switch result {
   case .fail(let error): print("Error: ", error)
   case .success: print("Saved successfully")
   }
}

Simple UITableViewController that uses the cryptos data source:

class ViewController: UITableViewController {

    let dataSource: CryptosDataSource = Store.viewContext.dataSource

    // MARK: UITableViewDataSource

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource.count
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return tableView.dequeueReusableCell(withIdentifier: "YourCellId", for: indexPath)
    }
    override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        let cryptos: Cryptos = dataSource.anyCryptos(at: indexPath)
        // TODO: Configure your cell with cryptos values.
    }
}
like image 102
Lukas Avatar answered Dec 26 '22 08:12

Lukas