I am developing an app in which the user can create a List which will contain a number of Items. My CoreData Model is as follows.
The code below gets the list.contains relationship and store it as an itemsOnList Set and then populate a UITableView with each Item in the Set
func getItemsOnList(){
let app = UIApplication.shared.delegate as! AppDelegate
let context = app.persistentContainer.viewContext
//fetchRequest to get the List
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "List")
let predicate = NSPredicate(format: "title == %#", listName)
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.predicate = predicate
if let fetchResults = try? context.fetch(fetchRequest){
if fetchResults.count > 0 {
for listEntity in fetchResults {
let list = listEntity as! List
print(list.title as Any)
itemsOnListSet = list.contains!
}
}
}
}
I want the user to be able to change either the name or quantity of the item.
My first way of doing this was since i already have the NSSet of Items i would just change the Item i want and then save the old list.contains with the new one.
Will that be a wise way to do it or is there an easier and simpler way of doing this?
Thanks
Rename contains to items so it describes what it is. (edit: also rename belongsTo to list)
So to access the items linked to a list it will just be list.items, much clearer.
Now if you want to edit an item just grab it out of that set and edit away, the relationships don't change. You only need to touch list.items again if you wanted to add or remove an item.
Related
In Core Data, I have a large number of "City" objects, each of which has a "country" property.
Rather than have a very long scrollable list of cities, I need to have a separate screen which lists just Countries in a UITableView. The user then selects a country and is taken to a new screen, which has the Cities for that Country.
Note: This is for a game, so the UITableView data source is bound to a SKScene, but I don't think that should make any difference as it works fine for other screens.
I have figured out the detail screen which displays the Cities for a specific country and it works fine in test with hard-coded data.
I am using Swift 5 so my data calls are like this:
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "City")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
fetchRequest.predicate = NSPredicate(format: "country = %#", country)
try fetchedResultsController.performFetch()
However for the other screen, I don't know how to "group" the Countries, which are the property on each City, and make the groupings the data source for the UITableView on the other screen.
a) I could create an array of Countries, but how would I have the fetchedResultsController return the Country data to UITableView?
b) Or, is it possible to group or filter objects within a fetchedResultsController?
c) Or, is it possible to do SQL-like grouping within a fetch request?
Thanks
Thanks #Larme for the link to https://developer.apple.com/documentation/coredata/nsfetchrequest/1506191-propertiestogroupby
So to get this working, Results Container needs to be defined with NSDictionary.
var fetchedResultsController: NSFetchedResultsController<NSDictionary>!
The fetchRequest needs to be set to group and fetch the specific property.
fetchRequest.propertiesToFetch = ["country"]
fetchRequest.propertiesToGroupBy = ["country"]
fetchRequest.resultType = .dictionaryResultType
Then in the methods where we need to access the data:
// cell data
let countryName = (fetchedResultsController.object(at: indexPath) as NSDictionary)["country"]!
cell.textLabel?.text = "\(countryName)"
Thanks for pointing me in the right direction!
TL;DR EDIT with answer
As Wain perfectly answered this is how I get my information now:
let ingredientsToRecipe = recipe.valueForKey("ingredientsToRecipe")! as! NSSet
for i in ingredientsToRecipe {
print(i.valueForKey("amount")!)
print(i.valueForKeyPath("ingredient.name")!)
}
Original question
I have a huge problem understanding the usage of intermediate tables in CoreData. I've searched SO for answers and found a few threads about intermediate tables and many-to-many relations but those where either Objective-C or didn't help me.
I have the following setup (simplified):
Now I want to add a new recipe with a bunch of ingredients.
Let's say a Burger. The burger consists of
1 cocumber,
1 Tomato,
1 Meat,
2 bread
(yummy...)
This is what I tried so far:
// Core Data
let appDelegate =
UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let entity = NSEntityDescription.entityForName("Recipe",
inManagedObjectContext:managedContext)
// creating a new recipe with name and id
let recipe = NSManagedObject(entity: entity!,
insertIntoManagedObjectContext: managedContext)
recipe.setValue("Burger", forKey: "name")
recipe.setValue("B_001", forKey: "id")
Now I got an Array:[NSManagedObject] of of ingredients (created just like the Burger) and a Dictionary of amounts to the ingredient_IDs. This is how I'm trying to marry my Recipe with the ingredients (over the intermediate table).
for i in selectedIngredients { // the ingredient array
let ingredientsToRecipe = NSEntityDescription.insertNewObjectForEntityForName("RecipeIngredient", inManagedObjectContext: managedContext)
ingredientsToRecipe.setValue(i, forKey: "ingredient")
ingredientsToRecipe.setValue(recipe, forKey: "recipe")
let quantity = Double(quantityDictionary[(i.valueForKey("id") as! String)]!) // the amount-to-ID dictionary
ingredientsToRecipe.setValue("\(quantity)", forKey: "quantity")
}
In the end I simply save everything:
do {
try managedContext.save()
print("Saved successfully")
self.dismissViewControllerAnimated(true, completion: nil)
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
}
All this above somehow works. But now I'm struggling to fetch information about my recipes.
How am I supposed to fetch the amount of tomatoes of this specific burger?
Things like
recipe.valueForKey("RecipeIngredient").valueForKey("amount") work but I don't know which amount is from which ingredient.
Am I doing anything wrong?
What can/should I do better?
The goal is to create a recipe with ingredients and later populate a Table with information about the recipe and the amounts of it's ingredients (and the ingredients themselves).
I appreciate any help!
The power of the intermediate object is that it takes your many-to-many relationship and breaks it into multiple one-to-many relationships. The to-one relationships are easy to navigate.
So, from your Recipe you can get an array of RecipeIngredients, and for each one you can get valueForKey("amount") and valueForKeyPath("ingredient.name").
For you to get the amount of an ingredient for a specific recipe you can create a fetch request at RecipeIngredient using predicates like this :
var request = NSFetchRequest(entityName: "RecipeIngredient")
let predicate = NSPredicate(format: "recipe.name = %# AND ingredient.name = %#", "burger","tomato")
request.predicate = predicate
Then you simply get tha amount value from the returned RecipeIngredient entity.
You don't need to use valueForKey and valueForKeyPath to access these kinds of properties... Rather, you should let Core Data do the work of traversing the relationships for you, and just ask for what you need using dot syntax:
for item in recipe.ingredientsToRecipe {
print(item.amount)
print(item.ingredient.name)
}
I suggest that you rename your intermediate entity from IngredientsToRecipe (which is thinking like database design) to Items or ReceipeItems to better capture what it is—the item that actually appears in a recipe, rather than the underlying food type itself.
But whether you do that or not, you could certainly name the relationship on Receipe to be items, resulting in the much more readable:
for item in recipe.items {
print(item.amount)
print(item.ingredient.name)
}
You could also go further and create a computed property on the intermediate entity called name that simply returned ingredient.name, which would then let you use:
for item in recipe.items {
print(item.amount)
print(item.name)
}
:)
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
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.
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
}