core data and relationship predicate - swift

I've start swift & core data few month ago usually I've found my answer on this website but for the first time I'm really stuck with "Relationship" and "Predicates"
I've created a first view controller with a tableview which is populated by the user and this part is working like I wish.
The user can "tap" a cell and open a new view controller with a new tableview and I'd like populate this tableview with data that in relation with the cell the user tapped.
I'm using CoreData and I've set 2 entities : "Compte" and "Operation" they are in relationship by ONE TO MANY (ONE compte for MANY operation)
Here where I am :
when the user tap the cell i'm using segue to send the "Compte" to the second view controller :
//Segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let guest = segue.destination as! OperationsViewController
let indexPath = tableView.indexPathForSelectedRow
let operation = fetchedResultController.object(at: indexPath!)
guest.compteTestRelation = operation
}
In the OperationsViewController i've set this variable :
var compteTestRelation: Compte!
for testing my data I've create a FOR LOOP like this and a FUNCTION:
for index in 1 ... 10 {
let newOp = Operation(context: context)
newOp.nom = "Test Compte \(index)"
newOp.date = NSDate()
newOp.moyenPaiement = "Test"
compteTestRelation.addToRelationCompte(newOp) // RelationShip
}
do {
try context.save()
} catch {
print(error.localizedDescription)
}
the FUNCTION
func displayOperation() {
if let opList = compteTestRelation.relationCompte as? Set<Operation> {
sortedOperationArray = opList.sorted(by: { (operationA:Operation, operationB:Operation) -> Bool in
return operationA.date!.compare(operationB.date! as Date) == ComparisonResult.orderedAscending
})
print(sortedOperationArray)
}
}
In the console with "print" It work like I wish depend the cell is tapped the print(sortedOperationArray) appear or not
My problem now is how populate my tableview with this data, when I use predicates in my FetchResultController I've got error or an empty tableview but in the console everything seems to work so I'm thinking the relationship is OK ..
If I don't use PREDICATE I can populate my tableview with the data but I see always ALL the data
I've seen other similar problems and answers on stackoverflow.com but nothing work for the moment.
Thank You! :)

I've found an another way to predicate my data and it works for me now
I've create a new attribute in my OPERATION entity called "id" and when I create my data I attribute an ID like this :
for index in 1 ... 10 {
let newOp = Operation(context: context)
newOp.nom = "Test Compte \(index)"
newOp.date = NSDate()
newOp.moyenPaiement = "Test"
newOp.id = "id123\(compteTestRelation.nom!)"
compteTestRelation.addToRelationCompte(newOp) // RelationShip
}
do {
try context.save()
} catch {
print(error.localizedDescription)
}
then I predicate my data like this in my FetchResultController :
func setupFetchedResultController () {
let operationsRequest: NSFetchRequest<Operation> = Operation.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "nom", ascending: true)
let keyPath = "id"
let searchString = "id123\(compteTestRelation.nom!)"
let operationsPredicate = NSPredicate(format: "%K CONTAINS %#", keyPath, searchString)
operationsRequest.returnsObjectsAsFaults = false
operationsRequest.sortDescriptors = [sortDescriptor]
operationsRequest.predicate = operationsPredicate
fetchedResultController = NSFetchedResultsController(fetchRequest: operationsRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
do {
try fetchedResultController.performFetch()
} catch {
print(error.localizedDescription)
}
}

Related

fatal errors with optionals not making sense

