Core Data with Different View Controllers - ios

I'm still not getting some concepts in Core Data. Bear with me, my app begins with a ViewController that takes some inputs from the user. Once the user clicks on a UIButton, it will transform him to the next ViewController. This is not the case, the problem is that I created an object of the core data inside the UIButton's action outlet:
// Creating a new username & Saving it to Core Data
let newUser = UserInfo(context: self.context)
newUser.name = usersName.text
newUser.birthday = bdPicker.date
newUser.gender = genderTextField.text!
newUser.prevTested = yesOrNoTextField.text!
newUser.score = 0
I declared the context as a global in the First ViewController:
var info:[UserInfo]?
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
Now, in the all the remaining ViewControllers, I need to modify the newUser.score entity. Since the app is question-and-answer, I need to modify the score each time I display a question to the user in each view controller. I tried so many solutions, but did not work. I've been told I should create newUser as a Property instead of a local variable. How is this possible?

One option could be to use the Singleton Pattern. Essentially, you create a new class, and create a static variable (by convention, often named shared) that's initialized to an instance of the class. Add a newUser property to this class, and then you can view or modify its state across all your ViewControllers.
class Singleton{
var shared = Singleton()
//Declare your newUser here
}
Additionally, if you are using CoreData in your application, I highly reccomend ommiting the use of the global variable for the context, and the placement of your CoreData save logic inside of the ViewController class. Instead, write another class, such as CoreDataManager or CoreDataService (You can call it whatever you'd like) and declare context as a property of that class so that it can be used in your create, fetch, and save operations.

Related

What happens to local variables storing references to deleted NSManagedObjects

When I delete an NSMangedObject from the database, what happens to local variables who were assigned to it?
For example, I have a simple NSManagedObject:
class MyManagedObject: NSManagedObject {
#NSManaged var name: String
}
And then in my ViewController, I pull it out of the database, and assign it locally:
class ViewController: UIViewController {
var myManagedObject: MyManagedObject!
}
Then I delete it from the database.
If print the object name I get the following in the console
print("myManagedObject.name = \(myManagedObject.name)")
//prints: "myManagedObject.name = "
As if the object isn't there? But if I turn the variable into an optional and check it for nil, I am told it's not nil.
I'm not quite sure how to reconcile this in my mind. There seems to be something pointing to the local variable, but its properties are gone.
If I have many disparate UI objects that rely on that object for its properties, I can't assume that there is some local deep copy of it in memory?
Here is more complete code:
In viewDidLoad I create the new object, save the context, fetch the object, then assign it locally.
class ViewController: UIViewController {
var myManagedObject: MyManagedObject!
override func viewDidLoad() {
super.viewDidLoad()
//1 Create the new object
let newObject = NSEntityDescription.insertNewObject(forEntityName: "MyManagedObject", into: coreDataManager.mainContext) as! MyManagedObject
newObject.name = "My First Managed Object"
//2 Save it into the context
do {
try coreDataManager.mainContext.save()
} catch {
//handle error
}
//3 Fetch it from the database
let request = NSFetchRequest<MyManagedObject>(entityName: "MyManagedObject")
do {
let saved = try coreDataManager.mainContext.fetch(request)
//4 Store it in a local variable
self.myManagedObject = saved.first
} catch {
//handle errors
}
}
}
At this point if I print the local variable's name property, I get the correct response:
print("The object's name is: \(myManagedObject.name)")
//prints: The object's name is: My First Managed Object
So, now I delete it from the database:
if let storedObject = myManagedObject {
coreDataManager.mainContext.delete(storedObject)
do {
try coreDataManager.mainContext.save()
} catch {
//handle error
}
}
But now, when I print I get the strangest output:
print("myManagedObject.name = \(myManagedObject.name)")
//prints: "myManagedObject.name = "
This is totally not the way I'm expecting memory to work. If I create a instance of a class Foo, and then pass that instance around to different objects, it's the same instance. It only goes away once no one is pointing to it.
In this case--- what is the variable, myManagedObject? It's not nil. And what is the string, name? Is it an empty string? Or is it some other weird meta-type?
The main thing what you are probably looking here for is the core data context. The context is a connection between your memory and the actual database.
Whenever you fetch the data you fetch it through context. These are managed objects which can be modified or even deleted. Still none of these really happen on the database until you save the context.
When you delete an object it is marked for deletion but it is not deleted from the memory, it must not be since if nothing else it will still be used by the context to actually delete the object from the database itself.
What happens to the managed object once you call to delete it is pretty much irrelevant, even if documented it may change as it is a part of the framework. So it is your responsibility to check these cases and to refetch the objects once needed. So you must ensure your application has a proper architecture and uses core data responsibly.
There are very many ways on how you use your database and more or less any of them has a unique way of using it optimally. You need to be more specific on what you are doing and where do you see potential issues so we can get you on the right track.
To give you an example consider data synchronization from remote server. Here you expect that data can be synchronized at any time no matter what user is doing or what part of the application he is.
For this I suggest you have a single context which operates on a separate thread. All the managed objects should be wrapped and its properties copied once retrieved from database. On most entities you would have something like:
MyEntity.findAll { items in
...the fetch happens on context thread and returns to main, items are wrappers
}
MyEntity.find(id: idString, { item in
...the fetch happens on context thread and returns to main, items are wrappers
})()
Then since you do not have any access to the managed object directly you need some kind of method to copy the data to the managed object like:
myEntityInstance.commit() // Copies all the data to core data object. The operation is done on a context thread. A callback is usually not needed
And then to save the database
MyEntity.saveDatabse {
... save happens on the context thread and callback is called on main thread
}
Now the smart part is that saveDatabse method will report to a delegate that changes have been made. A delegate is usually the current view controller so it makes sense to have a superclass like DataBaseViewController which on view did appear assigns itself as a delegate MyEntity.delegate = self, on view did load calls some method reloadData and in the databaseDidChange delegate method calls reloadData and same in viewWillAppear.
Now each of your view controllers that are subclass of DataBaseViewController will override the reloadData and in that method you will fetch the data from the database again. Either you are fetching all items or a single one. So for those singe ones you need to save the id of the object and fetch it again by that id. If the returned object is nil then item was deleted so you catch the issue you seem to be mentioning.
All of these things are oversimplified but I hope you get a basic idea about core data and how to use it. It is not easy, it never was and it most likely never will be. It is designed for speed to be able to access the data even from a very large database in shortest time possible. The result is that it might not be very safe.

Unacceptable type of value for to-one relationship: property = "user"; desired type = User; given type = User;

I'm having a wired problem with core data. With Swift3 in iOS 10 I get the managed object context each time I am fetching or storing data with
func getContext () -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
In my app I have two entities 'User' and 'Ledger'. I want to assign a ledger to a user, but a user can have multiple ledgers. Therefore, I have a UserTableView where I can display the users and a UserViewController class, where I create a user. The same I have for ledger. When creating a ledger I get also a list of all users from which I select one and which should assigned to the ledger and vice versa.
When saving like aforementioned, I get the error
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unacceptable type of value for to-one relationship: property = "user"; desired type = User; given type = User;
My data model looks as follows:
Data Model
Any help is highly appreciated:)
I had this same problem. In my case it was happening while I was running unit tests. In that case, there were two core data stacks in memory at once, one for the application harness and the other for my running unit tests.
The first clue to solving this was to put an assertion right before setting the relationship property to make sure the entity type of the object I was setting was the same as the expected entity type of the relationship. They should be identical, and in my case they were not.
In my case I had a MatchRequest that had a one-to-one relationship with Player called "initiator". So my assertion looked like the following:
let player = try Player.findLocal(for: matchRequest.initiator, in: moc, createIfMissing: true)
let expectedEntity = self.entity.relationshipsByName["initiator"]!.destinationEntity!
assert(player!.entity === expectedEntity, "Player returned doesn't have the same entity type")
self.initiator = player
The above assertion failed, which I suspect is similar to the assertion used by Core Data that causes the argument exception.
When inspecting Player.entity(), it would return the same entity instance that was causing the failure.
I think the root of the problem is that Core Data is setting some static property for entities that will get incorrectly shared across core data stacks. Calling MyManagedObject.entity() will work correctly when called from one stack but not the other.
So, to work around the problem, when I create my Player object to put into the relationship, I get the entity using the older NSEntityDescription.insertNewObject(...) API, rather than the newer MyManagedObject(context:) constructor. This ensures that the correct entity is used for the given managed object context.
So, to recap:
// SOMETIMES FAILS if you have more than one core data stack:
result = Player(context: managedObjectContext)
// ALWAYS WORKS:
result = NSEntityDescription.insertNewObject(forEntityName: "Player", into: managedObjectContext) as? Player
I had the same issue, but i was very sure I didn't have 2 Core Data Stacks like the previous answer did.
In the end i realized i had initialized a reference to NSStoreCoordinator and a view context and background context using the lazy keyword. I also had a bunch of code doing some heavy lifting in the background thread and saving using said stack.
Apple's docs say: "If a property marked with the lazy modifier is
accessed by multiple threads simultaneously and the property has not
yet been initialized, there is no guarantee that the property will be
initialized only once."
So, this unfortunate scenario is another way to get to this error. Solution: make sure don't lazy anything in your Core Data Stack specially when doing lots of background/foreground thread work.
I hope it helps someone in the future. To you future developer, if you are here: Good luck.
I came across the same issue. I think #Chris has given the reason clearly. There are more than one instance in the memory while doing the testing.
The solution is moving the NSPersistentContainer init part into setUpWithError and tearDownWithError.
In my example below, CoreDataManager take care of the init of NSPersistentContainer.
Here is the code:
Before
class FooTests: XCTestCase {
var manager: CoreDataManager = CoreDataManager()
}
after
class FooTests: XCTestCase {
var manager: CoreDataManager?
override func setUpWithError() throws {
manager = CoreDataManager(inMemory: true)
}
override func tearDownWithError() throws {
manager = nil
}
}

