How Can I Tell If My NSManagedObject Resides in a Read Only NSPersistentStore? - cocoa

I would like to add read-only example/tutorial data to my Core Data based macOS app.
I will include an SQL file in my application bundle containing the example data. My NSPersistentContainer will have 2 NSPersistentStores, one writable and one read only. I will only have a default configuration for my model since both stores will have the same model.
My UI will need to know if the data displayed is read only or not, for example, to stop this data being draggable.
I know that NSManagedObject does not support a readonly state, see and : Is it possible to return NSManagedObjects as read-only in Core Data? ...and the docs.
I think the best approach would be to add a readonly property to my NSManagedObject derived class that can be queried where necessary. However, I can't see how I could easily set this property! I can't find a direct link to an NSPersistentStore from an NSManagedObject.
I could set up an NSFetchRequest and specify the read only store and see if the NSManagedObject is in it, but that seems a little ridiculous.
Am I missing something more obvious here please?

With thanks to pbasdf for his suggestion...
I could find no straight-forward way to achieve this. I had to move away from using NSPersistentContainer to simplify my Core Data stack. However, I think this is a fairly elegant solution if you need a small subset of your graph to be readonly.
I subclassed NSPersistentStoreCoordinator to cache the NSManagedObjectIDs of any readonly store added to it:
class GraphStoreCoordinator: NSPersistentStoreCoordinator
{
override init(managedObjectModel model: NSManagedObjectModel)
{
readOnlyTestContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
super.init(managedObjectModel: model)
readOnlyTestContext.persistentStoreCoordinator = self
NotificationCenter.default
.addObserver(forName: .NSPersistentStoreCoordinatorStoresDidChange,
object: self, queue: nil) { [unowned self] notification in
// userInfo will be in this form for add/remove keys - not supporting migration here
guard let userInfo = notification.userInfo as? [String: [NSPersistentStore]] else {
unhandledError("Invalid userInfo for NSPersistentStoreCoordinatorStoresDidChange.") }
userInfo[NSAddedPersistentStoresKey]?.forEach { self.didAddStore($0) }
userInfo[NSRemovedPersistentStoresKey]?.forEach { self.didRemoveStore($0) }
}
}
deinit {
NotificationCenter.default
.removeObserver(self, name: .NSPersistentStoreCoordinatorStoresDidChange, object: self)
}
private func didAddStore(_ store: NSPersistentStore) {
guard store.isReadOnly else { return }
var addedObjects = Set<NSManagedObjectID>()
baseEntityNames.forEach { entityName in
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: entityName)
fetchRequest.affectedStores = [store]
do {
let addedEntityObjects = try readOnlyTestContext.fetch(fetchRequest)
addedObjects = addedObjects.union(addedEntityObjects.map { $0.objectID })
} catch {
unhandledError("Failed to fetch all \(entityName) for read only check: \(error)") }
}
readOnlyObjects[store.identifier] = addedObjects
}
private func didRemoveStore(_ store: NSPersistentStore) {
guard store.isReadOnly else { return }
readOnlyObjects.removeValue(forKey: store.identifier)
}
/// Returns the minimum set of entities that can be fetched for readonly checking
private lazy var baseEntityNames: [String] = {
return managedObjectModel.entitiesByName.compactMap { $1.superentity == nil ? $0 : nil }
}()
private var readOnlyTestContext: NSManagedObjectContext
/// Readonly objectIDs keyed per persistent store
private var readOnlyObjects = [String : Set<NSManagedObjectID>]()
internal func isObjectReadOnly(_ objectID: NSManagedObjectID) -> Bool {
return readOnlyObjects.contains(where: { $1.contains(objectID) } )
}
}
I then added an extension to NSManagedObject to that queries its NSPersistentStoreCoordinator for read-only status:
public extension NSManagedObject
{
/// Does this managed object reside in a read-only persistent store?
var isReadOnly: Bool {
guard let coordinator = managedObjectContext?
.persistentStoreCoordinator as? GraphStoreCoordinator else {
unhandledError("Should only check readonly status in a GraphStoreCoordinator") }
return coordinator.isObjectReadOnly(objectID)
}
}

Related

Passing NSOpenPanel options to NSDocument

