Before I even start asking my question, I'd like to add a little note - I'm not asking for code. I want to explicitly state that now, as some may perceive this question as a request for code. I'm asking for ideas on HOW to achieve this, I don't want someone else to do so for me.
Recently, I've gotten myself into Core Data, and I've been experiencing one unique issue that I'm having major problems solving. I have some objects subclassing NSManagedObject, and they have three properties, one of which is relevant to this situation - dueDate, which is of the type NSDate.
I've been working to find a way to sort the subclassing objects, and then add then into one of five different sections in the UITableView, which include:
Today
Tomorrow
One Week
One Month
Greater Than One Month
Sorting the elements works with this function:
func fetchAssignments() {
let appDelegate =
UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext!
let fetchRequest = NSFetchRequest(entityName:"Assignment")
var error: NSError?
let fetchedResults =
managedContext.executeFetchRequest(fetchRequest,
error: &error) as! [NSManagedObject]?
if let results = fetchedResults {
let calendar: NSCalendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
let todayDate = calendar.startOfDayForDate(NSDate())
for item in results {
let dueDate = item.valueForKey("dueDate") as! NSDate
println(dueDate)
let calendarComponents = calendar.components(.CalendarUnitYear | .CalendarUnitMonth | .CalendarUnitDay,
fromDate: todayDate, toDate: dueDate, options: nil)
println("Months: " + calendarComponents.month.description)
println("Days: " + calendarComponents.day.description)
if calendarComponents.month >= 2 || (calendarComponents.month == 1 && calendarComponents.day > 0) {
println("Greater")
} else if calendarComponents.month == 1 || calendarComponents.day > 7 {
println("One Month")
} else if calendarComponents.day > 1 {
println("One Week")
} else if calendarComponents.day == 1 {
println("Tomorrow")
} else {
println("Today")
}
}
println()
tableView.reloadData()
} else {
println("Could not fetch \(error), \(error!.userInfo)")
}
}
Currently, this function does not utilize my subclassing object, but that shouldn't have any effect other than the need to cast the value as a NSDate object.
My question is, how can I insert this objects into the UITableView. I understand this is quite a broad question, but I honestly do not know what else to do. I've tried using multiple arrays, one for each category, but that hasn't worked out to well - if one of the arrays is empty, problems arise. How can I sort these objects so that they can inserted and deleted once in the UITableView?
I'd look at moving your logic around a little, and at using a fetched results controller to deal with the sectioning.
To use a fetched results controller create a fetch and add a sort descriptor by date. This will be a plain list that you can get working first.
Then, add a transient property to you subclass and there you should return the string for which section the object belongs in (the result can be cached so you aren't recalculating it all the time). Set the name of this transient property as the section name key path on the FRC.
I'd recommend you look into the NSFetchedResultsController class. It's specifically designed to display Core Data into a tableview.
NSFetchedResultsController
You can add sort descriptors, which will keep your sorted and organized and supports the swipe left to delete functions. It does a lot of heavy lifting for you when it comes to managing core data in a table view.
Related
I've come across an issue which rarely happens, and (of course) it works perfectly when I test it myself. It has only happened for a few users, and I know I have at least a couple hundred who use the same App daily.
The issue
When updating a list of coredata objects in a tableview, it not only updates the objects (correctly), it also creates duplicates of all these objects.
Coredata setup
It's a NSPersistentCloudKitContainer with these settings:
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
Tableview setup
I have a tableview which displays a list of the 'ActivityType' objects. It's very simple, they have a name (some other basic string/int properties), and an integer called 'index'. This 'index' exists so that users can change the order in which they should be displayed.
Here is some code for how each row is setup:
for activityType in activityTypes {
row = BlazeRow()
row.title = activityType.name
row.cellTapped = {
self.selectedActivityType(activityType)
}
row.object = activityType
row.cellReordered = {
(index) in
self.saveNewOrder()
}
section.addRow(row)
}
As you can see, it has 2 methods. One for selecting the activity which shows its details in a new viewcontroller, and one which is called whenever the order is changed.
Here's the method that is called whenever the order is changed:
func saveNewOrder() {
Thread.printCurrent()
let section = self.tableArray[0] as! BlazeSection
for (index, row) in section.rows.enumerated() {
let blazeRow = row as! BlazeRow
let object = blazeRow.object as! ActivityType
object.index = Int32(index)
}
BDGCoreData.saveContext()
}
And here's the code that saves the context (I use a singleton to easily access the viewcontext):
class func saveContext(context: NSManagedObjectContext = BDGCoreData.viewContext) {
if(context.hasChanges) {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
Now, I swear to god it never calls the method in this viewcontroller to create a new object:
let activity = ActivityType(context: BDGCoreData.viewContext). I know how that works, and it truly is only called in a completely different view controller. I searched for it again in my entire project just in case, and it's really never called/created in any other places.
But somehow, in very rase cases, it saves the correct new order but also duplicates all objects in this list. Since it only happens rarely, I thought it might have something to do with threads? Which is why, as you can see in the code, I printed out the current thread, but at least when testing on my device, it seems to be on the main thread.
I'm truly stumped. I have a pretty good understanding of coredata and the app itself is quite complex with full of objects with different kind of relationships.
But why this happens? I have no clue...
Does anyone have an idea?
I'm reading data from my NoSQL Firebase database, parsing that data into individual components, then displaying them in my tableView. I've added table refreshing functionality so when a new piece of data is added the user can refresh and it will be added to the table.
The function that's call to refresh the table is the same function that does the initial table populating, so in a sense refreshing just restarts the view. The steps that are taken are:
Empty out array and dictionary that hold parsed data elements
Fetch data from database
Parse that data
Reload the table
Here's the full function:
func readEventsFromDb() {
// 1. Empty out data structures
eventsForDate.removeAll()
allDates.removeAll()
// 2. Fetch data
let dbRef = FIRDatabase.database().reference().child("pets").child(currentPet).child("events")
dbRef.observeSingleEvent(of: .value, with: { snapshot in
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
// 3. Parse data elements
for child in snapshots{
if let data = child.value as? [String: Any] {
if let c = data["comment"] as? String, let p = data["user"] as? String, let t = data["type"] as? Int, let d = data["date"] as? UInt64 {
let event = PetEvent(comment: c, person: p, type: t, time: self.timeFromEpoch(time: Double(d)))
let eventDate = self.dateFromEpoch(time: Double(d))
if (self.eventsForDate[eventDate] != nil) {
self.eventsForDate[eventDate]!.append(event)
} else {
self.eventsForDate[eventDate] = [event]
}
}
}
}
// 4. Refresh table
self.allDates = Array(self.eventsForDate.keys)
self.feedTable.reloadData()
self.refreshControl.endRefreshing()
}
})
}
It doesn't make a lot of sense to me that refreshing the table would pretty much just restart the view, as this is the only thing in the view. Is this how table refreshing usually works or is there a more efficient way to do such a thing?
Use ref.observe instead of ref.observeSingleEvent to continuous updating the table view.
ref.observe(.childAdded ...) //insert row
ref.observe(.childRemoved ...) //remove row
ref.observe(.childChanged ...) //update row
I'm not sure why you would do so much manual work to have the user refresh this data - one of the biggest values of Firebase is that you can do this automatically. This can work, but is definitely not how "most other applications" do this.
I would STRONGLY recommend you take a look at the FirebaseUI project:
https://github.com/firebase/FirebaseUI-iOS
This includes data sources for UITableView and UICollectionView displays that handle 90% of the work behind what you're doing, but also support incremental (and animated, like other iOS apps) row display. If a row is deleted, for instance, the user would see that deletion with a nice animation, while maintaining their scroll position within the table. (The solution you've outlined will lose this position, which isn't very user-friendly.)
Included in the project is a simple example app that uses the module to show a simple live table:
https://github.com/firebase/FirebaseUI-iOS/tree/master/FirebaseDatabaseUITests
So, I am new to cloudKit and to working with multiple threads in general, which I think is the source of the problem here, so if I simply need to research more, please just comment so and I will take that to heart.
Here is my question:
I am working in Swift 3 Xcode 8.1
I have in my view controller this variable:
var contactsNearby: [String:CLLocation]?
Then at the end of ViewDidLoad I call one of my view controllers methods let's call it:
populateContactsNearby()
inside that method I call:
container.discoverAllIdentities(completionHandler: { (identities, error) in
for userIdentity in identities! {
self.container.publicCloudDatabase.fetch(withRecordID: userIdentity.userRecordID!, completionHandler: { (userRecord, error) in
let contactsLocation = userRecord?.object(forKey: "currentLocation")
if self.closeEnough(self.myLocation!, contactLocation: contactsLocation as! CLLocation) {
var contactsName = ""
contactsFirstName = userIdentity.nameComponents?.givenName
if contactsName != "" && contactsLocation != nil {
self.contactsNearby?["\(contactsName)"] = contactsLocation as? CLLocation
}
}
})
}
})
}
I apologize if I am missing or have an extra bracket somewhere. I have omitted some error checking code and so forth in order to get this down to bare-bones. So the goal of all that is to populate my contactsNearby Dictionary with data from CloudKit. A name as the key a location as the value. I want to use that data to populate a tableview. In the above code, the call to closeEnough is a call to another one of my view controllers methods to check if the contact from CloudKit has a location close enough to my user to be relevant to the apps purposes. Also myLocation is a variable that is populated before the segue. It holds the CLLocation of the app users current location.
The Problem:
The if statement:
if contactsName != "" && contactsLocation != nil { }
Appears to succeed. But my view controllers variable:
var contactsNearby: [String:CLLocation]?
Is never populated and I know there is data available in cloudKit.
If it's relevant here is some test code that I have in cellForRowAtIndexPath right now:
let contact = self.contactsNearby?.popFirst()
let name = contact?.key
if name != nil {
cell.textLabel?.text = name
}else {
cell.textLabel?.text = "nothing was there"
}
My rows alway populate with "nothing was there". I have seen answers where people have done CKQueries to update the UI, but in those answers, the user built the query themselves. That seems different from using a CloudKit function like discoverAllIdentities.
I have tried to be as specific as possible in asking this question. If this question could be improved please let me know. I think it's a question that could benefit the community.
Okay, I need to do some more testing, but I think I got it working. Thank you Paulw11 for your comment. It got me on the right track.
As it turns out there were 2 problems.
First, as pointed out I have an asynchronous call inside a for loop. As recommended, I used a dispatchGroup.
Inside the cloudKit call to discoverAllIdentities I declared a dispatchGroup, kind of like so:
var icloudDispatchGroup = DispatchGroup()
Then just inside the for loop that is going to make an async call, I enter the dispatchGroup:
icloudDispatchGroup.enter()
Then just before the end of the publicCloudDatabase.fetch completion handler I call:
icloudDispatchGroup.leave()
and
icloudDispatchGroup.wait()
Which, I believe, I'm still new to this remember, ends the dispatchGroup and causes the current thread to wait until that dispatchGroup finishes before allowing the current thread to continue.
The Above took care of the multithreading issue, but my contactsNearby[String:CLLocation]? Dictionary was still not being populated.
Which leads me to the 2nd problem
At the top of my view controller I declared my Dictionary:
var contactsNearby: [String: CLLocation]?
This declared a dictionary, but does not initialize it, which I don't think I fully realized, so when I attempted to populate it:
self.contactsNearby?["\(contactsName)"] = contactsLocation as? CLLocation
It quietly failed because it is optional and returned nil
So, in viewDidLoad before I even call populateContactsNearby I initialize the dictionary:
contactsNearby = [String:CLLocation]()
This does not make it cease to be an optional, which Swift being strongly typed would not allow, but merely initializes contactsNearby as an optional empty Dictionary.
At least, that is my understanding of what is going on. If anyone has a more elegant solution, I am always trying to improve.
In case you are wondering how I then update the UI, I do so with a property observer on the contactsNearby Dictionary. So the declaration of the dictionary at the top of the view controller looks like this:
var contactsNearby: [String: CLLocation]? {
didSet {
if (contactsNearby?.isEmpty)! || contactsNearby == nil {
return
}else{
DispatchQueue.main.sync {
self.nearbyTableView.reloadData()
}
}
}
}
I suppose I didn't really need to check for empty and nil. So then in cellForRowAtIndexPath I have something kind of like so:
let cell = tableview.dequeueReusableCell(withIdentifier: "nearbyCell", for: indexPath)
if contactsNearby?.isEmpty == false {
let contact = contactsNearby?.popFirst()
cell.textLabel?.text = contact?.key
}else {
cell.textLabel?.text = "Some Placeholder Text Here"
}
return cell
If anyone sees an error in my thinking or sees any of this heading for disaster, feel free to let me know. I still have a lot of testing to do, but I wanted to get back here and let you know what I have found.
I'm trying to update the user's music listen history by checking to see if any MPMediaItemPropertyPlayCounts have changed since the last check. Right now, upon each check, I'm querying the entire iPod library and comparing their existing play counts already in Core Data. If current play count != play count stored in Core Data, we do something with that song, since we know the user has listened to it recently.
In my code, I'm struggling to loop through all iPod songs and search for corresponding Core Data objects simultaneously. The code below prints out way too many lines. How do I search for objects in Core Data in a for loop?
class func checkiPodSongsForUpdate() {
var appDel: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
var context: NSManagedObjectContext = appDel.managedObjectContext!
var newSong = NSEntityDescription.insertNewObjectForEntityForName("IPodSongs", inManagedObjectContext: context) as! NSManagedObject
var request = NSFetchRequest(entityName: "IPodSongs")
request.returnsObjectsAsFaults = false
var results = context.executeFetchRequest(request, error: nil)
let query = MPMediaQuery.songsQuery()
let queryResult = query.collections as! [MPMediaItemCollection]
for song in queryResult {
for song in song.items as! [MPMediaItem] {
request.predicate = NSPredicate(format: "title = %#", "\(song.valueForProperty(MPMediaItemPropertyTitle))")
for result: AnyObject in results! {
if let playCount = result.valueForKey("playCount") as? String {
if playCount != "\(song.valueForProperty(MPMediaItemPropertyPlayCount))" {
println("\(song.valueForProperty(MPMediaItemPropertyTitle)) has a new play count.")
} else {
println("User hasn't listened to this song since last check.")
}
}
}
}
}
}
The immediate problem is that you assign a value to request.predicate, but doing so has no effect. A fetch request predicate only has effect if you assign it before doing the fetch. It becomes part of the fetch, and the fetch results only include objects that match the predicate.
There are a couple of possibilities for fixing this specific problem:
Do your Core Data fetch in the loop after assigning the predicate, inside the loop. This would get the results you expect. However it will also be extremely inefficient and won't scale well. Fetch requests are relatively expensive operations, so you'll probably find that you're spending a lot of time doing them.
Instead of assigning a value to request.predicate, filter the results array in memory by doing something like
let predicate = NSPredicate(format: "title = %#", "\(song.valueForProperty(MPMediaItemPropertyTitle))")
let currentResults = results.filteredArrayUsingPredicate(predicate)
That will get you an array of only the songs matching the predicate. This will be faster than option #1, but it still leaves the major problem that you're fetching all of the songs here. This potentially means loading a ton of managed objects into memory every time you call this function (suppose the user has, say, 50,000 songs?). It's still not a good solution. Unfortunately I don't know enough about MPMediaQuery to recommend a better approach.
Either of these should fix the immediate problem, but you need to address the more significant conceptual issue. You're comparing two potentially large collections looking for a single match. That's going to be a lot of CPU time or memory or both no matter how you approach it.
Found my own solution. Looping through two arrays can get expensive with enough objects in each array, just as #Tom said. I decided to loop through arrayOne and inside that loop, call a custom function that looks for the given key/value pair in arrayTwo.
// Custom function that returns Bool if arrayTwo contains a given key/value pair from inside the arrayOne loop
func arrayContains(array:[[String:String]], #key: String, #value: String) -> Bool {
for song in array {
if song[key] == value {
return true
}
}
return false
}
Big thanks to #vacawama for the help here: Find key-value pair in an array of dictionaries
I am new to iOS development using Swift language.
I am using CoreData in my app as a database option.
I am using NSFetchRequest for fetching records from the table. I can retrieve all records from the table, but I can't retrieve a specific row from the same table.
I have checked for the solutions online and other forums, but I am unable to get a proper solution for this.
I want to implement this in Swift only. I don't want to add the sqlite library or Bridging-wrapper (Objective - C) which will be my last option to implement this.
Any links or tutorials or suggestions will be helpful.
NSFetchRequest also support NSPredicate. With NSPredicate you can choose which exactly rows or row you need from Core Data.
More about NSPredicate - https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSPredicate_Class/
You can fetch a desired row by executing a fetch request, casting it as an array, and simply using subscript to retrieve an index. Or you can narrow down your results using a unique id and a predicate and getting the object at index 0.
if let results = managedObjectContext?.executeFetchRequest(fetchRequest, error: nil) as? [SomeClass] {
let someObject = results[someIndex]
}
As Pavel said, use NSPredicate. Here is a working example from my code, it might help you to get started ;) I added some comments below relevant lines
var req = NSFetchRequest(entityName: "YourDBTable")
// your db-table goes here
req.sortDescriptors = [NSSortDescriptor(key: "timeMillis", ascending: true)]
// if you want to sort the results
let start = NSNumber(longLong:int64StartValue)
// you need to convert your numbers to NSNumber
let end = NSNumber(longLong:int64EndValue)
let pred = NSPredicate(format:"timeMillis>=%# AND timeMillis <=%# AND foreignTableObject=%#",start,end, foreignTableObject)
// if you have relationships, you can even add another NSManagedObject
// from another table as filter (I am doing this with foreignTableObject here)
req.predicate = pred
var fetchedResults = managedContext?.executeFetchRequest(req,error: &error) as! [YourDBTable]?
// YourDBTable-class was created using the Xcode data model -> Editor -> Create NSManagedObject SubClass... - option
if let results = fetchedResults {
// iterate through your results
}