say i have one view controller (VCA), which segues to another another view controller (VCB). while preparing to segue, VCA passes its managedObjectContext (i.e. following the "tell don't ask" convention). VCA also has a function saveManagedObjectContext() that performs the saving and error handling.
so, in VCA:
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
...
func saveManagedObjectContext() {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
abort()
}
}
}
...
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
...
if let vcb = segue.destinationViewController as? VCB {
vcb.managedObjectContext = managedObjectContext
}
...
}
and in VCB:
var managedObjectContext: NSManagedObjectContext? = nil
say VCB adds some entity to the managedObjectContext that was passed to it from VCA
func createSomeEntity() {
let entity = NSEntityDescription.entityForName("SomeEntity", inManagedObjectContext: managedObjectContext!)
something = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: managedObjectContext) as? SomeEntity
}
is there any (correct) way to call the saveManagedObjectContext() function from VCA rather than having to copy the function over to VCB?
thanks
An alternative is a singleton class.
Create a new Swift file, replace the predefined code with
class CoreDataManager: NSObject {
// MARK: - Shared Instance
class var sharedManager : CoreDataManager {
struct Singleton {
static let instance = CoreDataManager()
}
return Singleton.instance
}
// MARK: - Core Data stack
lazy var ...
}
and then replace lazy var ... with the entire Core Data stack from AppDelegate.
Now you can access Core Data from everywhere using
let managedObjectContext = CoreDataManager.sharedManager.managedObjectContext
or to call the save action
CoreDataManager.sharedManager.saveAction(self)
Good point about "tell don't ask". A lot of Apple sample code evangelizes this concept. However, in recent years, Apple has also provided sample code with a Core Data stack class that handles the object graph, practically abandoning the "tell don't ask" pattern.
Also, in many popular and acclaimed open source projects, this pattern is used. In most cases you reduce the code and still have a robust solution. For example, in more complex projects with nested background contexts it is often the only feasible setup.
Thus, I would recommend to create a CoreDataManager class that handles the core data stack, or for less complex apps (single, main thread context) use the app delegate.
Note that in Swift, you can really make this very concise with global variables (which you should use sparingly!). E.g., on top of the AppDelegate.swift you could write
let SharedAppDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
(I like to capitalize my global singletons). You can use this as follows
do { try SharedAppDelegate.context.save() } catch {}
Related
I am new to macOS Development and I am working on a project for macOS using Xcode 10 and Swift 4.2.
The project contains mainly 3 view controllers.
Firstly, ViewController (the main welcome screen which separates the other two) has two buttons to call the other two respectively.
Secondly, MakeEntry View Controller creates an array of strings data variable using a form type structure comprised of text views and save button etc. which in the end just saves all input data into an array of strings data variable called carrierArray
Thirdly, there is a split view controller for displaying two children view controller namely EntryList and EntryDetail
EntryList (the left pane) contains a Table View to display titles of entries and EntryDetail (the right pane) will contain the description of the title entry (somewhat like the default notes app of macOS)
I want to achieve a simple functionality of being able to access or read that Array of strings variable called carrierArray which is created when the MakeEntry view controller saves it into a global variable defined within its own class file But I want to access that array of strings anywhere and anytime later.
I cannot use delegates and protocols, closures, segues or storyboard identifiers to carry that data because I am not navigating to the Split View Controller straightaway and also because I want to store that data
to manipulate it further before displaying it in the right pane of split view controller (EntryDetail) .
I am unable to figure out whether how it might be possible to achieve this functionality using NSUserDefaults or CoreData.
Therefore I tried using the Notification Centre after storing that array of Strings in a Dictionary namely notifDictionary containing a key called carryData to be stored as the data object of notification centre And with some research and some trials and errors but without any luck all resulting in failure to get that data in the split view controller left pane class file namely (EntryDetail).
Code Snippets are as below, thanks a lot in advance for the kind help.
In MakeEntry View controller:
notifDictionary = ["carryData": carrierArray]
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "dataCarrier") , object: nil, userInfo: notifDictionary)
In EntryList View Controller:
(Tried using both types of selector methods one at a time and even using them together but all without luck! Please Help!)
The Variable datumDict and datumArray and nothing but copy receivers for carrierArray and notifDictionary
var datumDict: [String:[String]] = [:]
var datumArray: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(self.getThatDict(_:)), name: NSNotification.Name(rawValue: "dataCarrier") , object: nil)
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "dataCarrier") , object: nil, queue: nil, using: catchNotification)
//datumArray = datumDict["carryData"]!
}
#objc func onNotification(notification:Notification)
{
print(notification.userInfo!)
}
func catchNotification(notification: Notification) -> Void
{
let theDict = notification.object as! NSDictionary
datumDict = theDict as! [String: [String]]
guard let theData = notification.userInfo!["carryData"] as? [String:[String]] else { return }
datumDict = theData
}
#objc func getThatDict(_ notification: NSNotification)
{
print(notification.userInfo ?? "")
if let dict = notification.userInfo as NSDictionary?
{
if let thatDict = dict["carryData"] as? [String: [String]]
{
datumDict = thatDict
}
}
}
With the caveat that "globals and singletons should be avoided," it sounds like they are a good fit for what you're trying to do. As you get more comfortable in Cocoa you can move into more sophisticated means of accomplishing this (dependency injection). Look into this as you get more comfortable with Cocoa.
Create a simple singleton type:
// AppState.swift
class AppState {
// Basic singleton setup:
static let shared = AppState()
private init() {} // private prevents instantiating it elsewhere
// Shared state:
var carrierArray: [String] = []
}
Access it from your view controllers:
// YourViewController.swift:
#IBAction func doSomething(_ sender: Any) {
AppState.shared.carrierArray = ...
}
If you need to update the other view controllers when this shared state changes, notifications are a good tool for that. You could do this with a didSet on carrierArray, or simply trigger the notification manually.
I am hoping someone can explain why associated objects in the following example do not get automatically deallocated when the source/host object is deallocated. This example code below is somewhat contrived (apologies in advance), but it explains the my issue.
The example assumes a CoreData entity Product with an string attribute sku and the default CoreData stack provided by the Xcode template:
import UIKit
import CoreData
class ViewController: UIViewController {
#IBAction func createProduct(sender: AnyObject) {
let context = CoreDataHelpers.vendBackgroundWorkerContext()
let newProduct = CoreDataHelpers.newProduct(context: context)
newProduct.sku = "8-084220001"
do {
try newProduct.managedObjectContext?.save()
print("Product created [SKU: \(newProduct.sku ?? "NotDefined")]")
} catch {
print(error)
}
}
}
public class CoreDataHelpers {
public static let mainContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
public class func vendBackgroundWorkerContext() -> NSManagedObjectContext {
let managedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
managedObjectContext.parentContext = self.mainContext
return managedObjectContext
}
class func newProduct(context context: NSManagedObjectContext) -> Product {
let newProduct = NSEntityDescription.insertNewObjectForEntityForName("Product", inManagedObjectContext: context) as! Product
return newProduct
}
}
when the createProduct function is executed, a new PrivateQueueConcurrencyType Managed Object Context (MOC) will be vended and used by the new Product Managed Object (MO). This above code works correctly - so far.
However! if I combine the first two lines of the createProduct function such that:
let newProduct = CoreDataHelpers.newProduct(context: CoreDataHelpers.vendBackgroundWorkerContext())
then the app will crash at try newProduct.managedObjectContext?.save() with a EXC_BAD_ACCESS.
At first glance, this appears a little strange - as all we have done is refactored the code. Digging into the documentation, the managedObjectContext property is declared as unowned(unsafe). This probably means that the created MOC has been deallocated and we have a dangling pointer (please correct me if my assumption is wrong).
In order to ensure that MOC does not get deallocated, I tried associating it with the MO itself. newProduct:
class func newProduct(context context: NSManagedObjectContext) -> Product {
let newProduct = NSEntityDescription.insertNewObjectForEntityForName("Product", inManagedObjectContext: context) as! Product
var key: UInt8 = 0
objc_setAssociatedObject(newProduct, &key, context, .OBJC_ASSOCIATION_RETAIN)
return newProduct
}
This appears to works wonderfully - until I check in Instruments. It would appear that when the Product MO is deallocated, the now associated MOC is not (shouldn't it be automatically deallocated when the source object is deallocated?)
My question is:
Can someone explain where the additional reference is to the MOC that is preventing it from being deallocated? Have I created a retain cycle between the MO and the MOC?
You are probably creating a circular ownership (retain cycle).
Every managed object is owned by a managed context (the context owns the object) and setting the context as associated object means that the object now also owns the context.
Therefore, they won't get deallocated.
The real solution is to save the background context to a local property, the same you are doing with mainContext.
I'm banging my head for some time due to this issue. I precise my scenario in detail.
I have a table view where I can add data using a popover which gets displayed on clicking the '+' button in the navigation bar. I get the values from the popover but where I'm stuck is, the data received is not getting reflected in the tableview. If I move back and forth it gets displayed. Tried to reload the table with different possibilities but nothing works.
If you do want a taste of my code, you can get it here Data stored fails to display in the table view, in one to many relationship of core data?
Could anyone solve my problem, help is very much appreciated.
The idea here is to provide a way for the Add Teams popover view controller to tell the Team table view controller to reload its table view.
In the Add Team VC swift file, define a protocol:
protocol AddTeamsDelegateProtocol {
func didAddTeam()
}
In the Add Team class, add a new delegate property which of this type:
var delegate : AddTeamsDelegateProtocol? = nil
In the same class, call the delegate method when the new Team is saved:
#IBAction func submit(sender: AnyObject) {
let entity = NSEntityDescription.entityForName("Teams", inManagedObjectContext: managedObjectContext)
let team = Teams(entity: entity!, insertIntoManagedObjectContext: managedObjectContext)
team.teamName = teamNamePO.text
team.teamImage = teamImagePO.image
do{
try managedObjectContext.save()
} catch let error as NSError{
print("\(error), \(error.userInfo)")
}
self.delegate?.didAddTeam()
dismissViewControllerAnimated(true, completion: nil)
}
In the Team table view controller, implement the didAddTeam() method:
func didAddTeam() {
let request = NSFetchRequest(entityName: "Teams")
do{
teamData = try managedObjectContext.executeFetchRequest(request) as! [Teams]
} catch let error as NSError {
print("\(error), \(error.userInfo)")
}
self.tableView.reloadData()
}
Ensure that the Team table view controller conforms to the protocol
class GroupTable: UITableViewController, NSFetchedResultsControllerDelegate, AddTeamsDelegateProtocol {
Before segueing to (or presenting) the Add Teams popover (I couldn't see how this is done in your code in the other question), set the Add Teams controller's delegate:
addTeamsVC.delegate = self
In Xcode 6 (7 too), I'm looking to find out how to get managedObjectContext and use it in a ViewController in a different scene.
I started a new Cocoa project, clicked 'Use CoreData'
Added an entity in CoreData, generated a managedObject for it
Added an array controller, and bound it to the entity
I want to bind the array controller to the managedObjectContext, but I also want to bind the tableView (in my view controller) to the array controller. They are in different scenes, which seems to mean different namespaces.
What I've tried to do:
Set an instance variable in ViewController:
var managedObjectContext: NSManagedObjectContext!
In viewDidAppear() I added:
if let app = NSApplication.sharedApplication().delegate! as? AppDelegate {
if let context = app.managedObjectContext{
managedObjectContext = context
} else {
print("There was no context available, didn't work")
}
}
Then I bound the columns of the table to the properties of the entity. And cocoa bindings autocompleted, meaning the context was at least recognized properly.
However when I run it, it fails silently with: 'Cannot perform operation without a managed object context'. When debugging the context is being set as a real object, but I have no idea if it's actually initialized. I looked through the docs and cocoa binding troubleshooting but this seems to be a coredata issue.
(I've looked here: Getting managedObjectContext from AppDelegate but I can't override the normal init in swift)
I made one example for you. I have one Entity, called Person with 2 attributes, name and age. This is my ViewController:
import Cocoa
class ViewController: NSViewController {
// ManagedObjectContext from AppDelegate
lazy var moc: NSManagedObjectContext = {
let appDel = NSApplication.sharedApplication().delegate as! AppDelegate
return appDel.managedObjectContext
}()
override func viewDidLoad() {
super.viewDidLoad()
// Populate some sample data
let firstPerson = NSEntityDescription.insertNewObjectForEntityForName("Person", inManagedObjectContext: moc) as! Person
firstPerson.name = "Jesse Pinkman"
firstPerson.age = 25
}
}
And in IB i have one table view and one array controller. Set array controller's entity to Person:
And the bound your array controller's managed object context to your viewcontroller's managed object context, in my example self.moc:
And then just bound your table view's colum's to your array controller.
For what it's worth, you can bind to the Application in IB, with a model key path of self.delegate.managedObjectContext. This is the quick and dirty way.
Some people argue that this is bad, mostly because they think the app delegate shouldn't have the MOC in the first place. I think the app delegate owning the core data stack is fine.
But I'd warn it's not future-proof. If you want to do something like create a child MOC for the VC, and have an easy "undo" of all the local changes in that scene by skipping the save, you'd end up creating and maintaining the moc property on the VC.
I've spent the last 4 days trying to implement a proper Core Data stack with iCloud sync for my Swift 1.2 app, but I can really use some help.
Before, I was using a global Managed Context accessed from everywhere in the app; knowing that it was a bad implementation, now that I'm adding iCloud sync I decided to get rid of it, even though the app was working fine.
So far, I've implemented a new, working Core Data stack with decent - but not perfect - cloud sync between devices.
Now I face two issues:
Sometimes, a few objects don't sync.
Given the particular structure of my app, which I'll explain in a moment, I have no idea how and where in my code I should handle the notifications that Core Data sends when the user logs in or out of iCloud.
But, before tackling those problems, I'd really appreciate - if appropriate - some validation of the work I've done so far and it is mainly for some confirmations that I'm writing this: since I've already spent a lot of time changing my Core Data stack, before going forward I'd like to know if I'm propagating the context properly (the structure of my app doesn't conform to any tutorial I found online, so I had to improvise a bit), or if I made some basic mistakes that will compromise reliable syncing or the future development.
My app is structured as follow:
UITabBarViewController as initial ViewController
1st tab: UIViewController (shown when the app starts)
2nd tab: a UITableViewController embedded in a UINavigationController
3rd tab: another UITableViewController embedded in another UINavigationController
I have a CoreDataStack.swift class with the following code:
import CoreData
#objc class CoreDataStack : Printable {
let context : NSManagedObjectContext
let psc : NSPersistentStoreCoordinator
let model : NSManagedObjectModel
let store : NSPersistentStore?
var description : String {
return "context: \(context)\n" + "model: \(model)"
}
var applicationDocumentsDirectory : NSURL = {
let fileManager = NSFileManager.defaultManager()
let urls = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask) as! [NSURL]
return urls[0]
}()
init() {
let modelURL = NSBundle.mainBundle().URLForResource("MyDataModel", withExtension:"momd")
model = NSManagedObjectModel(contentsOfURL: modelURL!)!
psc = NSPersistentStoreCoordinator(managedObjectModel: model)
context = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.MainQueueConcurrencyType)
context.persistentStoreCoordinator = psc
let documentsURL = applicationDocumentsDirectory
let storeURL = documentsURL.URLByAppendingPathComponent("MyApp.sqlite")
let options = [NSPersistentStoreUbiquitousContentNameKey: "MyApp", NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true]
var error: NSError? = nil
var failureReason = "There was an error creating or loading the application's saved data."
store = psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: options, error:&error)
if store == nil {
let dict = NSMutableDictionary()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error
error = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict as [NSObject : AnyObject])
println("Error adding persistent store: \(error), \(error!.userInfo)")
abort()
}
}
func saveContext() {
var error: NSError? = nil
if context.hasChanges && !context.save(&error) {
println("Could not save: \(error), \(error!.userInfo)")
}
}
var updateContextWithUbiquitousContentUpdates: Bool = false {
willSet {
ubiquitousChangesObserver = newValue ? NSNotificationCenter.defaultCenter() : nil
}
}
private var ubiquitousChangesObserver : NSNotificationCenter? {
didSet {
oldValue?.removeObserver(self, name: NSPersistentStoreDidImportUbiquitousContentChangesNotification, object: psc)
ubiquitousChangesObserver?.addObserver(self, selector: "persistentStoreDidImportUbiquitousContentChanges:", name: NSPersistentStoreDidImportUbiquitousContentChangesNotification, object: psc)
}
}
func persistentStoreDidImportUbiquitousContentChanges(notification: NSNotification) {
println("Merging ubiquitous content changes")
context.performBlock {
self.context.mergeChangesFromContextDidSaveNotification(notification)
}
}
}
In my AppDelegate.swift I added the following code just under var window: UIWindow?:
lazy var coreDataStack = CoreDataStack()
coreDataStack.updateContextWithUbiquitousContentUpdates = true
// The following code is the way I found to propagate the managed context of the stack instantiated above in all the ViewControllers of the UITabBarController, including those embedded in the two NavigationControllers;
// since in the future I'll probably need some flexibility in term of adding / rearranging the VCs in the TabBar, I kind of like this way to pass around the context.
// I could have also passed the context to the CustomTabBarViewController and from there do the same thing, but I figured I could just pass the context from AppDelegate, since I already can access all the ViewControllers from here with the following code.
var tabBarController = self.window!.rootViewController as! CustomTabBarViewController
for eachViewController in tabBarController.viewControllers! {
if eachViewController.isKindOfClass(CustomViewController){
(eachViewController as! CustomViewController).passedManagedContext = coreDataStack.context // Context is passed to the VC of 1st tab
}
if eachViewController.isKindOfClass(UINavigationController){
var firstNavController = tabBarController.viewControllers![1] as! UINavigationController
for tvc in firstNavController.viewControllers! {
if tvc.isKindOfClass(FirstCustomTableViewController) {
(tvc as! FirstCustomTableViewController).passedManagedContext = coreDataStack.context // Context is passed to the TableVC inside the NavigationController in tab 2
}
}
var secondNavController = tabBarController.viewControllers![2] as! UINavigationController
for tvc in secondNavController.viewControllers! {
if tvc.isKindOfClass(SecondCustomTableViewController) {
(tvc as! SecondCustomTableViewController).passedManagedContext = coreDataStack.context // Context is passed to the TableVC inside the NavigationController in tab 3
}
}
}
}
// Of course, in applicationDidEnterBackground: and applicationWillTerminate: I save the context; obviously, I also save the context, when appropriate, from the other ViewControllers.
With this structure in place, I instantiate my stack in AppDelegate and from there I propagate it to the 3 elements of the TabBar; from those, I again propagate the context to every other ViewController I present. I logged to the console the context everywhere and I can confirm that it is always the same.
As a matter of fact, the app with this code works.
I can't say it is perfect because, as I said, sometimes a few objects don't sync, but I suspect the cause of those objects not syncing is another (briefly, I have 2 NSManagedObject subclasses; the objects of subclass1 have an object of subclass2 as property; if I create a new subclass1 object using an existing subclass2 object as property, sync is fine; if I also create a new subclass2 object, save it and immediately set it as property of subclass1, sometimes the subclass2 object doesn't sync on the other device, while the subclass1 does and then misses that property... I can work on that later).
Before digging into this sync issue, I'd really love to know if the work I've done so far with the stack makes sense, or if it is horrible and needs to be canned.
Then, if all the code above is not horrible and if the reason of the occasional missed sync of objects would turn out to be the one I suspect, comes the other issue, and it is a big one: where do I put the code to handle the notifications that occurr when the user logs in or out from iCloud (NSPersistentStoreCoordinatorStoresWillChangeNotification and NSPersistentStoreCoordinatorStoresDidChangeNotification)?
I tried to put methods I've written (without actual functionality, at the moment I only log something to the console to know that I got there) based on Core Data by Tutorials book in both my AppDelegate and my CoreDataStack class, but in both cases when I log in or out from iCloud while the app is running, the app crashes without a single line in the console, so I have no idea of the issue.
Maybe I should put the methods to handle these notifications in all the ViewControllers, since the fetch requests happen there and UI is updated from those classes, but I'm not passing the entire coreDataStack objects around, only the context... so I'm missing something. Should I pass the entire stack, not only the context? Is it okay to handle those notifications from my CoreDataStack, or should I do it from AppDelegate?
Any help would really be appreciated...
Thanks in advance and, please, excuse if my question is not clear (I'm quite a beginner and english is not my main language...).
Also, thank you for your time reading this long question!
#cdf1982
I think the problem is that iCloud + CD never worked properly. It's not a developer code issue, the problem is the Apple implementation of iCloud + CD that simply fails.