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
}
}
}