Best way to store a single instance of RLMObject

I'm currently developing an iOS app with Realm as the backend data storage. I have a class that is an RLMObject for the user profile. It stores their name, profile picture, stats, etc.
There should only always be one of these objects, however I know implementing a singleton pattern is usually a bad idea. Currently I have it implemented as
//AppDelegate.swift, applicationDidFinishLaunching
//If there's no user profiles yet (first time launch), create one
if UserProfile.allObjects().count == 0 {
let realm = RLMRealm.defaultRealm()
try! realm.transactionWithBlock() {
realm.addObject(UserProfile())
}
}
//ProfileViewController.swift
//Get the first UserProfile
var userProfile: UserProfile? {
get {
return UserProfile.allObjects().objectAtIndex(0) as? UserProfile
}
}
Is there a better way to keep track of a single instance of this class?
Your code sample uses a computed property, which will fetch the object from the Realm each time you access it.
Instead, try using a lazy var property:
lazy var userProfile: UserProfile? = {
return UserProfile.allObjects().objectAtIndex(0) as? UserProfile
}()
This type of property will load the object from the Realm only the first time it is accessed. All subsequent accesses will be directly to the object.
Note that, since UserProfile is a Realm object, its fields will automatically update in response to changes made to the underlying object in the Realm. Likewise, any changes you wish to make will need to be wrapped within a Realm write transaction.
In terms of your overall architecture, there is nothing wrong with storing a single instance of an object type in a Realm similar to what you are doing. You may want to give your UserProfile object a hardcoded constant primary key, then use the 'add or update' version of the update API (see https://realm.io/docs/swift/latest/#updating-objects). This will allow you to avoid having to explicitly create a new object.

Core Data and a content manager

First time working with Core Data en its .xcdatamodeld
I am using a contentManager for a notes app.
But I am difficulties creating standerd notes.
Is a contentManager normally used for core data? My Problem is where the ?? is.
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let entity = NSEntityDescription.insertNewObjectForEntityForName("Note", inManagedObjectContext: ?? ) as! Note
}
It isn't exactly clear what the issue is, but it looks like you simply need to replace ?? with managedObjectContext. But, this code should be in a function, not just as variable definitions in the class itself. If you did want to make them variables then you can make them lazy so they're populated on demand the first time they're used.
As for your content manager, I'm guessing you mean some kind of data controller. That's generally a matter of personal preference and how long you've been developing. Generally you want to move the context ownership and management out of the app delegate as that's an inappropriate place to have it and using a data controller for that is clearly better.

