EKEvent event.eventIdentifier not removing - swift

I am trying to remove an event I saved in the calendar with an event id but its removing a different event and sometimes it doesn't remove anything.
I am saving the eventId in a realm database when event is created and reading it back when I want to delete but its not working.
I have tried running it on an actual device, using an array instead of a dictionary, changing the span to .futureEvents but still doesn't work
my code for creating event and saving to realm database
/// function exits in another class
func addEventToCalendar(userName: String, userDate: Date) {
let userDefaults = UserDefaults.standard
let eventStore: EKEventStore = EKEventStore()
eventStore.requestAccess(to: .event) { (granted, error) in
if (granted) && (error == nil) {
print("granted \(granted)")
print("error \(String(describing: error))")
let event: EKEvent = EKEvent(eventStore: eventStore)
event.title = "\(userName) Birthday"
event.startDate = userDate
event.endDate = userDate
event.notes = "Happy Birthday!"
event.isAllDay = true
event.calendar = eventStore.defaultCalendarForNewEvents
let ekrules: EKRecurrenceRule = EKRecurrenceRule.init(recurrenceWith: .yearly, interval: 1, end: nil)
event.recurrenceRules = [ekrules]
//event.addAlarm(EKAlarm(absoluteDate: event.startDate))
//sets alert 00:00 on day of event
event.addAlarm(EKAlarm(relativeOffset: 0))
do {
try eventStore.save(event, span: .thisEvent, commit: true)
} catch let error as NSError {
print("error: \(error)")
}
let eventId = event.eventIdentifier ?? "nil-id"
userDefaults.setValue(eventId, forKey: "eventId")
print(eventId)
} else {
print("error not granted: \(String(describing: error))")
}
}
}
//saving it in a view controller class
#IBAction func okBtnPressed(_ sender: UIButton) {
let eventId = UserDefaults.standard.string(forKey: "eventId") ?? "no-id"
//// saving data to device
let newItem = Item()
newItem.userImageName = String(describing: userImageUrl)
newItem.userName = uName
newItem.isYearPresent = uYearPresent
newItem.userDOB = uDOB
newItem.color = UIColor.init(randomFlatColorOf: .dark).hexValue()
newItem.daysRemaining = daysRemain
newItem.eventId = eventId
self.save(item: newItem)
}
The event id saves succesfully in the realm database.
function for removing the event from calendar
func removeEvent(id: String) {
let store = EKEventStore()
store.requestAccess(to: .event) { (granted, error) in
if !granted { return }
// checking if event exists
if let eventToRemove = store.event(withIdentifier: id) {
do {
print("removing: \(id)")
try store.remove(eventToRemove, span: .thisEvent, commit: true)
print("event removed sucessfully")
} catch let error as NSError {
print("error: \(error)")
}
} else {
print("event doesnt exist.")
}
}
}
This is how I remove it
var eventIDS = [Int: String]()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// inserting evenIds to dictionary to access it in remove function
// I used an array but it gave me the same problem
if let item = itemsObject?[indexPath.row] {
eventIDS[indexPath.row] = item.eventId
}
}
// then I call remove function when swipe taps on cell
removeEvent(id: self.eventIDS[indexPath.row] ?? "")
Sometimes I get event removed successfully but it removes a different
event, sometimes I get the following errors
Error getting event with identifier 2BD633CA-BBEA-47CD-8410-40BCE6362A5C:98D9EAF2-D5EF-420F-B769-7F02B7795E54: Error Domain=EKCADErrorDomain Code=1010 "(null)"
event doesnt exist.

