Swift Core Data - handling empty fetch result - swift

I have a simple entity, a stationID and a type, both Strings
I use this method to search for a type for a given stationID,
func returnStationType(stationTypeId: String) -> PersistantStationType {
let context = container.viewContext
let request = PersistantStationType.fetchRequest() as NSFetchRequest<PersistantStationType>
request.predicate = NSPredicate(format: "%K == %#", #keyPath(PersistantStationType.stationId), stationTypeId as CVarArg)
do {
let result = try context.fetch(request)
if result.count != 0 {
let fetchedStationType = result.first!
return fetchedStationType
} else { print("4 Fetch result was empty for specified stationid: \(String(describing: stationTypeId))")
}
} catch { print("Fetch on goal id: \(String(describing: stationTypeId)) failed. \(error)") }
return PersistantStationType.init()
}
I call the method here:
let persistentStationType = persistenceController.returnStationType(stationTypeId: closestStation.id)
let stationType = persistentStationType.type?.lowercased().capitalized
let realName = helper.returnRealName(stationType: stationType ?? "None")
let imageName = helper.returnStationTypeImage(stationType: stationType ?? "None")
If a stationId is not found - so empty result I get a crash in the code(Bad Exec/ Access) where I call the method - not in the method itself. (I'm not surprised by this but i'm not sure how to handle it)
I can't use if let on the method .. .it's not an optional. Should I be returning an empty object and check for It when the fetch result is empty?
Thanks for any help.

I would declare the function to return an optional and return nil when nothing is found
func returnStationType(stationTypeId: String) -> PersistantStationType? {
let context = container.viewContext
let request: NSFetchRequest<PersistantStationType> = PersistantStationType.fetchRequest()
request.predicate = NSPredicate(format: "%K == %#", #keyPath(PersistantStationType.stationId), stationTypeId)
do {
let result = try context.fetch(request)
if result.count != 0 {
return result[0]
} else {
print("4 Fetch result was empty for specified stationid: \(String(describing: stationTypeId))")
return nil
}
} catch {
print("Fetch on goal id: \(String(describing: stationTypeId)) failed. \(error)")
return nil
}
}
If on the error hand it is considered an error if more than one objects (or none) exists for an id then it would be better to throw an error in those situations
func returnStationType(stationTypeId: String) throws -> PersistantStationType {
let context = container.viewContext
let request: NSFetchRequest<PersistantStationType> = PersistantStationType.fetchRequest()
request.predicate = NSPredicate(format: "%K == %#", #keyPath(PersistantStationType.stationId), stationTypeId)
do {
let result = try context.fetch(request)
switch result.count {
case 0:
throw FetchError("No station type found for id \(stationTypeId)")
case 1:
return result[0]
default:
throw FetchError("Multiple station types found for id \(stationTypeId)")
}
} catch let error as NSError {
throw FetchError(error.localizedDescription)
}
}
struct FetchError: LocalizedError {
private let message: String
var errorDescription: String? {
message
}
init(_ message: String) {
self.message = message
}
}

Related

Core Data Predicate Using KeyPath on a Collection

I have the following code which returns all movies matching the actor name.
static func byActorName(name: String) -> [Movie] {
let request: NSFetchRequest<Movie> = Movie.fetchRequest()
request.predicate = NSPredicate(format: "actors.name CONTAINS %#", name)
do {
return try viewContext.fetch(request)
} catch {
print(error)
return []
}
}
The code above works, but how can I convert it to a modern Swift KeyPath approach?
I implemented the following code which works but I don't like the idea of writing %K.name syntax.
static func byActorName(name: String) -> [Movie] {
let request: NSFetchRequest<Movie> = Movie.fetchRequest()
request.predicate = NSPredicate(format: "%K.name CONTAINS %#", #keyPath(Movie.actors), name)
do {
return try viewContext.fetch(request)
} catch {
print(error)
return []
}
}
Any recommendations?
Solution:
request.predicate = NSPredicate(format: "%K.%K CONTAINS %#", #keyPath(Movie.actors), #keyPath(Actor.name), name)

shared database zones cannot be found

I have sucessfully shared a record from private database customzone.
share url Optional(https://www.icloud.com/share/0N9smwzXZ0gfumZv8jyVo7uag)
When I print shared record I get this:
record completion <CKShare: 0x106702100; participants=(
"<CKShareParticipant: 0x2817115e0; hasProtectionInfo=1, isCurrentUser=YES, participantID=E742CBD3-41C9-4A9E-A392-08914E8C1D37, permission=readWrite, role=owner, acceptanceStatus=Accepted, identity=<CKUserIdentity: 0x281815ce0; lookupInfo=<CKUserIdentityLookupInfo: 0x2832ba910; email=vhrao#icloud.com>, cached=0, nameComponents=viswanatha, userID=<CKRecordID: 0x283c6f940; recordName=__defaultOwner__, zoneID=_defaultZone:__defaultOwner__>, contactIdentifiers={\n items = (\n );\n}>>"
), allowsReadOnlyParticipantsToSeeEachOther=YES, publicPermission=none, recordChangeTag=2z, rootRecordID=<CKRecordID: 0x283c39360; recordName=645B675E-121F-4DC8-B86D-8968C8DE7B1C-9651-00000ED7A0D0E95D, zoneID=userProfileZone:__defaultOwner__>, routingKey=0N9, recordID=Share-967C18F4-43C4-4958-9D40-6D890DD205AD:(userProfileZone:__defaultOwner__), recordType=cloudkit.share, etag=2z> and nil
record completion <CKRecord: 0x10bc05ac0; recordID=645B675E-121F-4DC8-B86D-8968C8DE7B1C-9651-00000ED7A0D0E95D:(userProfileZone:__defaultOwner__), recordChangeTag=30, values={
asOfDate = "2020-08-01 20:26:44 +0000";
latitude = "29.9288104125185";
locationId = "1D2A93A8-A22C-4B2A-9EC6-86F646667F3E-8282-00000CF78F38B1DB";
locationType = "Household User";
longitude = "-95.6080147578347";
profileImage = "<CKAsset: 0x10bc05f60; referenceSignature=<01ebb50c 4bd0e618 bb0f4486 b486145b 30f22689 83>, uploadRank=0, path=/private~/Library/Caches/CloudKit/0fce42f4be876f65546aa1b4549b3658e9291963/Assets/B21AB25E-C07A-4D65-9238-61A17B3E6372.01b1f7f738f239ee006815df2bb2896fade0f08229, size=34656, UUID=B21AB25E-C07A-4D65-9238-61A17B3E6372, signature=<01b1f7f7 38f239ee 006815df 2bb2896f ade0f082 29>, wrappedAssetKey=<24 bytes>>";
userType = "Consumer";
}, share=<CKReference: 0x283c38ae0; recordID=<CKRecordID: 0x283c390c0; recordName=Share-967C18F4-43C4-4958-9D40-6D890DD205AD, zoneID=userProfileZone:__defaultOwner__>>, recordType=infrastructure> and nil
Then I fetch RecordZones
func fetchRecordZones(completion: #escaping (CKRecordZoneID?, Error?) -> Void) {
var fetchedRecordZones: [CKRecordZoneID : CKRecordZone]? = nil
let fetchZonesOperation = CKFetchRecordZonesOperation.fetchAllRecordZonesOperation()
fetchZonesOperation.fetchRecordZonesCompletionBlock = { (recordZones: [CKRecordZoneID : CKRecordZone]?, error: Error?) -> Void in
guard error == nil else {
completion(nil, error)
return
}
if let recordZones = recordZones {
fetchedRecordZones = recordZones
for recordID in recordZones.keys {
print(recordID.zoneName)
if (recordID.zoneName == Cloud.SharedZone.UserProfile.ZoneName) {
completion(recordID, nil)
}
}
}
completion(nil, nil)
}
fetchZonesOperation.qualityOfService = .utility
self.sharedDB?.add(fetchZonesOperation)
}
I get no recordZones
I go to coud Kit and look at the shared database and fetch zones with fetch with change Token checkbox enabled I get name: userProfileZone owner record name:_9d4825db9b0aa911655e420ec0016129
Looks like it has a zone in shared database. At least seems like, so why am I not getting zones from shared database?
//Sharing code
var lookupInfos = [CKUserIdentity.LookupInfo]()
let creatorID = rootRecord.creatorUserRecordID
self.defaultContainer?.discoverUserIdentity(withUserRecordID: creatorID!) { [weak self] (userIdentity, error) in
guard error == nil else {
if let ckerror = error as? CKError {
self!.aErrorHandler.handleCkError(ckerror: ckerror)
}
completion(false, error)
return
}
if let userIdentity = userIdentity {
lookupInfos.append(userIdentity.lookupInfo!)
let share = CKShare(rootRecord: rootRecord)
share[CKShareTitleKey] = "Infrastructure" as CKRecordValue
//share[CKShareThumbnailImageDataKey] = shoppingListThumbnail as CKRecordValue
share[CKShareTypeKey] = "com.nr2r.infrastructure" as CKRecordValue
if let lookupInfo = userIdentity.lookupInfo {
let op: CKFetchShareParticipantsOperation = CKFetchShareParticipantsOperation(userIdentityLookupInfos: [lookupInfo])
op.fetchShareParticipantsCompletionBlock = { error in
if let error = error {
print("error: ", error)
}
}
op.shareParticipantFetchedBlock = { participant in
participant.permission = .readWrite
share.addParticipant(participant)
let modOp: CKModifyRecordsOperation = CKModifyRecordsOperation(recordsToSave: [rootRecord, share], recordIDsToDelete: nil)
modOp.savePolicy = .ifServerRecordUnchanged
modOp.perRecordCompletionBlock = {record, error in
print("record completion \(record) and \(String(describing: error))")
}
modOp.modifyRecordsCompletionBlock = {records, recordIDs, error in
guard let ckrecords: [CKRecord] = records, let record: CKRecord = ckrecords.first, error == nil else {
print("error in modifying the records " + error!.localizedDescription)
completion(false, error)
return
}
if let records = records {
for record in records {
print ("recordType: \(record.recordType)")
if (record.recordType == "cloudkit.share") {
self!.sharedCKRecordZoneID = record.recordID.zoneID
}
print ("recordName: \(record.recordID.recordName)")
print ("zoneID: \(record.recordID.zoneID.zoneName)")
print ("zoneID: \(record.recordID.zoneID.ownerName)")
}
}
if let anURL = share.url {
print("share url \(String(describing: share.url))")
completion(true, nil)
}
}
self?.privateDB?.add(modOp)
}
self?.defaultContainer?.add(op)
}
} else {
completion(false, nil)
}
}
More edits to show how to retrieve shared record:
let op = CKFetchShareMetadataOperation(shareURLs: [aURL])
op.perShareMetadataBlock = { shareURL, shareMetadata, error in
if let shareMetadata = shareMetadata {
if shareMetadata.participantStatus == .accepted {
let query = CKQuery(recordType: Cloud.Entity.Infrastructure, predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
let zone = CKRecordZoneID(zoneName: Cloud.PrivateZone.UserProfile.ZoneName, ownerName: (shareMetadata.ownerIdentity.userRecordID?.recordName)!)
self.sharedDB?.perform(query, inZoneWith: zone, completionHandler: { (records, error) in
if let ckerror = error as? CKError {
self.aErrorHandler.handleCkError(ckerror: ckerror)
completion(nil, error)
} else if let records = records, let firstRecord = records.first {
completion(firstRecord, nil)
}
})
}
}
Fetching shared database zones return zero
self.sharedDB?.fetchAllRecordZones(completionHandler: { (ckrecordZones, error) in
guard error == nil else {
if let ckError = error as? CKError {
self.aErrorHandler.handleCkError(ckerror: ckError)
}
completion (nil, error)
return
}
if let recordZones = ckrecordZones {
for i in 0 ..< recordZones.count{
// find the zone you want to query
if recordZones[i].zoneID.zoneName == Cloud.SharedZone.UserProfile.ZoneName {
completion (recordZones[i].zoneID, nil)
}
}
}
completion (nil, nil)
})

Core Data, Downcasing Any EXC_BAD_ACCESS (code=1, address=0x0)

I am saving some data I get from an Api with Core Data. Sometimes when I want to save the data an error occurs:
Thread 4: EXC_BAD_ACCESS (code=1, address=0x0)
in this line:
coin.name = ticker.value as? String
This is my full function:
private func saveDictionary(version: ApiParameter, _ data: [String:Any?]?, _ fetchRequest: NSFetchRequest<Coin>) throws {
if checkVersion(version: version.versionKey()) {
if data != nil {
for ticker in data! {
if ticker.value != nil {
fetchRequest.predicate = NSPredicate.init(format: "ticker = %#", "\(ticker.key)")
do {
let coins = try managedContext.fetch(fetchRequest)
for coin in coins {
// print("ticker:",coin.ticker)
if ticker.key == coin.ticker {
switch version {
case .linksWebsites:
coin.website = ticker.value as? String
case .fdefSymbolsTallCc:
coin.ccsymbol = ticker.value as? String
case .icons32Exall:
coin.icon = ticker.value as? NSData
case .fdefCoinsTarrExall:
coin.group = ticker.value as? [String]
case .coinNamesExall:
coin.name = ticker.value as? String
default: break
}
}
}
}
}
}
}
}
do {
try managedContext.save()
} catch let error {
print(error.localizedDescription)
throw error
}
}
After some research I found out that the error indicates there is a NULL pointer, but when I inspect the variable it is not null:

How to append a record in core data entity having relation one to many

I have two entities of Mobile and User in which Mobile has attribute name and user also has a attribute of name. User can has multiple mobile but mobile has can only one user. Scenario is that when ever I enter a name for user which is already present in User entity it should update that user.mobile set rather than duplicating a new user.
These are my save and fetch functions
func save () {
if context.hasChanges {
do {
try context.save()
print("saved succesfully")
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
func fetch<T: NSManagedObject>( _ objectType: T.Type) -> [T] {
let entityName = String(describing: objectType)
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
do {
let fetchedObjects = try context.fetch(fetchRequest) as? [T]
return fetchedObjects ?? [T]()
} catch {
print(error)
return [T]()
}
}
This is how i am adding
users = persistenceManager.fetch(User.self)
if users.count > 1 {
for val in users {
if val.name == "Umar" {
val.addToDevice(device)
device.user = val
persistenceManager.save()
}
else if val.name != nil {
user.name = "Umar"
user.addToDevice(device)
device.user = user
persistenceManager.save()
}
}
Your question lacks some details. BUt let me assume I understand you correctly.
let userName = "some name from user input"
let user = MOUser.getUser(context, name: userName, createIfMissing: true)
// here you have user with name required and not duplicated if already exists
device.user = user
And add static function either to your user class or to db coordinator
static func getUser(moc: NSMaagedObjectContext, name: String, createIfMissing: Bool) -> MOUser? {
let request: NSFetchRequest<MOUser> = MOUser.fetchRequest()
request.predicate = NSPredicate(format: "\(#keyPath(MOUser.name)) = %#", name)
request.fetchLimit = 1
var result: MOUser? = (try? moc.fetch(request))?.first
if result == nil && createIfMissing {
result = MOUser(context: moc)
result?.name = name
}
return result
}

Generic FetchRequest

I have a static function that extends NSManagedObject to get an object like so...
NSManagedObject.get(type: MYUser.self, with: ("id", "SomeUserId"), in: context)
extension NSManagedObject {
static func get<M: NSManagedObject>(type: M.Type, with kvp: (String, CVarArg), in context: NSManagedObjectContext) -> M? {
guard let name = entity().name else { return nil }
guard M.entity().propertiesByName[kvp.0] != nil else { Assert("\(name) does not have \(kvp.0)"); return nil }
let fetchRequest = NSFetchRequest<M>(entityName: name)
fetchRequest.predicate = NSPredicate(format: "\(kvp.0) == %#", kvp.1)
do {
let object = try context.fetch(fetchRequest)
if let foundObject = object.first { return foundObject }
return nil
} catch {
return nil
}
}
}
The syntax I would like is
MYUser.get(with: ("id", "SomeUserId"), in: context)
and to infer the Type from the class that made the call... but I'm unsure what to put in place of the generic here
NSFetchRequest<M>(entityName: name)
NSFetchRequest<???>(entityName: name)
Thanks in advance
If you don't mind writing MYUser twice, you can remove the type parameter and specify the type so that Swift can infer M:
extension NSManagedObject {
static func get<M: NSManagedObject>(with kvp: (String, CVarArg), in context: NSManagedObjectContext) -> M? {
guard let name = entity().name else { return nil }
guard M.entity().propertiesByName[kvp.0] != nil else {
print("\(name) does not have \(kvp.0)")
return nil
}
let fetchRequest = NSFetchRequest<M>(entityName: name)
fetchRequest.predicate = NSPredicate(format: "\(kvp.0) == %#", kvp.1)
do {
let object = try context.fetch(fetchRequest)
if let foundObject = object.first { return foundObject }
return nil
} catch {
return nil
}
}
}
// usage:
let user: MYUser? = MYUser.get(with: ("id", "SomeUserId"), in: context)
If you don't want to write MYUser twice, then I can't think of any solutions. If NSManagedObject were a protocol, you could have used Self in there.
Based on the link Passing generic Class as argument to function in swift suggested by Martin R
protocol Managed where Self: NSManagedObject { }
extension Managed where Self: NSManagedObject {
static func get(with kvp: (String, CVarArg), in context: NSManagedObjectContext) -> Self? {
guard let name = entity().name else { return nil }
guard entity().propertiesByName[kvp.0] != nil else { Assert("\(name) does not have \(kvp.0)"); return nil }
let fetchRequest = NSFetchRequest<Self>(entityName: name)
fetchRequest.predicate = NSPredicate(format: "\(kvp.0) == %#", kvp.1)
do {
let object = try context.fetch(fetchRequest)
if let foundObject = object.first { return foundObject }
return nil
} catch {
return nil
}
}