I keep getting a fatal error saying how a value was unwrapped and it was nil and I don't understand how. When I instantiate a view controller with specific variables they all show up, but when I perform a segue to the exact VC, the values don't show up.
Take these functions for example...
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let displayVC = storyboard?.instantiateViewController(withIdentifier: Constants.Storyboards.TeachStoryboardID) as? SchoolEventDetailsViewController {
displayVC.selectedEventName = events[indexPath.row].eventName
displayVC.selectedEventDate = documentsDate[indexPath.row].eventDate
displayVC.selectedEventCost = documentsCost[indexPath.row].eventCost
displayVC.selectedEventGrade = documentsGrade[indexPath.row].eventGrade
displayVC.selectedEventDocID = documentsID[indexPath.row]?.docID
navigationController?.pushViewController(displayVC, animated: true)
}
}
This combined with this function :
func verifyInstantiation() {
if let dateToLoad = selectedEventDate {
dateEditableTextF.text = dateToLoad
}
if let costToLoad = selectedEventCost {
costEditableTextF.text = costToLoad
}
if let gradesToLoad = selectedEventGrade {
gradesEditableTextF.text = gradesToLoad
}
if let docIDtoLoad = selectedEventDocID {
docIDUneditableTextF.text = docIDtoLoad
}
if let eventNameToLoad = selectedEventName {
eventNameEditableTextF.text = eventNameToLoad
}
}
Helps load the data perfectly, but when I try to perform a segue from a search controller the data is not there.
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = selectedEventName
I set the title of the vc to have the event name , and I also recently added a text field to store it as well for experimental purposes (this question).
Now the issue is I want to do a data transfer from an Algolia Search Controller to that VC and I got all the other fields to show up, except for one and that was the document ID. So I created a completion handler function to get the document ID as a string and have it inserted into the vc when the segue is performed, just like how it's there when the vc is instantiated.
Here is the function :
func getTheEventDocID(completion: #escaping ((String?) -> ())) {
documentListener = db.collection(Constants.Firebase.schoolCollectionName).whereField("event_name", isEqualTo: selectedEventName ?? navigationItem.title).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
if let error = error {
print("There was an error fetching the documents: \(error)")
} else {
self.documentsID = querySnapshot!.documents.map { document in
return EventDocID(docID: (document.documentID) as! String)
}
let fixedID = "\(self.documentsID)"
let substrings = fixedID.dropFirst(22).dropLast(3)
let realString = String(substrings)
completion(realString)
}
}
}
I thought either selectedEventName or navigationItem.title would get the job done and provide the value when I used the function in the data transfer function which I will show now :
//MARK: - Data Transfer From Algolia Search to School Event Details
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
}
}
}
}
But it ends up showing nothing when a search result is clicked which is pretty upsetting, I can't understand why they're both empty values when I declared them in the SchoolEventDetailsVC. I tried to force unwrap selectedEventName and it crashes saying there's a nil value and I can't figure out why. There's actually a lot more to the question but I just tried to keep it short so people will actually attempt to read it and help since nobody ever reads the questions I post, so yeah thanks in advance.
I'm a litte confused what the otherVC is, which sets a property of itself in the getTheEventDocID, whilste in the closure you set the properties of self, which is a different controller. But never mind, I hope you know what you are doing.
Since getTheEventDocID runs asynchronously, the view will be loaded and displayed before the data is available. Therefore, viewDidLoad does not see the actual data, but something that soon will be outdated.
So, you need to inform the details view controller that new data is available, and refresh it's user interface. Something like
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
vc.updateUI()
}
}
}
}
and in the destination view controller:
class SchoolEventDetailsViewController ... {
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
func updateUI () {
navigationItem.title = selectedEventName
// and so on
}
}
Ok so I decided to attempt a workaround and completely ditched the getTheEventDocID() method because it was just causing me stress. So I decided to ditch Firebase generated document IDS and just use 10 digit generated ids from a function I made. I also figured out how to add that exact same 10 digit id in the Algolia record by just storing the random 10 digit id in a variable and using it in both places. So now instead of using a query call to grab a Firebase generated document ID and have my app crash everytime I click a search result, I basically edited the Struct of the Algolia record and just added an eventDocID property that can be used with hits.hitSource(at: indexPath.row).eventDocID.
And now the same way I added the other fields to the vc by segue data transfer, I can now do the same thing with my document ID because everything is matching :).

Download single Object of Firestore and save it into an struct/class object

