1

I want to show a progress indicator when conducting FileManager tasks such as moveItem and/or copyItem calls.

Can Progress be used to monitor FileManager's progress? If so, can someone give me some guidance on how to do that? Maybe a short example?

I have found some very old posts here but those discussions and examples are in objective-c, or the solution was to write your own moveItem and copyItem methods. From what I can tell Apple used to provide a mechanism but removed it sometime around 10.9 release.

I was hoping a newer solution has evolved.

EDIT: Based on Willeke's comments, I went down the road of doing the copy in two processes as suggested in this question. I have the timer on the main thread and I run the copy process as a background process. But I cannot get it to work. I have changed my question as well.

However, as you can see in my example code, the destinationURL.fileSize remains nil during the copy process. I do not know how to get the actual bytes written during the copy operation.

I found an example by Luis Larod on github of something in objective-c from almost a decade ago that uses NSOperation to do the job, but I don't know objective-c to convert it to swift. It hurts my head looking at it. LOL!

Anyway, here's my example code:

import Cocoa

class ViewController: NSViewController {

    @IBOutlet weak var fileProgressInidicator: NSProgressIndicator?
    @IBOutlet weak var fileLabel: NSTextField?
    @IBOutlet weak var copyFileButton: NSButton?

    var timer: Timer?
    var timerIsActive: Bool = false
    var fileProgress: Double = 0
    var originURL: URL = URL(fileURLWithPath: "")
    var originFileSize: UInt64 = 0
    var destinationURL: URL = URL(fileURLWithPath: "")
    var destinationFileSize: UInt64 = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }
    
    override func viewDidAppear() {
        super.viewDidAppear()
        
        copyFile(origin: URL(fileURLWithPath: "/Path/To/Large/Example.file"), destination: URL(fileURLWithPath: "/Path/To/Different Location/Example.file"))
    }
    
    @objc func updateViewWithTimer(timer: Timer) {
        guard let userInfo = timer.userInfo as? Dictionary<String, String> else {
            print("Unable to get timer userInfo object.")
            return
        }
        // Get the destination file's fileSize
        guard let size = destinationURL.fileSize else {
            print("size is nil!")
            return
        }
        fileProgress = Double(size) / Double(originFileSize)
        fileProgressInidicator?.doubleValue = fileProgress
        // Turn on for first run
        if !timerIsActive {
            // Turn on UI elements
            fileLabel?.stringValue = "\(userInfo["activity"]!) \(userInfo["fileName"]!) to \(userInfo["destinationDirectoryPath"]!)"
            fileLabel?.isHidden = false
            timerIsActive = true
            print("Turned on UI elements.")
        }
        if fileProgressInidicator!.doubleValue >= 1 || fileProgress.isNaN {
            timer.invalidate()
            timerIsActive = false
            fileLabel?.stringValue = ""
            fileLabel?.isHidden = true
            fileProgressInidicator?.doubleValue = 0
            fileProgressInidicator?.isHidden = true
            print("Turned off UI elements.")
        }
    }
    
    func startTimer(userInfo: Dictionary<String, String>?) {
        timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.updateViewWithTimer(timer:)), userInfo: userInfo, repeats: true)
    }

    func copyFile(origin: URL, destination: URL) {
        // action string will be either copy or move
        var userInfo: Dictionary<String, String> = [String:String]()
        if origin.isFileURL && destination.isFileURL {
            // Check that origin exists
            if FileManager.default.fileExists(atPath: origin.path) {
                // Get the size in bytes of the file
                guard let size = origin.fileSize else {
                    print("An error occurred getting copyFile size.")
                    return
                }
                originFileSize = size
            }
            let destinationFileName = destination.lastPathComponent
            userInfo["fileName"] = destinationFileName
            let backupPath = "\(destinationFileName) copy.\(destination.pathExtension)"
            let destinationDirectoryURL = destination.deletingLastPathComponent()
            userInfo["destinationDirectoryPath"] = destinationDirectoryURL.path
            userInfo["directory"] = destinationDirectoryURL.lastPathComponent
            userInfo["activity"] = "Copying"
            // Set up the UI
            fileProgress = 0
            fileProgressInidicator?.doubleValue = 0
            fileProgressInidicator?.isHidden = false
            destinationFileSize = 0
            // Check whether or not destination exists
            if FileManager.default.fileExists(atPath: destination.path) {
                // Need to get a reply to replace or create a copy
                let alert = NSAlert()
                alert.alertStyle = .warning
                alert.messageText = "Destination file exists."
                alert.informativeText = "Do you wish to replace the original file, or do you wish to keep a copy?"
                alert.addButton(withTitle: "Replace")
                alert.addButton(withTitle: "Keep")
                alert.beginSheetModal(for: self.view.window!) { (returnCode: NSApplication.ModalResponse) -> Void in
                    alert.window.close()
                    switch returnCode.rawValue {
                    case 1001: // Keep
                        userInfo["filename"] = backupPath
                        self.startTimer(userInfo: userInfo)
                        DispatchQueue.global().async {
                            do {
                                let _ = try FileManager.default.replaceItemAt(destination, withItemAt: origin, backupItemName: backupPath, options: [.withoutDeletingBackupItem])
                            } catch {
                                print("An error occurred while making a copy of file \(backupPath), \(error).")
                            }
                        }
                    default: // Replace
                        self.startTimer(userInfo: userInfo)
                        DispatchQueue.global().async {
                            do {
                                let _ = try FileManager.default.replaceItemAt(destination, withItemAt: origin, backupItemName: backupPath, options: [])
                            } catch {
                                print("An error occurred while replacing \(destination.path), \(error).")
                            }
                        }
                    }
                }
            } else {
                // The destination file does not exist, so this will just be a copy
                self.startTimer(userInfo: userInfo)
                DispatchQueue.global().async {
                    do {
                        let _ = try FileManager.default.copyItem(at: origin, to: destination)
                    } catch {
                        print("An error occurred while trying to copy to \(destination). \(error)")
                    }
                }
            }
        }
    }
}
    
