I'm trying to build a cart with core data I have created 3 objects to model my data Product, Quantity, and CartItem which holds a product and a quantity and I save them as a transformable object in core data.
I have created a class for my xcdatamodeld that have a CartItem attribute
#objc(CartItemContainer)
class CartItemContainer: NSManagedObject {
#NSManaged var cartItemAttribute: CartItem
}
I'm saving to core data with no problems, but whenever I try to update the quantity using the code below it doesn't change it would still be 1.
static func changeQuantity(product: Product) {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<CartItemContainer>(entityName: "CartItemContainer")
var results: [CartItemContainer] = []
do {
results = try context.fetch(fetchRequest)
if let productToChangeQuantityOn = results.first(where: {$0.cartItemAttribute.product.productID == product.productID}) {
let oldQuantity = productToChangeQuantityOn.cartItemAttribute.quantity.value
productToChangeQuantityOn.cartItemAttribute.quantity.value = oldQuantity + 1
try context.save()
context.refresh(productToChangeQuantityOn, mergeChanges: false)
}
}
catch {
print("error executing fetch request: \(error)")
}
}
I have tried updating it without calling context.refresh(productToChangeQuantityOn, mergeChanges: false)
it would change the quantity at run time, but when the app is relaunched the quantity would still be 1.
What am I missing here?
Any kind of help will be appreciated.
Update:
Here is how I set up Product for example. Quantity, and CartItem have the same set up.
class Product: NSObject, NSCoding, Decodable {
let productID: String
let category: String
let images: Dictionary<String, String>
let name: Dictionary<String, String>
let price: Dictionary<String, String>
func encode(with aCoder: NSCoder) {
aCoder.encode(productID, forKey: "productID")
aCoder.encode(category, forKey: "category")
aCoder.encode(images, forKey: "images")
aCoder.encode(name, forKey: "name")
aCoder.encode(price, forKey: "price")
}
required init?(coder aDecoder: NSCoder) {
self.productID = aDecoder.decodeObject(forKey: "productID") as! String
self.category = aDecoder.decodeObject(forKey:"category") as! String
self.images = aDecoder.decodeObject(forKey: "images") as! Dictionary<String, String>
self.name = aDecoder.decodeObject(forKey: "name") as! Dictionary<String, String>
self.price = aDecoder.decodeObject(forKey: "price") as! Dictionary<String, String>
}
}
Welcome to Core Data, Ahmed. I'm happy you got it working. Before you go, I'd like to suggest a more conventional data model which will probably work out better for you in the long run. No transformables are needed.
Broadly, the reasons are given in the first page of Apple's Core Data Programming Guide states 11 features of Core Data, listed as bullet points. By using transformable attributes instead of relationships, I'd say you are only fully realizing the advantages of the first and fifth bullet points.
Regarding your particular design, I assume that the same product can be in many Carts. By giving CartItem a product attribute of type transformable, this same product must be somehow reproduced in each cart. If you want to change the attributes of a product, in your design, you must find all the carts with this product in it and change each one. With the conventional design, you just change the Product object. Your design requires more code (which is always bad), and your users' devices will use more resources and be slower executing your code.
Last but not least, you don't want other engineers to be scratching their heads when they look at your code or data model :) And you want to be able learn from Apple documentation, tutorials, blog posts, stackoverflow answers and sample code you find on the internet. These resources are not as applicable if you are doing things in a non-conventional way.
Transformable attributes are a immutable type, therefore cannot be changed. The only way of changing them is by creating a new object and saving it again to core data.
To solve this I deleted the property cartItemAttribute from my xcdatamodel which holds both a product and a quantity as one transformable attribute. I replaced it with a product attribute of type transformable and a quantity attribute of type Int and everything works fine now.
Here is my updated code
static func changeQuantity(product: Product) {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<CartItemContainer>(entityName: "CartItemContainer")
do {
let results = try context.fetch(fetchRequest)
if let productToChangeQuantityOn = results.first(where: {$0.product.productID == product.productID}) {
let oldQuantity = productToChangeQuantityOn.quantity
productToChangeQuantityOn.quantity = oldQuantity + 1
try context.save()
context.refresh(productToChangeQuantityOn, mergeChanges: false)
}
}
catch {
print("error executing fetch request: \(error)")
}
}
Related
I wrote an app includes a main program A and an action extension B.
Users can use action extension B to save some data into UserDefaults.
like this:
let defaults = UserDefaults(suiteName: "group.com.ZeKai")
defaults?.set(self.doubanID, forKey: "doubanID")
defaults?.set(self.doubanRating, forKey: "doubanRating")
defaults?.set(self.imdbRating, forKey: "imdbRating")
defaults?.set(self.rottenTomatoesRating, forKey: "rottenTomatoesRating")
defaults?.set(self.chineseTitle, forKey: "chineseTitle")
defaults?.set(self.originalTitle, forKey: "originalTitle")
defaults?.synchronize()
Wating to transfer this data to main program A while A is opened.
But if I use action extension twice to save data like data1 and data2, then I open the main program A, only data2 is received by A, means always new data overwrite the new data in UserDefaults.
so, I would like to know whether I can save multiple data in UserDefaults, and they will be all transferred to main program A?
Thanks in advance...
To save multiple data to UserDefaults you should use unique keys for that data. Also read about synchronization:
Because this method is automatically invoked at periodic intervals,
use this method only if you cannot wait for the automatic
synchronization (for example, if your application is about to exit) or
if you want to update the user defaults to what is on disk even though
you have not made any changes.
If the data is descriptive as in case you describe. Try to use structures to represent models.
Example of the user struct:
public struct User {
public var name: String
init(name: String) {
self.name = name
}
public init(dictionary: Dictionary<String, AnyObject>){
name = (dictionary["name"] as? String)!
}
public func encode() -> Dictionary<String, AnyObject> {
var dictionary : Dictionary = Dictionary<String, AnyObject>()
dictionary["name"] = name as AnyObject?
return dictionary
}
}
Usage of the structures and UserDefaults.
let user1 = User(name: "ZeKai").encode()
let user2 = User(name: "Oleg").encode()
let defaults = UserDefaults(suiteName: "User")
defaults?.set(user1, forKey: "User1")
defaults?.set(user2, forKey: "User2")
defaults?.synchronize()
let user1EncodeData = defaults?.dictionary(forKey: "User1")
let user = User(dictionary: user1EncodeData as! Dictionary<String, AnyObject>)
this Struct is work in swift 2
I have a Swift 3 struct like this.
let tempContacts = NSMutableArray()
let arrayOfArray = NSMutableArray()
I have encode The Person Object in this for loop
for person in tempContacts as! [Person] {
let encodedObject: Data = NSKeyedArchiver.archivedData(withRootObject: person) as Data
arrayOfArray.add(encodedObject)
}
I have decode the data in this for loop
let tempContacts2 = NSMutableArray()
for data in arrayOfArray {
let person: Person = NSKeyedUnarchiver.unarchiveObject(with: data as! Data) as! Person
tempContacts2.add(person)
}
but unarchiveObject is always return nil value
First your model class should conform to the NSCoder protocol. The rest is really simple, there's no need to store the archived results for each object in an array, you can pass the initial array directly to NSKeyedArchiver like this :
class Person: NSObject, NSCoding {
var name = ""
init(name: String) {
self.name = name
}
// NSCoder
required convenience init?(coder decoder: NSCoder) {
guard let name = decoder.decodeObject(forKey: "name") as? String else { return nil }
self.init(name: name)
}
func encode(with coder: NSCoder) {
coder.encode(self.name, forKey: "name")
}
}
let tempContacts = [Person(name: "John"), Person(name: "Mary")]
let encodedObjects = NSKeyedArchiver.archivedData(withRootObject: tempContacts)
let decodedObjects = NSKeyedUnarchiver.unarchiveObject(with: encodedObjects)
As a side note : if NSCoder compliance is correctly implemented in your model class, you can of course use your way of archiving/unarchiving individual objects too. So your original code works too, with some minor adjustments:
for person in tempContacts {
let encodedObject = NSKeyedArchiver.archivedData(withRootObject: person)
arrayOfArray.add(encodedObject)
}
var tempContacts2 = [Person]()
for data in arrayOfArray {
let person: Person = NSKeyedUnarchiver.unarchiveObject(with: data as! Data) as! Person
tempContacts2.append(person)
}
Note 2: if you absolutely wants to use NSMutableArrays that's possible too, just define tempContacts like this:
let tempContacts = NSMutableArray(array: [Person(name: "John"), Person(name: "Mary")])
The rest is working without changes.
Note 3: The reason it used to work in Swift 2 and it's not working anymore in Swift 3 is that the signature for the NSCoder method func encode(with coder:) changed in Swift 3.
I have created two types of custom objects:
The first object, called Contact, has 4 fields which store Strings.
The second object, called ContactList, has 1 field which stores a list of my first object [Contact].
In my View Controller, I have an instance of a ContactList that I would like to save into User Defaults. Looking through other questions, I have added the following methods into the Contact class (as well as making it inherit from NSObject, and NSCoding)
required init(coder aDecoder: NSCoder) {
self.firstName = (aDecoder.decodeObject(forKey: "first") as? String)!
self.lastName = (aDecoder.decodeObject(forKey: "last") as? String)!
self.phoneNumber = (aDecoder.decodeObject(forKey: "phone") as? String)!
self.email = (aDecoder.decodeObject(forKey: "email") as? String)!
}
func encode(with aCoder: NSCoder){
aCoder.encode(self.firstName, forKey: "first")
aCoder.encode(self.lastName, forKey: "last")
aCoder.encode(self.phoneNumber, forKey: "phone")
aCoder.encode(self.email, forKey: "email")
}
And then in my ContactList class, I have added two functions:
func saveData(){
let data = NSKeyedArchiver.archivedData(withRootObject: list)
let defaults = UserDefaults.standard
defaults.set(data, forKey:"contacts" )
}
func retrieveData(){
if let data = UserDefaults.standard.object(forKey: "contacts") as? NSData
{
list = NSKeyedUnarchiver.unarchiveObject(with: data as Data) as! [Contact]
}
}
In my View Controller, I'm calling saveData() on my current instance of ContactList. In my ViewDidLoad method, I have an assignment statement to my variable which holds the instance of this by creating a new instance and then calling retrieveData() on it.
However, when I run my program and add elements to the list in ContactList's list field, exit out, and then come back into the app, the elements that I added is not there (I have a table that updates and displays the contents of the list in the ContactList).
Am I supposed to have ContactList inherit something, or am I just implementing these methods wrong? This is the first time that I'm using UserDefaults, so any help would be greatly appreciated!
I'm discovering new concepts as a fresh developer, I have been trying to understand core data, and run into an issue with a tutorial I've been walking through. I am getting an error when I call an item using the Object ID. The error is - Type 'Person.Type' has no subscript members. it may be because I am just not doing it correctly, or some other reason. I'm sure someone can shine some light on the subject
Here is a function I wrote to get a specific item out of the Data Stack,
func getObjectById(id: NSManagedObjectID) -> Person?{
return context.objectWithID(id) as? Person
}
Here is how I am calling the function
func callFirstObject(){
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let personService = PersonService(context: context)
let firstPerson = personService.getObjectById(Person[0].objectID!)
}
and from there I am just calling callFirstObject() inside a button.
Edit: I have a function to call all of my objects
func getAllObjects() -> [Person]{
return getObject(withPredicate: NSPredicate(value: true))
}
and a function to call all of my objects with a predicate
func getObject(withPredicate queryPredicate: NSPredicate) -> [Person]{
let fetchRequest = NSFetchRequest(entityName: Person.entityName)
fetchRequest.predicate = queryPredicate
do {
let response = try context.executeFetchRequest(fetchRequest)
print("\(response)")
return response as! [Person]
} catch let error as NSError {
// In case of failure
print("There was an error - \(error)")
return [Person]()
}
}
I am just trying to call a specific name in the stack.
If more information is needed, I am glad to provide.
Person has no subscript members because Person is the class name. The subscript, [0], should be called on an array of Person objects.
That could look something like this:
func callFirstObject(){
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let personService = PersonService(context: context)
// Person Array
let persons: [Person] = [person1, person2, person3 ]
let firstPerson = personService.getObjectById(persons[0].objectID!)
}
Edit: I'm kind of confused the logic though. If you have the person already, you have to in order to access the objectID, then I don't see why you need to fetch it again. Can we see some more context around these two methods?
Answering your question from the comment below:
If you want to get all of the records for the Person model you can do as follows:
let appDelegate =
UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Person")
do {
let results =
try managedContext.executeFetchRequest(fetchRequest)
people = results as! [NSManagedObject]
} catch let error as NSError {
print("Could not fetch \(error)")
}
This will give you all of the Person objects stored in Core Data
I have a group of arrays that all contain a number of instances of a custom Task object that I have created. I am saving the arrays to NSUserDefaults as follows:
Custom Task Object
class Task:NSObject, NSCoding {
var name:String
var notes:String
var date:NSDate
var dateUse:Bool
var taskCompleted:Bool
init(name:String, notes:String, date:NSDate, dateUse:Bool, taskCompleted:Bool){
self.name = name
self.notes = notes
self.date = date
self.dateUse = dateUse
self.taskCompleted = taskCompleted
}
required init(coder decoder: NSCoder){
self.name = decoder.decodeObjectForKey("name") as! String
self.notes = decoder.decodeObjectForKey("notes") as! String
self.date = decoder.decodeObjectForKey("date") as! NSDate
self.dateUse = decoder.decodeObjectForKey("dateUse") as! Bool
self.taskCompleted = decoder.decodeObjectForKey("taskCompleted") as! Bool
}
func encodeWithCoder(coder: NSCoder) {
coder.encodeObject(self.name, forKey: "name")
coder.encodeObject(self.notes, forKey: "notes")
coder.encodeObject(self.date, forKey: "date")
coder.encodeObject(self.dateUse, forKey: "dateUse")
coder.encodeObject(self.taskCompleted, forKey: "taskCompleted")
}
}
Saving:
let defaults = NSUserDefaults(suiteName: "group.com.myGroupName")
let nowData = NSKeyedArchiver.archivedDataWithRootObject(nowTasks)
defaults!.setObject(nowData, forKey: "nowData")
Retrieving
let nowDataPull = defaults!.objectForKey("nowData") as? NSData
if let nowDataPull2 = nowDataPull{
let nowTasks2 = NSKeyedUnarchiver.unarchiveObjectWithData(nowDataPull2) as? [Task]
if let nowTasks21 = nowTasks2{
nowTasks = nowTasks21
}
}
The above method works fine for setting and retrieving data from the iPhone itself. However, this method does not work when trying to retrieve the data via the today extension.
When trying to retrieve from the today extension's .swift file I get the following errors:
Failed to inherit CoreMedia permissions from 52550: (null)
Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '* -[NSKeyedUnarchiver
decodeObjectForKey:]: cannot decode object of class (MyAppName.Task)
for key (NS.objects); the class may be defined in source code or a
library that is not linked'
I know that the extension can read the data because when I call:
if (defaults?.objectForKey("nowData") != nil){
print("there is data")
}
I get the printed response..
I can successfully save an Integer and retrieve via the today extension, but not objectForKey
I have tried various other saving methods including .plist files, but nothing seems to work. The same errors keep occurring. Any input would be greatly appreciated!
another way is to fix the name of the class used for NSCoding. You simply have to use:
NSKeyedArchiver.setClassName("Task", forClass: Task.self before serializing
NSKeyedUnarchiver.setClass(Task.self, forClassName: "Task") before deserializing
wherever needed.
Looks like iOS extensions prefix the class name with the extension's name.
Set Object
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject("iOS", forKey: "userNameKey")
defaults.setInteger(25, forKey: "Age")
defaults.setBool(true, forKey: "UseTouchID")
defaults.setDouble(M_PI, forKey: "Pi")
Reading
let defaults = NSUserDefaults.standardUserDefaults()
let name = defaults.stringForKey("userNameKey")
Workaround:
So I have found a workaround (which is actually more efficient) for my problem. Because the decoding of [Task] was causing problems, I decided to go ahead and retrieve the data from an array that did not have objects inside of it, but rather just NSDate() instances. Because of this I can now save and access the important aspects of data that the today extension needs. I urge anyone else who is having issues with custom class decoding to try and simplify your data if at all possible in order to retrieve and use it in your extension. I am not exactly sure what the problem was or is, but retrieving simple data from an array stored in NSUserDefaults works without problems.
Hope all of this nonsense can help someone else out there!