How to get a specific attribute in a core data entity swift - swift

I know there are similiar questions but I have spent hours of trying to find relevant result to my situation without success.
I have got one entity: EntityDate.
The entity has got 2 attributes: date and time
I have a variable "date" that I save as a String to core data like this:
addToCoreData(name: getDateFormatted(), entityName: "EntityDate", key: "date")
func addToCoreData(name: String, entityName: String, key: String) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: entityName, in: context)
let newEntity = NSManagedObject(entity: entity!, insertInto: context)
newEntity.setValue(name, forKey: key)
do {
try context.save()
print("saved")
} catch {
print("Failed saving")
}
}
Later in my code I retrieve the data like this:
var dateFromCD = getCoreData(Entity: "EntityDate")
func getCoreData(Entity: String) -> Array<NSManagedObject>{
var coreData= [NSManagedObject]()
coreData= []
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: Entity)
request.returnsObjectsAsFaults = false
do {
let result = try context.fetch(request)
for data in result as! [NSManagedObject]
{
coreData.append(data)
}
} catch {
print("Failed")
}
return coreData
}
Now I thought I would add a variable time to the seperate attribute "time", and naturally I proceeded by doing like this:
addToCoreData(name: getFormattedTime(), entityName: "EntityDate", key: "time")
The problem I have is that when I call the function
"getCoreData(Entity: "EntityDate")" it gives me this output
[<EntityDate: 0x6000034fc2d0> (entity: EntityDate; id: 0xc229f3dcff76efb1 <x-coredata://2E6F1D46-F49D-436C-9608-FEC59EEB21E6/EntityDate/p18>; data: {
date = "2/21/20";
time = nil;
}), <EntityDate: 0x6000034fc320> (entity: EntityDate; id: 0xc229f3dcff72efb1 <x-coredata://2E6F1D46-F49D-436C-9608-FEC59EEB21E6/EntityDate/p19>; data: {
date = nil;
time = "13:46";
})]
I dont understand the output and I would like to be able to retrieve only date and only time as two seperate variables from the array.

The problem is that you are creating two records, one with the date and the other with the time information instead of one with both date and time.
If you have only one entity anyway take advantage of the generic behavior and write the method more specific like
func createEntityDate(date: String, time: String) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let newEntity = EntityDate(context: context)
newEntity.date = date
newEntity.time = time
do {
try context.save()
print("saved")
} catch {
print("Failed saving", error)
}
}
If you need to modify attributes of an existing record you have to fetch it, modify and save it back.
In any case it's highly recommended to save the whole date as Date and use computed properties to extract the date and time portion.

Related

Swift CoreData - objects not being inserted in to managed object context