I am coding since January 2019 and this is my first post here.
I am using Swift and Firestore. In my App is a tableView where I display events loaded out of a single Document with an array of events inside as [String: [String:Any]]. If the user wants to get more infos about an event he taps on it. In the background the TableViewController will open a new "DetailEventViewController" with a segue and give it the value of the eventID in the tapped cell.
When the user is on the DetailViewController Screen the app will download a new Document with the EventID as key for the document.
I wanna save this Data out of Firestore in a Struct called Event. For this example just with Event(eventName: String).
When I get all the data I can print it directly out but I can't save it in a variable and print it out later. I really don't know why. If I print the struct INSIDE the brackets where I get the data its working but if I save it into a variable and try to use this variable it says its nil.
So how can I fetch data out of Firestore and save in just a Single ValueObject (var currentEvent = Event? -> currentEvent = Event.event(for: data as [String:Any]) )
I search in google, firebaseDoc and stackoverflow but didn't find anything about it so I tried to save all the singe infos of the data inside a singe value.
// Struct
struct Event {
var eventName: String!
static func event(for eventData: [String:Any]) -> Event? {
guard let _eventName = eventData["eventName"] as? String
else {
print("error")
return nil
}
return Event(eventName: _eventName)
}
// TableView VC this should work
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowEventDetailSegue" {
if let ShowEvent = segue.destination as? DetailEventViewController, let event = eventForSegue{
ShowEvent.currentEventId = event.eventID
}
}
}
// DetailViewController
var currentEvent = Event()
var currentEventId: String?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let _eventID = currentEventId else {
print("error in EventID")
return}
setupEvent(eventID: _eventID) /* currentEvent should be set here */
setupView(event: currentEvent) /* currentEvent has after "setupEvent" the value of nil */
}
func setupEvent(eventID: String) {
let FirestoreRef = Firestore.firestore().collection("events").document(eventID)
FirestoreRef.getDocument { (document, error) in
if let err = error {
debugPrint("Error fetching docs: \(err)")
SVProgressHUD.showError(withStatus: "Error in Download")
}else {
if let document = document, document.exists {
guard let data = document.data() else {return}
let eventData = Event.event(for: data as [String:Any])
print(eventData)
//here all infos are printed out - so I get them
self.currentEvent = eventData!
//Here is the error.. I can't save the fetched Data in my single current Event
} else {
SVProgressHUD.showError(withStatus: "Error")
}
}
}
}
func setupView(event: Event) {
self.titleLabel.text = event.eventName
}
I expect that the function setupEvents will give the currentEvent in the DetailViewController a SINGLEvalue cause its a SINGLE document not an array. So I can use this single Eventvalue for further actions. Like starting a new segue for a new ViewController and just push the Event there not

Why tableView.reloadData() is not triggered after Core Data container.performBackgroundTask()