I figured it out. Because eventStore.requestAccess(to: .event) is asynchronous, I was saving the event id in the database before the id existed.
So I had to declare the function to accept a completion handler and return the value inside the completion handler.
//// adding events to calendar
func addEventToCalendar(userName: String, userDate: Date, completion: #escaping (String?)->()) {
let userDefaults = UserDefaults.standard
var eventId = ""
let eventStore: EKEventStore = EKEventStore()
eventStore.requestAccess(to: .event) { (granted, error) in
if (granted) && (error == nil) {
print("granted \(granted)")
print("error \(String(describing: error))")
let event: EKEvent = EKEvent(eventStore: eventStore)
event.title = "\(userName) \(NSLocalizedString("birthday", comment: "birthday"))"
event.startDate = userDate
event.endDate = userDate
event.notes = NSLocalizedString("happyBirthday", comment: "happyBirthday")
event.isAllDay = true
event.calendar = eventStore.defaultCalendarForNewEvents
let ekrules: EKRecurrenceRule = EKRecurrenceRule.init(recurrenceWith: .yearly, interval: 1, end: nil)
event.recurrenceRules = [ekrules]
//event.addAlarm(EKAlarm(absoluteDate: event.startDate))
//sets alert 00:00 on day of event
event.addAlarm(EKAlarm(relativeOffset: 0))
do {
try eventStore.save(event, span: .futureEvents, commit: true)
eventId = event.eventIdentifier ?? "no-Id"
print("Event has been saved with id \(String(describing: eventId))")
userDefaults.setValue(eventId, forKey: "eventId")
} catch let error as NSError {
print("error: \(error)")
}
completion(eventId)
} else {
print("error not granted: \(String(describing: error))")
completion(nil)
}
}
}
and then use it like so
addEventToCalendar(userName: uName, userDate: uDate) { (eventIdentifier) in
if let eventId = eventIdentifier {
print("Event add birthday id \(eventId)")
//// saving data to device
// run on main thread to avoid 'RLMException', reason: 'Realm accessed from incorrect thread.'
DispatchQueue.main.async {
let newItem = Item()
newItem.userImageName = String(describing: self.userImageUrl)
newItem.userName = uName
newItem.isYearPresent = uYearPresent
newItem.userDOB = uDOB
newItem.color = UIColor.init(randomFlatColorOf: .dark).hexValue()
newItem.daysRemaining = daysRemain
newItem.eventId = eventId
self.save(item: newItem)
// review app
self.review()
}

Related

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)
})

How to add an event at eventStore.save?

I'm developing an OS X app with Swift.
If pressing the Add button and call the function "saveEvent()" on the screen for adding an event to the calendar, the error "unrecognized selector sent to instance" will appear.
It stops at eventStore.save and the event is not added.
How can I add an event?
import Cocoa
import EventKit
class ViewController: NSViewController {
let eventStore:EKEventStore = EKEventStore()
override func viewDidLoad() {
super.viewDidLoad()
let status = EKEventStore.authorizationStatus(for: EKEntityType.event)
if status == .authorized {
print("success")
}else if status == .notDetermined {
eventStore.requestAccess(to: EKEntityType.event) { (granted, error) in
if granted {
print("true")
}else {
print("false")
}
}
}
saveEvent()
}
func saveEvent() {
if EKEventStore.authorizationStatus(for: .event) == .authorized{
let event = EKEvent(eventStore: eventStore)
event.title = "sample"
event.startDate = Date()
event.endDate = Date().addingTimeInterval(2 * 60 * 60)
event.isAllDay = false
event.calendar = self.eventStore.defaultCalendarForNewEvents
do {
try eventStore.save(event, span: .thisEvent)
}
catch {
print("Save is failed.")
}
}
}
}
I think what you can do is:
eventStore.requestAccess(to: .event, completion: { (granted, error) in
if (granted) && (error == nil) {
let event = EKEvent(eventStore: eventStore)
event.title = title
event.startDate = startDate
event.endDate = endDate
event.notes = note
event.calendar = eventStore.defaultCalendarForNewEvents
do {
try eventStore.save(event, span: .thisEvent)
} catch let e as NSError {
NSLog(e as! String)
return
}
}
})

Preventing saving duplicate values in CoreData

