My iOS 13.2 Swift app is a type of SceneKit editor which uses Core Data to persist user edits. I'm wondering if Core Data might be associated with the message below since NSManagedObject is a subclass of NSObject and since a test app I created without Core Data does not cause the message to be displayed.
Although I haven't subclassed any SceneKit classes which support NSSecureCoding, and no other classes in the app use NSSecureCoding, the following message is displayed when a SCNScene is displayed in a SCNView:
The Displayed Message:
[general] NSSecureCoding allowed classes list contains [NSObject class], which bypasses security by allowing any Objective-C class to be implicitly decoded. Consider reducing the scope of allowed classes during decoding by listing only the classes you expect to decode, or a more specific base class than NSObject.
This message is displayed only once even though a SCNScene can be reopened multiple times to reflect the user's edits.
Possible Causes
1. Some of the Core Data entities contain binary data properties used to display thumbnail images. However, when I comment-out the code associated with creating/displaying the thumbnails, the above message is still displayed. The thumbnail data is created with the following code which returns an optional data object. I wonder about this, because Swift bridges to NSData which is a subclass of NSObject.
static func createNonCachedItemThumbnailData(item: Item) -> Data? {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let dataSource = appDelegate.dataSource
guard let selectedDesign = dataSource.selectedDesign else { return nil }
let resourceSubdirectoryName = selectedDesign.uniqueID!
guard let bundleURL = Bundle.main.url(forResource: item.uniqueID!, withExtension: "png", subdirectory: resourceSubdirectoryName + ".scnassets") else { return nil }
guard let imageSource = CGImageSourceCreateWithURL(bundleURL as CFURL, nil) else { return nil }
/*
maxDimension is the lesser of the width or height of the UIImageView in ItemSCNNodeSelectionViewCell.
*/
let maxDimension: CGFloat = 64.0
let options: [NSString: Any] = [
kCGImageSourceThumbnailMaxPixelSize: maxDimension,
kCGImageSourceCreateThumbnailFromImageAlways: true]
guard let scaledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else { return nil }
return UIImage(cgImage: scaledImage).pngData()
}
One of the Core Data entities uses NSKeyedArchiver/NSKeyedUnarchiver to archive SCNMaterial as binary data. I didn't use the transformable type for this property, because I read that the transformable type doesn't notice changes when the context is saved. This code seems far from the problem, but the compiler may have noticed it.
Any help would be appreciated.
Related
I am trying to implement ForEach.onMove using Core Data. I have two entities, Parent with a To-Many relationship to Child, with the relationship marked as Ordered. The view is something like this:
struct ParentView : View {
#ObservedObject var parent : Parent
var body : some View {
List {
ForEach($parent.children! as! [Child]) { child in
ChildView(child)
}.onMove {
parent.managedObjectContext!.performAndWait { indices, to in
(parent.children! as! NSMutableOrderedSet).moveObjects(at: indices, to: to)
try! parent.managedObjectContext!.save()
parent.objectWillChange.send()
}
}
}
}
}
Results are:
No errors of any kind.
During debug of the onMove function, I can see that the items are re-ordered as required
The managedObjectContext.updatedObjects is empty at the same debug step, before the call to save()
When reloading the app, the re-ordering is obviously not saved (apparently because the updatedObjects set was empty at #3.)
What am I doing wrong? How can I make the MOC realize the re-ordering change?
I fixed this, apparently with a simple solution. I am posting this for future generations of google searchers. :D
The problem is with this line:
(parent.children! as! NSMutableOrderedSet).moveObjects(at: indices, to: to)
Apparently, taking the immutable version and making it mutable works, but doesn't update the managed object. This works:
parent.mutableOrderedSetValue(forKey: "children").moveObjects(at: indices, to: to)
My app uses Core Data for persistence and I have been writing unit tests to check that view models that touch the persistence layer of the app behave as expected.
I am using an in-memory persistent store for test as recommended and the state of the store varies depending on the test I am performing. For example, in some instances I start with an empty store; in others I want to use a pre-populated store and then check the behaviour of the code that observes the store's state as it is mutated.
Although the "live" code is behaving as expected, I've had issues with the store not being in the expected state before the unit test runs and this would appear to be due to async optimisations of the stack. Changing the test order will influence which tests pass or fail and this certainly supports my suspicion that concurrency is causing the problem.
I can eliminate the errors by saving the view context after CRUD operations and wrapping this in a test expectation that waits for the NSManagedObjectContextDidSave notification thereby preventing concurrent operations. However, this makes me feel uncomfortable as it's not the way the code operates in the production app - saving the view context is expensive and so only happens when the app is terminated / pushed to background. As the tests do not accurately reflect what's happening in the live app, there's a chance that bugs could be missed despite apparent coverage.
I've been unable to find any solutions or recommendations about managing this problem in unit tests beyond using expectations. Is there a better solution?
Example tests - removing the 'executeAndWaitForSave' call in the second test method will cause the first test to fail - the MOC will contain more objects than expected:
class ParameterGridViewModelTests: XCTestCase {
var dependencies: AppDependencies?
var viewContext: NSManagedObjectContext?
var patient: Patient?
var parameter: Parameter?
override func setUp() {
dependencies = AppDependencies.emptyPersistentStore
viewContext = dependencies!.persistenceService.viewContext
patient = Patient.instanceForTest(using: dependencies!.persistenceService.viewContext)
parameter = Parameter.instanceForTest(using: dependencies!.persistenceService.viewContext)
}
override func tearDown() {
dependencies = nil
viewContext = nil
patient = nil
parameter = nil
}
func testEnvironmentPropertiesMatchExpectations() {
let persistenceService = dependencies!.persistenceService
XCTAssertEqual(persistenceService.count(for: Observation.fetchRequest()), 0, "There should be no Observation instances in store")
XCTAssertEqual(persistenceService.count(for: Parameter.fetchRequest()), 1, "There shouldn't be ONE Parameter instance in store")
}
func testObservationsPropertyIsUpdatedWhenAnObservationIsMade() {
// given
let sut = ParameterGridView.ViewModel(patient: patient!, dependencies: dependencies!)
let sutMirror = ParameterGridViewModelMirror(viewModel: sut)
// when
let numberOfObservations = 5
for _ in 1...numberOfObservations {
_ = Observation.create(
in: viewContext!,
patient: patient!,
parameter: parameter!,
numericValue: Double.random(in: 5...50)
)
}
// then
XCTAssertEqual(sutMirror.observations!.count, numberOfObservations, "Observations property of view model does not contain correct number of elements")
}
func testHistoricalObservationsEmptyWhenObservationTimeExceedsFourHoursInPast() {
// given
let sut = ParameterGridView.ViewModel(patient: patient!, dependencies: dependencies!)
// when
executeAndWaitForSave(managedObjectContext: viewContext!) {
let firstObservationTime = Date.now
.offset(.hour, direction: .past, value: 4)!
.offset(.minute, direction: .past, value: 10)!
let secondObservationTime = Date.now
.offset(.hour, direction: .past, value: 6)!
let observationOne = Observation(context: viewContext!)
observationOne.timestamp = firstObservationTime
observationOne.cd_Parameter = parameter!
observationOne.cd_Patient = patient!
observationOne.numericValue = 10
let observationTwo = Observation(context: viewContext!)
observationTwo.timestamp = secondObservationTime
observationTwo.cd_Parameter = parameter!
observationTwo.cd_Patient = patient!
observationTwo.numericValue = 100
}
// then
XCTAssertTrue(sut.lastObservation.isEmpty, "lastObservations should be empty")
XCTAssertTrue(sut.recentObservation.isEmpty, "recentObservation should be empty")
}
}
Helper function used to ensure synchronous mutation of Core Data stack:
extension XCTestCase {
/// Helper function to which attempts to mitigate test failures relating
/// to asynchronous Core Data operations leaving the MOC in an unpredictale
/// state.
/// Although saving the MOC is expensive in the production app and should only
/// occur when absolutely necessary, the associated notification can be used to
/// ensure the MOC in in the expected state before test execution proceeds
/// - Parameters:
/// - managedObjectContext: MOC to use for the operation
/// - block: code to be executed (typically creation, mutation or deletion of a MOM linked to the MOC)
func executeAndWaitForSave(managedObjectContext: NSManagedObjectContext, block: () -> Void) {
expectation(forNotification: .NSManagedObjectContextDidSave, object: managedObjectContext) { _ in
return true
}
block()
try! managedObjectContext.save()
waitForExpectations(timeout: 2.0) { error in
XCTAssertNil(error, "Save did not occur: \(error!.localizedDescription)")
}
}
}
In xCode I created some Core Data Entities for my app in the Default Configuration and I set up my Data Controller class as follows:
class DataController: ObservableObject {
let container: NSPersistentCloudKitContainer
class DataController: ObservableObject {
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "KoyaQuest", managedObjectModel: Self.model)
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Fatal error loading data store: \(error.localizedDescription)")
}
...
}
Over in CloudKit Dashboard, I notice that any records being created are stored in the "Private Database." And I'm not sure how that happened as I don't recall (nor can I find any evidence of) my specifying the type of database. I am in Development. Is this a default of that mode?
Also, since I really want these data to be public, is there a way to make the change? I've scoured Apple's documentation and other sources but can't seem to find a simple answer.
Thanks.
Soon after posting this question, I re-discovered this video (which I had actually watched before!). In case anyone has the same confusion as I did, I'm sharing here. Note that one of the two indices that need to be added to the CloudKit schema seems to have been re-named to "modifiedTimestamp" (at least that's the only one I see that looks close enough to "modifiedAt" as shown in the video).
When I generate an NSManagedObject subclass with swift, the property types are all #NSManaged, meaning I can't observe them. This is a problem when using bindings in a Cocoa application because updating the property frequently requires other properties to be 'updated'.
For example, if I add this method to my NSManagedObject subclass:
dynamic var ratePerPoint: Double {
guard pointsPerMonth > 0 else { return 0 }
return monthlyRate / Double(pointsPerMonth)
}
Then it's important that whenever I update the pointsPerMonth variable, which is part of the core data object, that I send a didChangeValueForKey("ratePerPoint") message.
If I don't, then the UI bindings don't update properly.
If ratePerPoint is a calculated property you have to implement keyPathsForValuesAffectingRatePerPoint in your NSManagedObject subclass.
+ (NSSet *)keyPathsForValuesAffectingRatePerPoint {
return [NSSet setWithObjects:#"monthlyRate", #"pointsPerMonth", nil];
}
Documentation: Registering Dependent Keys
I get strange errors when I am trying to pass around NSManagedObject through several functions. (all are in the same VC).
Here are the two functions in question:
func syncLocal(item:NSManagedObject,completionHandler:(NSManagedObject!,SyncResponse)->Void) {
let savedValues = item.dictionaryWithValuesForKeys([
"score",
"progress",
"player"])
doUpload(savedParams) { //do a POST request using params with Alamofire
(success) in
if success {
completionHandler(item,.Success)
} else {
completionHandler(item,.Failure)
}
}
}
func getSavedScores() {
do {
debugPrint("TRYING TO FETCH LOCAL SCORES")
try frc.performFetch()
if let results = frc.sections?[0].objects as? [NSManagedObject] {
if results.count > 0 {
print("TOTAL SCORE COUNT: \(results.count)")
let incomplete = results.filter({$0.valueForKey("success") as! Bool == false })
print("INCOMPLETE COUNT: \(incomplete.count)")
let complete = results.filter({$0.valueForKey("success") as! Bool == true })
print("COMPLETE COUNT: \(complete.count)")
if incomplete.count > 0 {
for pendingItem in incomplete {
self.syncScoring(pendingItem) {
(returnItem,response) in
let footest = returnItem.valueForKey("player") //only works if stripping syncScoring blank
switch response { //response is an enum
case .Success:
print("SUCCESS")
case .Duplicate:
print("DUPLICATE")
case .Failure:
print("FAIL")
}
}
} //sorry for this pyramid of doom
}
}
}
} catch {
print("ERROR FETCHING RESULTS")
}
}
What I am trying to achieve:
1. Look for locally saved scores that could not submitted to the server.
2. If there are unsubmitted scores, start the POST call to the server.
3. If POST gets 200:ok mark item.key "success" with value "true"
For some odd reason I can not access returnItem at all in the code editor - only if I completely delete any code in syncLocal so it looks like
func syncLocal(item:NSManagedObject,completionHandler:(NSManagedObject!,SyncResponse)->Void) {
completionHandler(item,.Success)
}
If I do that I can access .syntax properties in the returning block down in the for loop.
Weirdly if I paste the stuff back in, in syncLocal the completion block keeps being functional, the app compiles and it will be executed properly.
Is this some kind of strange XCode7 Bug? Intended NSManagedObject behaviour?
line 1 was written with stripped, line 2 pasted rest call back in
There is thread confinement in Core Data managed object contexts. That means that you can use a particular managed object and its context only in one and the same thread.
In your code, you seem to be using controller-wide variables, such as item. I am assuming the item is a NSManagedObject or subclass thereof, and that its context is just one single context you are using in your app. The FRC context must be the main thread context (a NSManagedObjectContext with concurrency type NSMainThreadConcurrencyType).
Obviously, the callback from the server request will be on a background thread. So you cannot use your managed objects.
You have two solutions. Either you create a child context, do the updates you need to do, save, and then save the main context. This is a bit more involved and you can look for numerous examples and tutorials out there to get started. This is the standard and most robust solution.
Alternatively, inside your background callback, you simply make sure the context updates occur on the main thread.
dispatch_async(dispatch_get_main_queue()) {
// update your managed objects & save
}