Search code examples
iosobjective-cswiftswift3nsfilemanager

How to handle errors with Swift (FileManager and others in general)


Note: I posted a lazy question before for converting code to Swift 3 (deleted it)

Apple has some sample code for managing files. It is an old guide and is all in Objective-C. I converted the snippet to Swift 3. My concern is over the error handling part. I'm nesting multiple do/catch blocks... just want to know if this is the optimal way of doing things??

There is a similar question/amswer to this here.

The document is: Apple File System Programming Guide, under section "Managing Files and Directories".

This is my code (converted to Swift 3):

func backupMyApplicationData() {
        // Get the application's main data directory
        let directories = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)

        guard directories.count > 0,
            let appSupportDir = directories.first,
            let bundleID = Bundle.main.bundleIdentifier else {
            return
        }

        // Build a path to ~/Library/Application Support/<bundle_ID>/Data
        // where <bundleID> is the actual bundle ID of the application.
        let appDataDir = appSupportDir.appendingPathComponent(bundleID).appendingPathComponent("Data")

        // Copy the data to ~/Library/Application Support/<bundle_ID>/Data.backup
        let backupDir = appDataDir.appendingPathExtension("backup")

        // Perform the copy asynchronously.
        DispatchQueue.global(qos: .default).async { _ in
            // It's good habit to alloc/init the file manager for move/copy operations,
            // just in case you decide to add a delegate later.
            let fileManager = FileManager()

            do {
                // Just try to copy the directory.
                try fileManager.copyItem(at: appDataDir, to: backupDir)

            } catch CocoaError.fileWriteFileExists {
                // If an error occurs, it's probably because a previous backup directory
                // already exists.  Delete the old directory and try again.
                do {
                    try fileManager.removeItem(at: backupDir)
                } catch let error {
                    // If the operation failed again, abort for real.
                    print("Operation failed again, abort with error: \(error)")
                }

            } catch let error {
                // If the operation failed again, abort for real.
                print("Other error: \(error)")
            }
        }
    }

This is Apple's code in their docs which I converted:

- (void)backupMyApplicationData {
   // Get the application's main data directory
   NSArray* theDirs = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory
                                 inDomains:NSUserDomainMask];
   if ([theDirs count] > 0)
   {
      // Build a path to ~/Library/Application Support/<bundle_ID>/Data
      // where <bundleID> is the actual bundle ID of the application.
      NSURL* appSupportDir = (NSURL*)[theDirs objectAtIndex:0];
      NSString* appBundleID = [[NSBundle mainBundle] bundleIdentifier];
      NSURL* appDataDir = [[appSupportDir URLByAppendingPathComponent:appBundleID]
                               URLByAppendingPathComponent:@"Data"];

      // Copy the data to ~/Library/Application Support/<bundle_ID>/Data.backup
      NSURL* backupDir = [appDataDir URLByAppendingPathExtension:@"backup"];

      // Perform the copy asynchronously.
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         // It's good habit to alloc/init the file manager for move/copy operations,
         // just in case you decide to add a delegate later.
         NSFileManager* theFM = [[NSFileManager alloc] init];
         NSError* anError;

         // Just try to copy the directory.
         if (![theFM copyItemAtURL:appDataDir toURL:backupDir error:&anError]) {
            // If an error occurs, it's probably because a previous backup directory
            // already exists.  Delete the old directory and try again.
            if ([theFM removeItemAtURL:backupDir error:&anError]) {
               // If the operation failed again, abort for real.
               if (![theFM copyItemAtURL:appDataDir toURL:backupDir error:&anError]) {
                  // Report the error....
               }
            }
         }

      });
   }
}

Any thoughts?


Solution

  • You forgot to retry the copy operation after deleting an existing backup. Also, "catch let error" can be written as just "catch" because the error will be automatically assigned to a constant named "error" if you don't specify a catch pattern. Here is your code with these changes:

    func backupMyApplicationData() {
        // Get the application's main data directory
        let directories = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
    
        guard
            directories.count > 0,
            let appSupportDir = directories.first,
            let bundleID = Bundle.main.bundleIdentifier
        else {
            return
        }
    
        // Build a path to ~/Library/Application Support/<bundle_ID>/Data
        // where <bundleID> is the actual bundle ID of the application.
        let appDataDir = appSupportDir.appendingPathComponent(bundleID).appendingPathComponent("Data")
    
        // Copy the data to ~/Library/Application Support/<bundle_ID>/Data.backup
        let backupDir = appDataDir.appendingPathExtension("backup")
    
        // Perform the copy asynchronously.
        DispatchQueue.global(qos: .default).async { _ in
            // It's good habit to alloc/init the file manager for move/copy operations,
            // just in case you decide to add a delegate later.
            let fileManager = FileManager()
    
            do {
                // Just try to copy the directory.
                try fileManager.copyItem(at: appDataDir, to: backupDir)                
            } catch CocoaError.fileWriteFileExists {
                // Error occurred because a previous backup directory
                // already exists. Delete the old directory and try again.
                do {
                    try fileManager.removeItem(at: backupDir)
                } catch {
                    // The delete operation failed, abort.
                    print("Deletion of existing backup failed. Abort with error: \(error)")
                    return
                }
                do {
                    try fileManager.copyItem(at: appDataDir, to: backupDir)
                } catch {
                    // The copy operation failed again, abort.
                    print("Copy operation failed again. Abort with error: \(error)")
                }                
            } catch {
                // The copy operation failed for some other reason, abort.
                print("Copy operation failed for other reason. Abort with error: \(error)")
            }
        }
    }
    

    If you want a translation that's closer to the Objective-C original, where there's only one error output, try this:

    func backupMyApplicationData() {
        // Get the application's main data directory
        let directories = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
    
        guard
            directories.count > 0,
            let appSupportDir = directories.first,
            let bundleID = Bundle.main.bundleIdentifier
        else {
            return
        }
    
        // Build a path to ~/Library/Application Support/<bundle_ID>/Data
        // where <bundleID> is the actual bundle ID of the application.
        let appDataDir = appSupportDir.appendingPathComponent(bundleID).appendingPathComponent("Data")
    
        // Copy the data to ~/Library/Application Support/<bundle_ID>/Data.backup
        let backupDir = appDataDir.appendingPathExtension("backup")
    
        // Perform the copy asynchronously.
        DispatchQueue.global(qos: .default).async { _ in
            // It's good habit to alloc/init the file manager for move/copy operations,
            // just in case you decide to add a delegate later.
            let fileManager = FileManager()
    
            // Just try to copy the directory.
            if (try? fileManager.copyItem(at: appDataDir, to: backupDir)) == nil {
                // If an error occurs, it's probably because a previous backup directory
                // already exists.  Delete the old directory and try again.
                if (try? fileManager.removeItem(at: backupDir)) != nil {
                    do {
                        try fileManager.copyItem(at: appDataDir, to: backupDir)
                    } catch {
                        // The copy retry failed.
                        print("Failed to backup with error: \(error)")
                    }
                }
            }
        }
    }