2

I have an app that uses NSPersistentDocument (without autosaving) on OS X and UIDocument (also without autosaving) on iOS. The file representation is Binary Core Storage. This app has been working fine since iOS 7 + macOS 10.10.

If I open a document on OS X 10.13, and another device (macOS 10.13 or iOS 11) opens the same file, on the next save I get a warning "This document's file has been changed by another application since you opened or saved it.". The warning is spurious, because only an open has occured on another device - not a save.

In looking for a possible reason for this notification, I notice that when an iCloud file open occurs on one device, an extended attribute named com.apple.lastuseddate#PS is updated. I have confirmed this extended attribute is updated on both iOS 11 and macOS 10.13. This extended attribute doesn't appear to have been used in prior versions of iOS or macOS. I wonder if the updating of file metadata is triggering this spurious warning.

(I suspect this attribute may related to NSFileProvider on iOS 11 as there is a new method setLastUsedDate:forItemIdentifier:completionHandler: and FinderSync on macOS 10.13 as setLastUsedDate:forItemWithURL:completion: is also new.)

My question is - do others see this new behavior? Is it causing others such annoying side effects?

MichaelR
  • 1,681
  • 15
  • 28

1 Answers1

2

I have studied this problem further. I have determined what seems to be going on, and also workaround. NOTE this only applies to NSPersistentDocument - without autosaving.

Firstly an important note on file timestamps and file system type. HFS+ timestamps have a resolution on one second. APFS timestamps have a resolution of 1 nanosecond.

My problems only started to manifest itself when the OS X App's iCloud container was migrated to APFS.

Here is a typical sequence (I've used OS X and iOS as example devices - but the same sequence happens regardless OS type for the 'other' iCloud connected device):

  1. Open a file on OS X in the iCloud app container.
  2. Make a change and save the change to the file, at this point the file modification date on APFS will have a fractional second component.
  3. Open the same file on another device - say iOS (after allowing a moment for iCloud to propagate the changes). The modification date on opening the file on iOS has the fractional seconds component truncated.
  4. Soon afterwards, back on OS X, the modification date of the recently saved file will have the fractional seconds component truncated (by iCloud processes beyond my control - presentedItemDidChange is called at this time).
  5. If another change is saved to the OS X file, the change warning "This document's file has been changed by another application since you opened or saved it." will appear. This is because of a mismatch between the NSDocument self.fileModificationDate from the last save (which contains a fractional seconds component) and the file modification date (which has had the fractional seconds component truncated).

The implications are:

  • when iCloud exchanges timestamp metadata between hosts, the resolution is in seconds.
  • when a file is opened, and the timestamp metadata is conveyed to other devices, it is in truncated form. It is also written back to the file on the device that wrote the file last.

The workaround I have employed (which is only necessary because I am using NSPersistentDocument without autoSaving) is to set self.fileModificationDate to the fractional seconds truncated version (yes it is read-write), if and only if self.fileModificationDate matches the file modification date ignoring fractional seconds. If this is done, the warning doesn't appear - and no harm is done (as the file had not been modified):

// saving file changes

NSDate *modDate = nil;
[self.fileURL getResourceValue:&modDate forKey:NSURLContentModificationDateKey error:NULL];

if (modDate && self.fileModificationDate) {
    NSTimeInterval delta = [modDate timeIntervalSinceDate:self.fileModificationDate];
    if (fabs(delta) > 0.0 && [modDate ss_isEqualToDateInSeconds:self.fileModificationDate])
        self.fileModificationDate = modDate.ss_dateWithDateInSeconds;
}

[self saveDocumentWithDelegate:self
               didSaveSelector:@selector(documentSaved:didSave:contextInfo:)
                   contextInfo:nil];

As I said at the outset — surprising behaviour. It would be nice if it was documented. Because my use of NSDocument is non-standard, I don't know if others may have this problem.

MichaelR
  • 1,681
  • 15
  • 28
  • 2
    NSPersistentDocument supports autosaving. At least it works for me. – Dirk Feb 28 '18 at 08:51
  • Great investigation. However, I agree with Dirk, `NSPersistentDocument` supports autoSavesInPlace (just override the method to return true). – vomi Mar 28 '20 at 08:39
  • 1
    Thank you @vomi. Corrected my answer so as not to impute that autosaving didn't work. I needed more control. – MichaelR Mar 28 '20 at 22:30