I've reworked this question after further research and in response to comments that it was too long.
I am downloading and decoding data, in CSV format using CodableCSV, from three URLs and I've been able to confirm that I am receiving all the data I expect (as of today, 35027 lines). As the data is decoded, I am injecting a NSManagedObjectContext in to the decoded object. Here is my managed object class:
import Foundation
import CoreData
#objc(MacListEntry)
class MacListEntry: NSManagedObject, Decodable {
//var id = UUID()
#NSManaged var registry: String?
#NSManaged var assignment: String?
#NSManaged var org_name: String?
#NSManaged var org_addr: String?
required convenience init(from decoder: Decoder) throws {
guard let keyManObjContext = CodingUserInfoKey.managedObjectContext,
let context = decoder.userInfo[keyManObjContext] as? NSManagedObjectContext,
let entity = NSEntityDescription.entity(forEntityName: "MacListEntry", in: context) else {
fatalError("Failed to receive managed object context")
}
self.init(entity: entity, insertInto: context)
let container = try decoder.container(keyedBy: CodingKeys.self)
self.registry = try container.decode(String.self, forKey: .registry)
self.assignment = try container.decode(String.self, forKey: .assignment)
self.org_name = try container.decode(String.self, forKey: .org_name)
self.org_addr = try container.decode(String.self, forKey: .org_addr)
}
private enum CodingKeys: Int, CodingKey {
case registry = 0
case assignment = 1
case org_name = 2
case org_addr = 3
}
}
public extension CodingUserInfoKey {
static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")
}
I then attempt to save the context using try context.save() but before doing so, examine the numbers of records I am trying to insert using:
print("Deleted objects: (self.persistentContainer.viewContext.deletedObjects.count)")
print("Inserted objects: (self.persistentContainer.viewContext.insertedObjects.count)")
print("Has changes: \(self.persistentContainer.viewContext.hasChanges)")
and get a different number of inserted records every time the code runs - always short, by around 0.5%. I am struggling to understand under what circumstances objects added to a managed object context in this way simply don't appear in the list of inserted objects and don't make it in to the saved database. Is there a practical limit on the number of records inserted in one go?
Can anyone suggest where else I should be looking - the error is tiny enough that it looks like the program is running fine, but it isn't.
Many thanks.
I think I've found the problem and if I'm correct, posting my solution here may help others. I'm downloading the three CSV files using Combine and dataTaskPublisher - creating three separate publishers and then merging them in to a single stream. While I was aware that I couldn't update the UI on a background thread so had added the .receive(on: DispatchQueue.main) in the chain (indicated by (1) in the code below), I put it AFTER the .tryMap { } where the decoding was happening. Because it was the process of decoding that inserted the object in to the managed object context, this must have been happening asynchronously and hence causing problems. By moving the .receive(on: ...) line ABOVE the .tryMap (see (2) below), this appears to have resolved the problem - or at least made the fetch, decode and insert return the correct number of inserted records consistently.
enum RequestError: Error {
case sessionError(error: HTTPURLResponse)
}
struct Agent {
let decoder: CSVDecoder
struct Response<T> {
let value: T
let response: URLResponse
}
func run<T: Decodable>(_ request: URLRequest) -> AnyPublisher<Response<T>, Error> {
print("In run()")
return URLSession.shared
.dataTaskPublisher(for: request)
.receive(on: DispatchQueue.main) // -- (1)
.tryMap { result -> Response<T> in
print(result)
guard let httpResponse = result.response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw RequestError.sessionError(error: result.response as! HTTPURLResponse)
}
let value = try self.decoder.decode(T.self, from: result.data)
return Response(value: value, response: result.response)
}
//.receive(on: DispatchQueue.main). // -- (2)
.eraseToAnyPublisher()
}
}
struct Endpoint {
var agent: Agent
let base: String
}
extension Endpoint {
func run<T: Decodable>(_ request: URLRequest) -> AnyPublisher<T, Error> {
return agent.run(request)
.map(\.value)
.eraseToAnyPublisher()
}
func fetch(thing: String) -> AnyPublisher<[MacListEntry], Error> {
return run(URLRequest(url: URL(string: base+thing)!))
}
}
struct Response: Codable {
let name: String
}
--- snip ---
var requests:[AnyPublisher<[MacListEntry],Error>] = []
requests.append(endpoint.fetch(thing: "file1.csv"))
requests.append(endpoint.fetch(thing: "file2.csv"))
requests.append(endpoint.fetch(thing: "file3.csv"))
let _ = Publishers.MergeMany(requests)
.sink(receiveCompletion: { completion in
...
)
.store(in: &bag)

How to Fetch Multiple Types From CloudKit using CKRecord.ID?

When fetching multiple types from CloudKit using CKRecord.ID I get the following error.
Error
Cannot invoke 'map' with an argument list of type '(#escaping (CKRecord.ID, String, CKAsset, Int) -> Lib)'
CloudKit Fetch Function
static func fetch(_ recordID: [CKRecord.ID], completion: #escaping (Result<[Lib], Error>) -> ()) {
let recordID: [CKRecord.ID] = recordID
let operation = CKFetchRecordsOperation(recordIDs: recordID)
operation.qualityOfService = .utility
operation.fetchRecordsCompletionBlock = { (record, err) in
guard let record = record?.values.map(Lib.init) ?? [] //returns error here
else {
if let err = err {
completion(.failure(err))
}
return
}
DispatchQueue.main.async {
completion(.success(record))
}
}
CKContainer.default().publicCloudDatabase.add(operation)
}
Lib
struct Lib {
var recordID: CKRecord.ID
var name: String
var asset: CKAsset
var rating: Int
}
How can I retrieve multiple types from CloudKit using the CKRecord.ID?
You haven't defined an initializer that accepts a CKRecord.
This will make it compile:
extension Lib {
init(_: CKRecord) { fatalError() }
}
Get rid of your ?? [] and go from there!
It may help you if you use accurate pluralization:
operation.fetchRecordsCompletionBlock = { records, error in
guard let libs = records?.values.map(Lib.init)
The answer from #Jessey was very helpful but kept returning a fatalError(). What ended up working was adding to the init.
Lib
struct Lib {
var recordID: CKRecord.ID
var name: String
var asset: CKAsset
var rating: Int
init(record: CKRecord){
recordID = record.recordID
name = (record["name"] as? String)!
asset = (record["asset"] as? CKAsset)!
rating = (record["rating"] as? Int)!
}
}
The "name", "asset", and "rating" are the custom field names in CloudKit dashboard records. I also got rid of the ?? [] in the fetch function per the instruction.
CloudKit Tutorial: Getting Started was a good reference.

how to get single variable name from struct

I have a core data framework to handle everything you can do with coredata to make it more cooperateable with codable protocol. Only thing i have left is to update the data. I store and fetch data by mirroring the models i send as a param in their functions. Hence i need the variable names in the models if i wish to only update 1 specific value in the model that i request.
public func updateObject(entityKey: Entities, primKey: String, newInformation: [String: Any]) {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityKey.rawValue)
do {
request.predicate = NSPredicate.init(format: "\(entityKey.getPrimaryKey())==%#", primKey)
let fetchedResult = try delegate.context.fetch(request)
print(fetchedResult)
guard let results = fetchedResult as? [NSManagedObject],
results.count > 0 else {
return
}
let key = newInformation.keys.first!
results[0].setValue(newInformation[key],
forKey: key)
try delegate.context.save()
} catch let error {
print(error.localizedDescription)
}
}
As you can see the newInformation param contains the key and new value for the value that should be updated. However, i dont want to pass ("first": "newValue") i want to pass spots.first : "newValue"
So if i have a struct like this:
struct spots {
let first: String
let second: Int
}
How do i only get 1 name from this?
i've tried:
extension Int {
var name: String {
return String.init(describing: self)
let mirror = Mirror.init(reflecting: self)
return mirror.children.first!.label!
}
}
I wan to be able to say something similar to:
spots.first.name
But can't figure out how
Not sure that I understood question, but...what about this?
class Spots: NSObject {
#objc dynamic var first: String = ""
#objc dynamic var second: Int = 0
}
let object = Spots()
let dictionary: [String: Any] = [
#keyPath(Spots.first): "qwerty",
#keyPath(Spots.second): 123,
]
dictionary.forEach { key, value in
object.setValue(value, forKeyPath: key)
}
print(object.first)
print(object.second)
or you can try swift keypath:
struct Spots {
var first: String = ""
var second: Int = 0
}
var spots = Spots()
let second = \Spots.second
let first = \Spots.first
spots[keyPath: first] = "qwerty"
spots[keyPath: second] = 123
print(spots)
however there will be complex (or impossible) problem to solve if you will use dictionary:
let dictionary: [AnyKeyPath: Any] = [
first: "qwerty",
second: 123
]
you will need to cast AnyKeyPath back to WritableKeyPath<Root, Value> and this seems pretty complex (if possible at all).
for path in dictionary.keys {
print(type(of: path).rootType)
print(type(of: path).valueType)
if let writableKeyPath = path as? WritableKeyPath<Root, Value>, let value = value as? Value { //no idea how to cast this for all cases
spots[keyPath: writableKeyPath] = value
}
}

Best way to save many different objects to Core Data

I finished my first app this weekend (thanks to you Stack Overflow users!) and I'm currently in the midst of optimizing the code. I already fixed most of the duplicates and un-safe practices but this one is getting me stuck.
Here is how I save all the informations I need for a given cryptocurrency to Core Data when the user adds it to his wallet:
if addedCrypto == "Augur REP" {
if CoreDataHandler.saveObject(name: "Augur", code: "augur", symbol: "REP", placeholder: "REP Amount", amount: "0.00000000", amountValue: "0.0") {
for _ in CoreDataHandler.fetchObject()! {
}
}
}
This is pretty handy for one crypto, but my app supports 25 of them. Currently the above lines are duplicated 24 more times in my code, once for each different crypto.
I thought about using a dictionary where I could save Augur REP as a key and then (name: "Augur", code: "augur", ...") as a value but I couldn't figure out how to properly to do it..
What could be the solution here?
EDIT: Here is the saveObject(...) method:
class func saveObject(name:String, code:String, symbol:String, placeholder:String, amount:String, amountValue:String) -> Bool {
let context = getContext()
let entity = NSEntityDescription.entity(forEntityName: "CryptosMO", in: context)
let managedObject = NSManagedObject(entity: entity!, insertInto: context)
managedObject.setValue(name, forKey: "name")
managedObject.setValue(code, forKey: "code")
managedObject.setValue(symbol, forKey: "symbol")
managedObject.setValue(placeholder, forKey: "placeholder")
managedObject.setValue(amount, forKey: "amount")
managedObject.setValue(amountValue, forKey: "amountValue")
do {
try context.save()
return true
} catch {
return false
}
}
I think you should create a type alias for a tuple type that stores these information:
typealias CryptosMOInfo = (name:String, code:String, symbol:String, placeholder:String, amount:String, amountValue:String)
And then you can just create a dictionary like this:
let cryptosDictionary: [String, CryptosMOInfo] = [
"Augur REP": (name: "Augur", code: "augur", symbol: "REP", placeholder: "REP Amount", amount: "0.00000000", amountValue: "0.0"),
// ...
]
The signature of the saveObject method can be changed to this:
static func saveOject(cryptosInfo: CryptosMOInfo) -> Bool
Just remember to access cryptosInfo:
managedObject.setValue(cryptosInfo.name, forKey: "name")
managedObject.setValue(cryptosInfo.code, forKey: "code")
managedObject.setValue(cryptosInfo.symbol, forKey: "symbol")
managedObject.setValue(cryptosInfo.placeholder, forKey: "placeholder")
managedObject.setValue(cryptosInfo.amount, forKey: "amount")
managedObject.setValue(cryptosInfo.amountValue, forKey: "amountValue")
If you don't like the type alias, you can change it into a struct as well.
Why not work with the managed object subclass directly?
Create a new instance when a new crypto is added and then save all aded/updated objects at once.
if addedCrypto == "Augur REP" {
let crypto = CryptosMO(context: CoreDataHandler.getContext())
crypto.name = "Augur"
crypto.code = "augur"
// and so on
}
Since this code seems to be the same you could create factory methods in for each crypto you support to make the code easier to read.
if addedCrypto == "Augur REP" {
_ = CryptoFactory.createAugur()
}
class CryptoFacory {
static func CreateAugur() -> CryptoMO {
return create(name: "Augur", code: "augur",...
}
//... other crypto factory methods
private static create(name: String, code: String,...) -> CryptoMO {
let crypto = CryptosMO(context: CoreDataHandler.getContext())
crypto.name = name
crypto.code = code
//...
return crypto
}
Then the save method in CoreDataHandler would not need any arguments since the crypto instance is already in the managed object context. So it would simply be
func save() -> Bool {
let context = getContext()
do {
try context.save()
return true
} catch {
return false
}
}

How to parse data from firebase and then save it within a model. Swift3 MVC format

I have a user class within my firebaseDB. Its what you would expect a flat file DB would look like: users->UID->profile->key/values. I am trying to implement best practice within my code so I want the data to be handled by the User model in my Xcode project. How would I go about parsing the data and saving it as class variables within my model.
class User {
private var _username: String
private var _uid: String
var uid: String {
return _uid
}
var username: String {
return _username
}
init (uid: String, username:String) {
self._uid = uid
self._username = username
}
func getUserData() {
DataService.ds.REF_USERS.observeSingleEvent(of: .value, with: { (snapshot) in
if let users = snapshot.value as? Dictionary<String, Any>{
for (key, value) in users { // We use the for in loop to iterate through each key/value pair in the database which is stored as a dictionary.
if let dict = value as? Dictionary<String, Any> { // This gets us inside the user ID
if let profile = dict["profile"] as? Dictionary<String, Any> { // This gets us inside the profile
if let username = profile["username"] as? String {
self._uid = key // Stores the UID of the user as the key.
self._username = username
} else {
let username = profile["name"] as? String
self._uid = key
self._username = username!
}
}
}
}
}
This is what I am trying to do but I am lost as to how I would actually store the data values within the class. FYI: username is an attribute under profile.
You are currently looping through the entire list of users in the database and assigning the User properties repeatedly, overwriting the value from the previous loop cycle.
In order to get the correct user values assigned, you will need a way of knowing the correct path to the values. I would suggest saving the users using their assigned Firebase UID.
If you know the correct UID to look up you can then implement the following code:
func getUserData() {
DataService.ds.REF_USERS.child(_uid).child("profile").observeSingleEvent(of: .value, with: { (snapshot) in
guard let profileDict = snapshot.value as? [String: Any] else {
print("Error fetching user")
return
}
self._username = profileDict["username"] as? String ?? profileDict["name"] as! String
})
}
The way that "username" is force unwrapped in your original code doesn't appear safe but I'm assuming you are certain that it will work out. Otherwise the following line should be changed to safely unwrap the optional:
self._username = profileDict["username"] as? String ?? profileDict["name"] as! String