extension URL {
    // Credit - liquid LFG UKRAINE
    // https://stackoverflow.com/questions/32814535/how-to-get-directory-size-with-swift-on-os-x
    var fileSize: UInt64? { // in bytes
        do {
            let val = try self.resourceValues(forKeys: [.totalFileSizeKey, .totalFileAllocatedSizeKey, .fileSizeKey, .fileAllocatedSizeKey])
            if val.totalFileAllocatedSize != nil {
                return UInt64(val.totalFileAllocatedSize!)
            } else if val.fileAllocatedSize != nil {
                return UInt64(val.fileAllocatedSize!)
            } else if val.totalFileSize != nil {
                return UInt64(val.totalFileSize!)
            } else if val.fileSize != nil {
                return UInt64(val.fileSize!)
            } else {
                return nil
            }
        } catch {
            print("An error occurred getting URL.fileSize. Error = \(error).")
            return nil
        }
    }
}
SouthernYankee65
  • 1,129
  • 10
  • 22
  • Does this answer your question? [update progress bar during copy file with NSFileManager](https://stackoverflow.com/questions/17544145/update-progress-bar-during-copy-file-with-nsfilemanager) – Willeke Mar 17 '22 at 12:56
  • Is that how Apple does it with the Finder? I was hoping to use the same API they use for Finder, but I cannot find anything that relates to that. If that is the best solution I guess I may have to go with that... – SouthernYankee65 Mar 17 '22 at 15:16
  • The Finder can use private or deprecated API. – Willeke Mar 17 '22 at 16:34
  • I sent a request to Apple to add the progress capability back into FileManager. It would be difficult to implement something, I think, to monitor the progress of say a folder move and its children. But maybe I've just not thought it out completely yet. – SouthernYankee65 Mar 17 '22 at 23:03
  • @Willeke So I have been messing around with the idea in the suggested answer to push the process to the background and use a timer to monitor the progress of the destination file size, but the fileSize for ```let resource = try! destinationURL.resourceValues(forKeys: [.fileSizeKey]) if resource.fileSize != nil { print(resource.fileSize!) }``` never prints. The size is always nil during the copying process. I am copying a multi-gigabyte file to an SD card for testing and the progress bar never moves. Is there another way to do this? – SouthernYankee65 Mar 18 '22 at 18:23
  • Not with `FileManager` but maybe `DispatchSource` or `FSEvents` provide functionality to monitor the file size without polling. – vadian Mar 18 '22 at 19:08
  • @vadian I will look into those to see if I can learn about those two options. After a cursory glance at DispatchSource there's little documentation on it's use. FSEvents Programming Guide on the documents archive is in objective-c. – SouthernYankee65 Mar 18 '22 at 19:24
  • `FSEvents` works pretty well in Swift – vadian Mar 18 '22 at 19:36
  • After further research I think I'm going to have to roll my own file/folder copy/move routine to achieve my goal of monitoring progress. Everything I have found is relying on the URL attributes for fileSize. The issue is, from what I have read, FileManager does not directly write the file to the destination. It first writes to a temporary location and then, apparently, another process moves the item to the location after FileManager is done. So the URL fileSize will not see a change until the behind the scenes work is done. – SouthernYankee65 Mar 18 '22 at 23:13

0 Answers0