I am using Swift 4 to build a single view iOS 11 application that has a UITableViewController that is also defined as a delegate for a NSFetchedResultsController.
class MyTVC: UITableViewController, NSFetchedResultsControllerDeleagate {
var container:NSPersistentContainer? =
(UIApplication.shared.delegate as? AppDelegate)?.persistentContainer
var frc : NSFetchedResultsController<Student>?
override func viewDidLoad() {
container?.performBackgroundTask { context in
// adds 100 dummy records in background
for i in 1...100 {
let student = Student(context: context)
student.name = "student \(i)"
}
try? context.save() // this works because count is printed below
if let count = try? context.count(for: Student.fetchRequest()) {
print("Number of students in core data: \(count)") // prints 100
}
} // end of background inserting.
// now defining frc:
if let context = container?.viewContext {
let request:NSFetchRequest<Student> = Student.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
frc = NSFetchedResultsController<Student> (
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil )
try? frc?.performFetch() // this works and I get no errors
tableView.reloadData()
frc.delegate = self
} // end of frc definition
}
}
If I add one row of Student using the viewContext, the frc will fire the required methods to show it in the tableView. However, the 100 dummy rows are not shown. In fact, If I try to tell the tableview to reload after the insertion is done, my app starts to behave weirdly and becomes buggy, and does not do what it should do (i.e: does not delete rows, does not edit, etc).
But If I restart my app, without calling the dummy insertion, I can see the 100 rows inserted from the previous run.
The only problem is that I can't call tableView.reloadData() from the background thread, so I tried to do this:
// after printing the count, I did this:
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData() // causes UI to behave weirdly
}
then I tried to call viewContext.perform to reload the table view in the proper thread
func viewDidLoad() {
// code for inserting 100 dummy rows in background thread
// code for defining frc and setting self as delegate
if let context = container?.viewContext {
context.perform { [weak self] in
self?.tableView.reloadData() // that also causes UI to behave weirdly
}
}
}
How can tell my tableview to reload and display the 100 dummy rows in a thread-safe manner?
override func viewDidLoad() {
super.viewDidLoad()
//Always need your delegate for the UI to be set before calling the UI's delegate functions.
frc.delegate = self
//First we can grab any already stored values.
goFetch()
//This chunk just saves. I would consider putting it into a separate function such as "goSave()" and then call that from an event handler.
container?.performBackgroundTask { context in
//We are in a different queue than the main queue, hence "backgroundTask".
for i in 1...100 {
let student = Student(context: context)
student.name = "student \(i)"
}
try? context.save() // this works because count is printed below
if let count = try? context.count(for: Student.fetchRequest()) {
print("Number of students in core data: \(count)") // prints 100
}
//Now that we are done saving its ok to fetch again.
goFetch()
}
//goFetch(); Your other code was running here would start executing before the backgroundTask is done. bad idea.
//The reason it works if you restart the app because that data you didn't let finish saving is persisted
//So the second time Even though its saving another 100 in another queue there were still at least 100 records to fetch at time of fetch.
}
func goFetch() {
if let context = container?.viewContext {
let request:NSFetchRequest<Student> = Student.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
frc = NSFetchedResultsController<Student> (
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil )
try? frc?.performFetch()
//Now that records are both stored and fetched its safe for our delegate to access the data on the main thread.
//To me it would make sense to do a tableView reload everytime data is fetched so I placed this inside o `goFetch()`
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
}
}
}
After a lot of reading about the NSFetchedResultsController and the NSPersistentContainer and finally finding an important piece of information here at SO I think I have a working example.
My code is slightly different since I used a project I had for this. Anyway here is what I did:
In my view controller I had a property for my container
private var persistentContainer = NSPersistentContainer(name: coreDataModelName)
And in viewDidLoad I loaded the persistent store and created my 100 records.
persistentContainer.loadPersistentStores { persistentStoreDescription, error in
if let error = error {
print("Unable to add Persistent Store [\(error)][\(error.localizedDescription)]")
} else {
self.createFakeNotes() // Here 100 elements get created
DispatchQueue.main.async {
self.setupView() // other stuff, not relevant
self.fetchNotes() // fetch using fetch result controller
self.tableView.reloadData()
}
}
}
Below is createFakeNotes() where I use a separate context for inserting the elements in a background thread, this code is pretty much taken from Apple's Core Data programming guide but to make the UI being updated I needed to set automaticallyMergesChangesFromParent to true which I found out in this SO answer
I also delete old notes first to make the testing easier.
private func createFakeNotes() {
let deleteRequest = NSBatchDeleteRequest(fetchRequest: Note.fetchRequest())
do {
try persistentContainer.persistentStoreCoordinator.execute(deleteRequest, with: persistentContainer.viewContext)
} catch {
print("Delete error [\(error)]")
return
}
let privateContext = persistentContainer.newBackgroundContext()
privateContext.automaticallyMergesChangesFromParent = true //Important!!!
privateContext.perform {
let createDate = Date()
for i in 1...100 {
let note = Note(context: privateContext)
note.title = String(format: "Title %2d", i)
note.contents = "Content"
note.createdAt = createDate
note.updatedAt = createDate
}
do {
try privateContext.save()
do {
try self.persistentContainer.viewContext.save()
} catch {
print("Fail saving main context [\(error.localizedDescription)")
}
} catch {
print("Fail saving private context [\(error.localizedDescription)")
}
}
}
You should fetch your data by calling it from viewwillappear and then try to reload your tableview.
override func viewWillAppear(_ animated: Bool) {
getdata()
tableView.reloadData()
}
func getdata() {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
do{
persons = try context.fetch(Person.fetchRequest())
}
catch {
print("fetching failed")
}
}