I know how to add an AccessoryView to an NSOpenPanel (and that works correctly).
Now I would like to make the options that the user selects in the AccessoryView available to the document that is opened.
Any suggestions how that can be doen (if at all?)
I have not found a standard solution, so I created my own:
Introduced a dictionary in the NSDocumentController that associates file URLs with option sets
Override the runModalOpenPanel and wrap the runModalOpenPanel of super with first the setup of the accessory view, and afterwards the evaluation of the options and adding of the options to the dictionary for the associated urls.
When a document is opened, the document can -through the shared NSDocumentController- access the dictionary and retrieve the options.
I am not blown away by this solution, but I also do not see an easier path.
Example code:
struct OptionsAtFileOpen {
let alsoLoadFormat: Bool
}
class DocumentController: NSDocumentController {
var fileOptions: Dictionary<URL, OptionsAtFileOpen> = [:]
var accessoryViewController: OpenPanelAccessoryViewController!
override func runModalOpenPanel(_ openPanel: NSOpenPanel, forTypes types: [String]?) -> Int {
// Load accessory view
let accessoryViewController = OpenPanelAccessoryViewController(nibName: NSNib.Name(rawValue: "OpenPanelAccessoryView"), bundle: nil)
// Add accessory view and make sure it is shown
openPanel.accessoryView = accessoryViewController.view
openPanel.isAccessoryViewDisclosed = true
// Run the dialog
let result = super.runModalOpenPanel(openPanel, forTypes: types)
// If not cancelled, add the files to open to the fileOptions dictionary
if result == 1 {
// Return the state of the checkbox that selects the loading of the formatting file
let alsoLoadFormat = accessoryViewController.alsoLoadFormatFile.state == NSControl.StateValue.on
for url in openPanel.urls {
fileOptions[url] = OptionsAtFileOpen(alsoLoadFormat: alsoLoadFormat)
}
}
return result
}
}
And then in Document
override func read(from data: Data, ofType typeName: String) throws {
...
if let fileUrl = fileURL {
if let dc = (NSDocumentController.shared as? DocumentController) {
if let loadFormat = dc.fileOptions[fileUrl]?.alsoLoadFormat {
...
}
}
}
}

Swift 2 + Parse: Array index out of range

I have a UITableViewController which is basically a news feed. I have also implemented a pull to refresh feature. However sometimes when I pull to refresh it gives me the error
'Array index out of range'.
I know this means an item it is trying to get does not exist but can you tell me why? Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
refresher = UIRefreshControl()
refresher.attributedTitle = NSAttributedString(string: "Pull to refresh")
refresher.addTarget(self, action: "refresh", forControlEvents: UIControlEvents.ValueChanged)
self.tableView.addSubview(refresher)
refresh()
tableView.delegate = self
tableView.dataSource = self
}
and the refresh() function:
func refresh() {
//get username and match with userId
let getUser = PFUser.query()
getUser?.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if let users = objects {
//clean arrays and dictionaries so we don't get indexing error
self.messages.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in users {
if let user = object as? PFUser {
//make userId = username
self.users[user.objectId!] = user.username!
}
}
}
})
let getPost = PFQuery(className: "Posts")
getPost.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if let objects = objects {
self.messages.removeAll(keepCapacity: true)
self.users.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in objects {
self.messages.append(object["message"] as! String)
self.usernames.append(self.users[object["userId"] as! String]!)
self.tableView.reloadData()
}
}
}
self.refresher.endRefreshing()
}
It appears you are removing everything from users and then trying to access it in getPost.findObjectsInBackgroundWithBlock.
Specifically, self.users.removeAll(keepCapacity: true) is called just before self.usernames.append(self.users[object["userId"] as! String]!).
Also, be aware that the two queries execute asynchronously. You have no guarantee that users has been populated when the second query's completion block is reached. You will most likely want to restructure your two queries into one compound query or nest them (not recommended).
Lastly, from the looks of it, you may want to use removeAll(keepCapacity: false) rather than keeping the capacity with removeAll(keepCapacity: true).

Accessing PFObject Subclassed Properties from a Query

