UIDocument saveToURL deletes original file

108 Views Asked by At

I'm trying to use -[UIDocument saveToURL:(NSURL *)url forSaveOperation:(UIDocumentSaveOperation)saveOperation completionHandler:(void (^ __nullable)(BOOL success))completionHandler] to save a duplicate at a new URL, however the original file -[UIDocument fileURL] is deleted when doing so?

3

There are 3 best solutions below

0
catlan On BEST ANSWER

I couldn't find any documentation regarding this, but it seems to be intend. ¯\_(ツ)_/¯ Here is the Hopper disassembly of iOS 15.2:

enter image description here

0
Ash On

Yeah, UIDocument does that. It's a pain.

If you want to save a UIDocument to a new URL without losing the original, you can do it with a function like this:

func saveDocument(_ document: UIDocument, asCopyTo url: URL, completion: @escaping (Bool, Error?) -> Void) {
    let oldUrl = document.fileURL
    document.save(to: url, for: .forOverwriting) { success in
        if success {
            do {
                try FileManager.default.copyItem(at: url, to: oldUrl)
            } catch let error {
                completion(false, error)
            }
        }
        completion(success, nil)
    }
}

This first saves the existing document at its new url, updating the fileURL so that the UIDocument object remains current and doesn't keep on saving over the old file. Once that's done, and assuming it succeeds, we use FileManager to copy the new file's data back to its original location.

It might seem weird to delete and recreate the file at its original location, and I agree that UIDocument is a pretty horrible creation, but this does at least save you from having to reload the document from the file system to maintain the correct location.

0
John Scalo On

If you have a UIDocument subclass, add this to the subclass and call it instead of the normal save(to:for:). If there's an existing document in the target location, this moves it to a tmp location, increments a number after the name until it finds an unused one, saves the document, and finally restores the original from the tmp location.

    func saveRenamingExistingFileIfNecessary(completionHandler: @escaping (_ success: Bool)->()) {
        let fm = FileManager.default
        let targetDir = fileURL.deletingLastPathComponent()
        var tmpURL: URL?
        let originalURL = fileURL
        var workingFilename = fileURL.lastPathComponent
        var workingURL = fileURL
        
        if fm.fileExists(atPath: fileURL.path) {
            tmpURL = targetDir.appendingPathComponent("tmp.data")
            do {
                try? fm.removeItem(at: tmpURL!)
                try fm.copyItem(at: fileURL, to: tmpURL!)
            } catch {
                print("*** save error: \(error)")
                completionHandler(false)
                return
            }

            let filenameWithoutExtension = ((fileURL.lastPathComponent as NSString).deletingPathExtension as String)
            let fileExtension = fileURL.pathExtension
            var fileCounter = 2
            while fm.fileExists(atPath: workingURL.path) {
                workingFilename = "\(filenameWithoutExtension) \(fileCounter).\(fileExtension)"
                workingURL = targetDir.appendingPathComponent(workingFilename, isDirectory: false)
                fileCounter += 1
            }
        }
        
        save(to: workingURL, for: .forCreating) { success in
            if success {
                if let tmpURL {
                    do {
                        try fm.copyItem(at: tmpURL, to: originalURL)
                        try fm.removeItem(at: tmpURL)
                    } catch {
                        print("*** save error: \(error)")
                        completionHandler(false)
                        return
                    }
                }
            }
            completionHandler(success)
        }
    }