cloudkit how to write to public database

Iam using cloudkit to read and write in public database - in my application user suppose to be able to upload files and write records to database and other users to read it
I am using cloudkit however as far as I know to write to the public database user has to login with icloud account however apple does not allow this in production so how to solve this - how can I give the users a right to write to DB
let Container = CKContainer.default()
let database = Container.publicCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "NewCode", predicate: predicate) //Photos is table name in cloudkit server
//-------Fetch the data---------------------
database.perform(query, inZoneWith: nil,
completionHandler: ({results, error in
DispatchQueue.main.async() { //Disp
if (error != nil) {
DispatchQueue.main.async() {
}
}
else { //a
if results!.count > 0 {
print("count = \(results!.count)")
record = results![0]
currentRecord = record
newcode = currentRecord?.object(forKey: "Code") as! String
newcodevalue = Int(newcode)!
newcodevalue = newcodevalue + 10
print("new code is = \(newcodevalue)")
myCodeStrinValue = String(newcodevalue)
print("new code string = \(newcodevalue)")
record?.setObject(myCodeStrinValue as CKRecordValue?,forKey: "Code")
database.save(record!, completionHandler: //save
({returnRecord, error in
if let err = error {
DispatchQueue.main.async() {
}
} else {
DispatchQueue.main.async() {
print("NewCode Table updated successfully")
// passing the code value we fetched above to the second viewcontroller "Uploadphotoviewcontroller" to be saved with the uploaded photo when saved button on the second controller clicked. remember you have to set an ID for the second view controller "Uploadphotoviewcontroller" to be used here when passing the value to it (set ID in the attributes right panel
// Instantiate SecondViewController
let UploadPhotoviewcontroller = self.storyboard?.instantiateViewController(withIdentifier:
"UploadPhotoviewcontroller") as! UploadPhotoviewcontroller
// Set the code value got from the DB above to the variable "myCodeValue" (this variable declared in the second view controller that will receive the value passed from here "Uploadphotoviewcontroller"
// add alpha numeric value to the code to make it more complicated for secuirty reasons
let CodeLetter = self.randomAlphaNumericString (length: 3)
UploadPhotoviewcontroller.myCodeValue = CodeLetter + myCodeStrinValue
UploadPhotoviewcontroller.codeOnly = myCodeStrinValue
// Take user to SecondViewController and accordingly remember not create graphical sague way link in the main storyboard to avoid reload of the view controller - remember to set ID attribute to UploadPhotoviewcontroller to use it here
self.navigationController?.pushViewController(UploadPhotoviewcontroller, animated: true)
}
}
})) //save
}
else { //G
DispatchQueue.main.async()
{
}
} //G
} //a
} //Disp
}))

TableView.reloadData() doesn't work after save data into core data entity

I'm trying to insert a default record into my core data entity while the tableview first-time loaded and checked there's no data in the entity.
The data inserted just fine , but the reloadData() didn't work, after navigate to other view and navigate back to the view the data appears. no matter the reloadData() in or out of the .save() method.
override func viewDidLoad() {
let cateContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let categoryRequest = NSFetchRequest(entityName: "Category")
categoryArray = (try! cateContext.executeFetchRequest(categoryRequest)) as! [Category]
if categoryArray.count == 0 {
let category = NSEntityDescription.insertNewObjectForEntityForName("Category", inManagedObjectContext: cateContext) as! Category
category.sno = "1"
category.category = "General"
category.locate = false
do {
try cateContext.save()
self.categoryTableView.reloadData()
} catch let saveError as NSError {
print("Saving Error : \(saveError.localizedDescription)")
}
}
//self.categoryTableView.reloadData()
}
If your are calling self.categoryTableView.reloadData() in viewDidLoad() method it will not reload your tableView twice. You need to call self.categoryTableView.reloadData() after you have checked if entity existed again.