I'm attempting to use this method below to save or create a GameMO object which is a subclass of NSManagedObject. The problem is that when I call my winnerChanged() method below when I change a UISegmentedControl's selected value, I call this saveOrCreateGameMO() method below and that ends up creating a new GameMO object stored in CoreData instead of loading up the one already in CoreData and just updating it. I can't figure out why this is happening. Here's what I see in the logs when I change the winner of a game:
Saving game because winner was changed
Updating Game to CoreData
This tells me that the game is trying to update an already existing GameMO object because the NSPredicate in the FetchRequest is evaluating to true and is finding at least one game in CoreData with the same id (just a random UUID string) and is using that instead of creating a new one.
Even more confusing, the first time I run my app (after removing it from the simulator entirely to reset CoreData's persistent store) it spits out Updating Conference to CoreData and Updating Team to CoreData from saveOrCreateConference() and saveOrCreateTeam() respectively, indicating that it's already somehow found a valid value for the NSPredicate like NSPredicate(format: "name == %#", teamMO.name). How is this possible, if the persistent store is cleared? I've also included how I'm loading games below.
#IBAction func winnerChanged(_ sender: UISegmentedControl) {
os_log("Saving game because winner was changed", type: .debug)
let viewcell = sender.superview?.superview as? ConferenceGamesTableViewCell
guard viewcell != nil else {
os_log("viewcell is nil in winnerChanged in ConferenceGamesTableViewController", type: .debug)
return
}
guard let game = viewcell?.game else {
os_log("Could not unwrap game in winnerChanged in ConferenceGamesTableViewController", type: .debug)
return
}
guard let winnerName = sender.titleForSegment(at: sender.selectedSegmentIndex) else {
os_log("Could not unwrap game winner from UISegmentedControl in ConferenceGamesTableViewController", type: .debug)
return
}
guard let winnerConferenceName = Conference.name(forTeamName: winnerName) else {
os_log("Could not unwrap game winner conference name in ConferenceGamesTableViewController", type: .debug)
return
}
game.winner = Team(teamName: winnerName, conferenceName: winnerConferenceName)
guard let gameMO = GameMO.newGameMO(fromGame: game) else {
os_log("Could not unwrap gameMO from game in ConferenceGamesTableViewController", type: .debug)
return
}
let dataModelManager = DataModelManager.shared
dataModelManager.saveOrCreateGameMO(gameMO: gameMO)
}
public func saveOrCreateGameMO(gameMO: GameMO) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "GameMO")
fetchRequest.predicate = NSPredicate(format: "id == %#", gameMO.id)
do {
let test = try managedContext.fetch(fetchRequest)
let objectUpdate = test[0] as! GameMO
objectUpdate.setValue(gameMO.id, forKey: "id")
objectUpdate.setValue(gameMO.conferencesNames, forKeyPath: "conferencesNames")
objectUpdate.setValue(gameMO.confidence, forKeyPath: "confidence")
objectUpdate.setValue(gameMO.contestantsNames, forKeyPath: "contestantsNames")
objectUpdate.setValue(gameMO.winnerName, forKeyPath: "winnerName")
do {
try managedContext.save()
os_log("Updating Game to CoreData", type: .debug)
} catch let error as NSError {
print("Could not update game to CoreData. \(error), \(error.userInfo)")
}
} catch {
os_log("Could not fetch game from CoreData. Saving it as a new game.", type: .debug)
let entity = NSEntityDescription.entity(forEntityName: "Game", in: managedContext)!
let newGameMO = GameMO(entity: entity, insertInto: managedContext)
newGameMO.setValue(gameMO.id, forKey: "id")
newGameMO.setValue(gameMO.conferencesNames, forKeyPath: "conferencesNames")
newGameMO.setValue(gameMO.confidence, forKeyPath: "confidence")
newGameMO.setValue(gameMO.contestantsNames, forKeyPath: "contestantsNames")
newGameMO.setValue(gameMO.winnerName, forKeyPath: "winnerName")
do {
try managedContext.save()
} catch let error as NSError {
print("Could not save new game to CoreData. \(error), \(error.userInfo)")
}
}
}
public func loadGames() {
os_log("loadGames() called", log: OSLog.default, type: .debug)
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<GameMO>(entityName: "GameMO")
do {
var gamesMO = try managedContext.fetch(fetchRequest)
os_log("Loading %d games", log: OSLog.default, type: .debug, gamesMO.count)
if gamesMO.count == 0 {
guard let game1 = GameMO.newGameMO(id: UUID().uuidString, contestants: ["Elon", "NC A&T"], winner: "Elon", confidence: 65, conferences: [.caa], week: 0),
let game2 = GameMO.newGameMO(id: UUID().uuidString, contestants: ["James Madison", "Towson"], winner: "James Madison", confidence: 85, conferences: [.caa], week: 0),
let game3 = GameMO.newGameMO(id: UUID().uuidString, contestants: ["Samford", "Youngstown State"], winner: "Samford", confidence: 75, conferences: [.mvfc], week: 0),
let game4 = GameMO.newGameMO(id: UUID().uuidString, contestants: ["Elon", "The Citadel"], winner: "Elon", confidence: 60, conferences: [.caa, .southern], week: 1),
let game5 = GameMO.newGameMO(id: UUID().uuidString, contestants: ["Elon", "Richmond"], winner: "Elon", confidence: 80, conferences: [.caa], week: 2) else {
os_log("Could not create stub games in DataModelManager.loadGames()", type: .default)
return
}
gamesMO = [game1, game2, game3, game4, game5]
os_log("Needed to load games for the first time", log: OSLog.default, type: .debug)
}
self.allGames = gamesMO.map { (gameMO) -> Game in
if let game = Game(fromGameMO: gameMO) {
return game
} else {
return Game()
}
}
} catch let error as NSError {
print("Could not fetch games. \(error), \(error.userInfo)")
}
}
public static func newGameMO(fromGame game: Game) -> GameMO? {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return nil
}
let managedContext = appDelegate.persistentContainer.viewContext
let gameMO = GameMO(context: managedContext)
gameMO.id = game.id
guard let teamName1 = game.contestants.first?.name, let teamName2 = game.contestants.last?.name else {
os_log("Could not unwrap team names in GameMO.newGameMO(fromGame:)", type: .debug)
return nil
}
gameMO.contestantsNames = [teamName1, teamName2]
gameMO.confidence = game.confidence
guard let conferenceName1 = game.contestants.first?.conferenceName, let conferenceName2 = game.contestants.last?.conferenceName else {
os_log("Could not unwrap conference names in GameMO.newGameMO(fromGame:)", type: .debug)
return nil
}
gameMO.conferencesNames = [conferenceName1, conferenceName2]
gameMO.week = game.week
gameMO.winnerName = game.winner.name
return gameMO
}