How to pass json data in swift app between two viewcontrollers

I need help with passing json data.I have json data in this array
var TableData:Array< String > = Array < String >()
In this Array I have Name,Address,Latitude, Longitude
I show Name and Address in a tableView , but I would like to create annotation in different viewController with Latitude and Longitude depending on which cell user taps(Name,Adress,latitude,Longitude shoulld be equal) , so I am asking you if there is some good tutorial in swift , or if you have just some advice.
Thank you.
There are many different ways to pass data from one swift file to another. In the case that there is a rootViewController and a button is clicked to open a new ViewController. Having previously defined an array in the new ViewController, the json parsed array can be passed along using the prepareForSegue method.
Another popular way to pass information between different swift files would be using the AppDelegate. Here you can create instances of different swift classes using a method known as instantiateViewControllerWithIdentifier("identifier"). This can be done by creating a storyboard variable then calling this method by doing storyboard.instantiateViewControllerWithIdentifier("identifier").
let newvc = self.storyboard?.instantiateViewControllerWithIdentifier("newvcIdentifier") as! (UIViewController extended class)
newvc.data = TableData
self.navigationController?.pushViewController(newController, animated: true)
where newvc has a variable declared as follows:
var data: Array <String>!
Another method that can be used is having shared data among all of the classes using a singleton. A singleton can be created very simply in swift3, take a look here for details.
class JSONData {
var json: Array <String>
static let sharedInstance = JSONData()
}
Before segueing to the next vc, you should store the data in the sharedInstance class. This should be done by overriding the prepare method. Documentation on this method can be found here
sharedInstance.json = self.json
Once the data is set in the shared instance, after the new view controller is loaded, this data can be accessed through sharedInstance.json.
Hope this works for you and just comment if you have any other questions!
I would do something like this:
let newController = self.storyboard?.instantiateViewControllerWithIdentifier("newControllerIdentifier") as! NewControllerClassName
newController.data = array
self.navigationController?.pushViewController(newController, animated: true)
It also appears you are using a array of string type that are comma separated.
I would rather create a variable like below
var jsonArray:[[String:String]]?
so I am asking you if there is some good tutorial in swift
http://matteomanferdini.com/how-ios-view-controllers-communicate-with-each-other/
He covers clean code and best practices. Better read this before implementing anything.
or if you have just some advice
Your code needs to be clean. Passing latitude as String it's going to create you a problem later (conversions, code understanding, or even bugs).
Make a new account here and watch uncle Bob. His lessons are valuable. matteomanferdini above follow uncle bob practices.
https://www.safaribooksonline.com/library/view/clean-code/9780134661742/

Resources