I am using Parse and have created a subclass of PFObject. When creating objects it makes things much easier. Once objects are created, I am experimenting with querying the database and accessing the custom properties I created. What I am finding is that I cannot use dot notation to access the properties when I am working the the PFObjects returned from the query. Is this normal?
Here is subclass I created.
import Foundation
import UIKit
import Parse
class MessagePFObject: PFObject
{
#NSManaged var messageSender : String
#NSManaged var messageReceiver : String
#NSManaged var messageMessage : String
#NSManaged var messageSeen : Bool
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Custom Query method.
override class func query() -> PFQuery?
{
let query = PFQuery(className: MessagePFObject.parseClassName())
query.includeKey("user")
query.orderByDescending("createdAt")
return query
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
init(messageSenderInput: String?, messageReceiverInput: String?, messageMessageInput: String?)
{
super.init()
self.messageSender = messageSenderInput!
self.messageReceiver = messageReceiverInput!
self.messageMessage = messageMessageInput!
self.messageSeen = false
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
override init()
{
super.init()
}
}
//++++++++++++++++++++++++++++++++++++ EXTENSION +++++++++++++++++++++++++++++++++++++++++++++++++++
extension MessagePFObject : PFSubclassing
{
class func parseClassName() -> String
{
return "MessagePFObject"
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
override class func initialize()
{
var onceToken: dispatch_once_t = 0
dispatch_once(&onceToken) {
self.registerSubclass()
}
}
}
Here is my query and what I am required to do to access the properties. createdAt, updatedAt, etc are all available with dot notation but none of my custom properties are. You can see I access messageSeen with element.objectForKey("messageSeen").
let predicate = NSPredicate(format: "messageSender == %# OR messageReceiver == %#", self.currentUser!.username!, self.currentUser!.username!)
let query = messagePFObject.queryWithPredicate(predicate)
query!.findObjectsInBackgroundWithBlock ({ (objects, error) -> Void in
if error == nil && objects!.count > 0
{
for element in objects!
{
print(element)
print(element.parseClassName)
print(element.objectId)
print(element.createdAt)
print(element.updatedAt)
print(element.objectForKey("messageSeen"))
}
}
else if error != nil
{
print(error)
}
})
If this is normal then that is fine. I just want to make sure I am not missing something.
Take care,
Jon
Your object subclass has to implement the PFSubclassing protocol and you need to call MessagePFObject.registerSubclass() in your app delegate.
The parse documentation is very good : https://parse.com/docs/ios/guide#objects-subclasses

Passing Dictionary to Watch

I'm trying to pass data from iPhone -> Watch via Watch Connectivity using background transfer via Application Context method.
iPhone TableViewController
private func configureWCSession() {
session?.delegate = self;
session?.activateSession()
print("Configured WC Session")
}
func getParsePassData () {
let gmtTime = NSDate()
// Query Parse
let query = PFQuery(className: "data")
query.whereKey("dateGame", greaterThanOrEqualTo: gmtTime)
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]?, error:NSError?) -> Void in
if error == nil
{
if let objectsFromParse = objects as? [PFObject]{
for MatchupObject in objectsFromParse
{
let matchupDict = ["matchupSaved" : MatchupObject]
do {
try self.session?.updateApplicationContext(matchupDict)
print("getParsePassData iPhone")
} catch {
print("error")
}
}
}
}
}
}
I'm getting error twice printed in the log (I have two matchups in Parse so maybe it knows there's two objects and thats why its throwing two errors too?):
Configured WC Session
error
error
So I haven't even gotten to the point where I can print it in the Watch app to see if the matchups passed correctly.
Watch InterfaceController:
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
let matchupWatch = applicationContext["matchupSaved"] as? String
print("Matchups: %#", matchupWatch)
}
Any ideas? Will post any extra code that you need. Thanks!
EDIT 1:
Per EridB answer, I tried adding encoding into getParsePassData
func getParsePassData () {
let gmtTime = NSDate()
// Query Parse
let query = PFQuery(className: "data")
query.whereKey("dateGame", greaterThanOrEqualTo: gmtTime)
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]?, error:NSError?) -> Void in
if error == nil
{
if let objectsFromParse = objects as? [PFObject]{
for MatchupObject in objectsFromParse
{
let data = NSKeyedArchiver.archivedDataWithRootObject(MatchupObject)
let matchupDict = ["matchupSaved" : data]
do {
try self.session?.updateApplicationContext(matchupDict)
print("getParsePassData iPhone")
} catch {
print("error")
}
}
}
}
}
}
But get this in the log:
-[PFObject encodeWithCoder:]: unrecognized selector sent to instance 0x7fbe80d43f30
*** -[NSKeyedArchiver dealloc]: warning: NSKeyedArchiver deallocated without having had -finishEncoding called on it.
EDIT 2:
Per EridB answer, I also tried just pasting the function into my code:
func sendObjectToWatch(object: NSObject) {
//Archiving
let data = NSKeyedArchiver.archivedDataWithRootObject(MatchupObject)
//Putting it in the dictionary
let matchupDict = ["matchupSaved" : data]
//Send the matchupDict via WCSession
self.session?.updateApplicationContext(matchupDict)
}
But get this error on the first line of the function:
"Use of unresolved identifer MatchupObject"
I'm sure I must not be understanding how to use EridB's answer correctly.
EDIT 3:
NSCoder methods:
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
//super.init(coder: aDecoder)
configureWCSession()
// Configure the PFQueryTableView
self.parseClassName = "data"
self.textKey = "matchup"
self.pullToRefreshEnabled = true
self.paginationEnabled = false
}
Error
You are getting that error, because you are putting a NSObject (MatchupObject) which does not conform to NSCoding inside the dictionary that you are going to pass.
From Apple Docs
For most types of transfers, you provide an NSDictionary object with
the data you want to send. The keys and values of your dictionary must
all be property list types, because the data must be serialized and
sent wirelessly. (If you need to include types that are not property
list types, package them in an NSData object or write them to a file
before sending them.) In addition, the dictionaries you send should be
compact and contain only the data you really need. Keeping your
dictionaries small ensures that they are transmitted quickly and do
not consume too much power on both devices.
Details
You need to archive your NSObject's to NSData and then put it in the NSDictionary. If you archive a NSObject which does not conform to NSCoding, the NSData will be nil.
This example greatly shows how to conform a NSObject to NSCoding, and if you implement these things then you just follow the code below:
//Send the dictionary to the watch
func sendObjectToWatch(object: NSObject) {
//Archiving
let data = NSKeyedArchiver.archivedDataWithRootObject(MatchupObject)
//Putting it in the dictionary
let matchupDict = ["matchupSaved" : data]
//Send the matchupDict via WCSession
self.session?.updateApplicationContext(matchupDict)
}
//When receiving object from the other side unarchive it and get the object back
func objectFromData(dictionary: NSDictionary) -> MatchupObject {
//Load the archived object from received dictionary
let data = dictionary["matchupSaved"]
//Deserialize data to MatchupObject
let matchUpObject = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! MatchupObject
return matchUpObject
}
Since you are using Parse, modifying an object maybe cannot be done (I haven't used Parse in a while, so IDK for sure), but from their forum I found this question: https://parse.com/questions/is-there-a-way-to-serialize-a-parse-object-to-a-plain-string-or-a-json-string which can help you solve this problem easier than it looks above :)

Change default NSManagedObjectContext of NSPersistentDocument

Core data newbie here. I'm trying to change the default NSManagedObjectContext of an NSPersistentDocument, in order to initialise and use it with NSMainQueueConcurrencyType.
Currently I'm doing it in -windowControllerDidLoadNib: like this:
- (void)windowControllerDidLoadNib:(NSWindowController *)aController
{
[super windowControllerDidLoadNib:aController];
NSManagedObjectContext *newMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[newMOC setPersistentStoreCoordinator:[self.managedObjectContext persistentStoreCoordinator]];
[self setManagedObjectContext:newMOC];
}
This seemingly works fine. But I'm wondering if initialisation of the MOC in -windowControllerDidLoadNib: is the best thing to do, or whether it should be placed somewhere else and/or initialised in a different way.
Thanks for any help.
I'm experimenting with the Xcode template for a document-based CoreData app. The template creates an init() override which just calls super.init(). I want to run a large import in the background, so I added this to the document class:
class Document: NSPersistentDocument {
private var importQueue = DispatchQueue(label: "Importer")
override init() {
super.init()
let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
moc.mergePolicy = self.managedObjectContext!.mergePolicy
moc.persistentStoreCoordinator = self.managedObjectContext!.persistentStoreCoordinator
self.managedObjectContext = moc
}
func importStuff(url: URL) {
let moc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
moc.parent = self.managedObjectContext
var count = 0
moc.performAndWait {
...
count += 1
if count % 10000 == 0 {
do {
try moc.save()
moc.reset()
}
catch {
Swift.print("save failed at record #\(count): \(error.localizedDescription)")
}
}
return true
}
do {
try moc.save()
}
catch {
Swift.print("save failed at records #\(count): \(error.localizedDescription)")
}
}
Swift.print("imported \(count) records.")
}
#IBAction func import(_ sender: Any) {
...
importQueue.async {
self.importStuff(url: url)
}
}
}
This seems to work OK in my initial tests. I think initializing a new MOC in -windowControllerDidLoadNib: is OK, but if you have object controllers bound to the document MOC, they might perform a second fetch when the MOC is changed. Initializing it in the init will initialize it sooner, before the UI is loaded.
The preferred pattern is the "pass-the-baton" approach where you pass the managed object context down to the child view controllers. Give your controllers a managed object context attribute and simply pass it on when you present them.

Resources