Remove firestore snapshot listener not working (Swift)

I have the following code to start listening for changes in a specific directory of firestore:
func updateCart(){
database.collection("Users").document(currentUserUUID!).collection("Transactions").addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { event in
let Active = event.document.data()["Active"] as? Bool
let Type = event.document.data()["Instore"] as? Bool
let Store = event.document.data()["Store"] as? String
let TDate = event.document.data()["Date"] as? String
if (Active == true) {
print("New Transaction at \(Store!) - (Instore location:\(Type!)) on \(TDate!)")
self.searchCart(Document: event.document.documentID)
self.DocumentID = event.document.documentID
self.startTransaction()
}
if (event.type == .modified && Active == false){
print("Transaction at \(Store!) - (Instore location:\(Type!)) on \(TDate!) is no longer active or has been finalized")
let updatedData = ["Total": "\(self.Total)","Saved": "\(self.Savings)"]
database.collection("Users").document(currentUserUUID!).collection("Transactions").document(event.document.documentID).setData(updatedData, merge: true)
self.completeTransaction()
self.DocumentID = " "
}
}
}
}
In viewDidLoad I run the function: updateCart(), but when the field Active in the database changes to false, I am trying to stop the listener in the app. I tried the following code but it does not stop the listener:
func detachListener() {
let listener = database.collection("Users").document(currentUserUUID!).collection("Transactions").addSnapshotListener { querySnapshot, error in
if error != nil{
print(error as Any)
}
}
listener.remove()
}

Facing issues while integrating core-data in chat application

We are facing issues while integrating coredata with our chat application. Please help us to resolve the issue. We tried to figure out each issue individually but sometimes it gets fixed and then shows up randomly. We are tring to fix it from last 1 week.
Our setup stack
We are using sockets library to for real time chatting. To persist the data we are using core-data. Our application is supporting iOS 8 and above so we can't use PersistenceContainer so to workoround this we are using BNRCoreDataStack [url: https://github.com/bignerdranch/CoreDataStack] which is similiar to what PersistenceContainer does.
Also to display chat we are using IGListKit and we have created viewModels to avoid sending mutable coredata objects to IGLIstkit as IGListkit works fine with immutable model. Also we have used this setup to create our own viewModels [url: https://github.com/Instagram/IGListKit/blob/master/Guides/Working%20with%20Core%20Data.md]
issues we are facing
1] Constraint validation failure
2] FetchResult controller crash issue
crash-log:
2018-07-19 21:41:36.515153+0530 Toppr Doubts[62803:2359707] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3698.54.4/UITableView.m:2012
2018-07-19 21:41:36.517093+0530 Toppr Doubts[62803:2359707] [error] error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (60) must be equal to the number of rows contained in that section before the update (50), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (60) must be equal to the number of rows contained in that section before the update (50), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
3] Illegal attempt to establish a relationship 'lastMessage' between objects in different contexts
Below is our CoreDataModel, we are not using any abstract entity for Message as we came to know this could cause lot of performance issues.
Session Entity
We are creating object in fromJSON method and setup is same for rest of the entities. Also I am sharing LastMessage Entity to show the way we are integrating relationships, again same for others
public class Session: NSManagedObject {
class func fromJSON(_ json: JSON, moc: NSManagedObjectContext) -> Session? {
if let entityDescription = NSEntityDescription.entity(forEntityName: "Session", in: moc) {
// Object creation
let object = Session(entity: entityDescription, insertInto: moc)
object.internalIdentifier = json[kSessionIdKey].int64Value
if let date = json[kSessionStartedOnKey].dateTime as NSDate? {
object.startedOn = date
}
if let date = json[kSessionEndedOnKey].dateTime as NSDate? {
object.endedOn = date
}
object.statusType = SessionStatus(rawValue: json[kSessionStatusKey].stringValue).map { $0.rawValue } ?? SessionStatus.none.rawValue
object.stateType = SessionState(rawValue: json[kSessionStateKey].stringValue).map { $0.rawValue } ?? SessionState.none.rawValue
if let ratingDict = json[kSessionRatingKey].dictionaryObject {
if let rating = ratingDict["student"] as? Int {
object.rating = rating
}
}
object.subjectID = json[kSessionSubjectKey]["id"].intValue
// Subjects are already stored need to fetch and assign
let subjectFetchRequest: NSFetchRequest<Subject> = Subject.fetchRequest()
subjectFetchRequest.predicate = NSPredicate(format: "internalIdentifier == %d", Int64(json[kSessionSubjectKey]["id"].intValue))
do {
if let subject = try moc.fetch(subjectFetchRequest).first {
object.subject = subject
}
} catch let erroe as NSError {
Logger.log.error(error)
}
// Student Object initialisation
if json[kSessionStudentKey] != JSON.null {
if let student = Student.fromJSON(json[kSessionStudentKey], moc: moc) {
object.student = student
}
}
// Tutor Object initialisation
if json[kSessionTutorKey] != JSON.null {
if let tutor = Tutor.fromJSON(json[kSessionTutorKey], moc: moc) {
object.tutor = tutor
}
}
// LastMessage Object initialisation
if json[kSessionLastMessageKey] != JSON.null {
if let lastMessage = LastMessage.fromJSON(json[kSessionLastMessageKey], moc: moc) {
object.lastMessage = lastMessage
} else {
return nil
}
}
return object
}
return nil
}
}
LastMessage
public class LastMessage: NSManagedObject, Message {
class func fromJSON(_ json: JSON, moc: NSManagedObjectContext) -> LastMessage? {
if let entityDescription = NSEntityDescription.entity(forEntityName: "LastMessage", in: moc) {
let object = LastMessage(entity: entityDescription, insertInto: moc)
object.id = json[kMessageIdKey].intValue
object.body = json[kBodyKey].stringValue
object.type = MessageType(rawValue: json[kTypeKey].stringValue) ?? MessageType.none
object.doubtTag = json[kDoubtTagKey].stringValue
if json[kAttachmentKey] != JSON.null {
if let attachment = Attachment.fromJSON(json[kAttachmentKey], moc: moc) {
object.attachment = attachment
}
}
if let date = json[kSentOnKey].dateTime as NSDate? {
object.sentOn = date
}
if json[kSentByKey] != JSON.null {
if let sentBy = SentBy.fromJSON(json[kSentByKey], moc: moc) {
object.sentBy = sentBy
}
}
object.deliveryState = DeliveryState(rawValue: json[kDeliveryStateKey].stringValue) ?? DeliveryState.none
object.sessionId = json[kSessionIdKey].intValue
return object
}
return nil
}
}
Get User State
Helps us fetch data for subject and live chat.
static func getUserState(completion:#escaping (_ success: Bool) -> Void) {
SocketManager.sharedInstance.send(eventName: .userState) { (response) in
guard !response.isError() else { return completion(false) }
// Coredatastack
guard let coreDataStack = (UIApplication.shared.delegate as! AppDelegate).coreDataStack else { return }
let wmoc = coreDataStack.newChildContext()
// Save Subject and Live Sessions
let subjects = response.result["subjects"].arrayValue.flatMap({ Subject.fromJSON($0, moc: wmoc) })
let sessions = response.result["live_sessions"].arrayValue.flatMap({ Session.fromJSON($0, moc: wmoc) })
if sessions.isNotEmpty || subjects.isNotEmpty {
CoreDataStack.batchUpdate(moc: wmoc, completion: {
NotificationCenter.default.post(name: NSNotification.Name("didUpdateUserState"), object: nil)
completion(true)
})
}
completion(false)
}
}
Get Previous Session
Helps us fetch data for inactive chats. We are getting problem in this while storing sessions
static func getPreviousSessions(isLoadingMore: Bool, completion: #escaping (_ success: Bool,_ isMore: Bool)->Void) {
guard let coreDataStack = (UIApplication.shared.delegate as! AppDelegate).coreDataStack else { return }
let wmoc = coreDataStack.newChildContext()
var sessionID = 0
// TODO: - Need to implement last sessionID from CoreData
if isLoadingMore {
// Get Sessions with Status == closed order by sessionID asc
let archiveSession = Session.filterArchivedSessions(moc: wmoc)
let sortedArchiveSession = archiveSession?.sorted(by: { $0.0.id < $0.1.id })
// Get last sessionID
if let lastSessionID = sortedArchiveSession?.first?.id {
sessionID = lastSessionID
}
}
let request: [String: Any] = [ "last_session_id": sessionID ]
SocketManager.sharedInstance.send(request, eventName: .getPreviousSessions) { (response) in
if response.result.isEmpty {
completion(false, false)
} else {
let sessions = response.result["sessions"].arrayValue.flatMap({ Session.fromJSON($0, moc: wmoc) })
if sessions.isNotEmpty {
CoreDataStack.batchUpdate(moc: wmoc)
} else {
for session in sessions { wmoc.delete(session) }
}
if let isMore = response.result["is_more_server"].bool {
completion(true, isMore)
}
}
}
}
In the above image every session is suppose to have one last message and one subject. But as you can see, there are no last messages and some session have subjects as null
CoreDataStack+Extension
To Save data directly to Store
extension CoreDataStack {
// This method will add or update a CoreData's object.
static func batchUpdate(moc: NSManagedObjectContext? = nil, completion: (()-> Void)? = nil) {
guard let moc = moc, moc.hasChanges else { return }
if #available(iOS 10.0, *) {
moc.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
}
do {
try moc.performAndWaitOrThrow {
try moc.saveContextToStoreAndWait()
DispatchQueue.main.async {
completion?()
}
}
} catch {
print("Error creating initial data: \(error)")
}
}
}
NSFetchedResultsController Setup
lazy var archievedSessionFRC: NSFetchedResultsController<Session> = {
guard let coreDataStack = (UIApplication.shared.delegate as! AppDelegate).coreDataStack else { return NSFetchedResultsController() }
// Create Fetch Request
let fetchRequest: NSFetchRequest<Session> = Session.fetchRequest()
// Configure Fetch Request
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "internalIdentifier", ascending: false)]
fetchRequest.predicate = NSPredicate(format: "statusType = %#", "closed")
let archievedSessionFRC = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: coreDataStack.mainQueueContext,
sectionNameKeyPath: nil,
cacheName: nil)
archievedSessionFRC.delegate = self
return archievedSessionFRC
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
do {
try archievedSessionFRC.performFetch()
if let sessions = archievedSessionFRC.fetchedObjects {
self.previousSessions = sessions
}
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.localizedDescription)")
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension HomeVC: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
if let indexPath = newIndexPath {
self.tableView.insertRows(at: [indexPath], with: .automatic)
}
case .delete:
if let indexPath = indexPath {
self.tableView.deleteRows(at: [indexPath], with: .automatic)
}
case .move:
if let indexPath = indexPath , let newIndexPath = newIndexPath {
self.tableView.moveRow(at: indexPath, to: newIndexPath)
}
case .update:
if let indexPath = indexPath {
self.tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.endUpdates()
}
}
Thanks in Advance