14

I am currently using local storage in my iOS App. The user data is stored in the Document Directory and now I am planning to use iCloud Documents storage instead.

Here is how I intend to do it :

  1. Checking if iCloud is available on the device

  2. If yes, use URLForUbiquityContainerIdentifier to get the iCloud container URL

  3. Save new files and documents to this new URL

For that I am using this code that will return the URL of the document folder (iCloud or local)

class CloudDataManager {

    class func getDocumentDiretoryURL() -> NSURL {
        let localDocumentsURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: .UserDomainMask).last! as NSURL
        let iCloudDocumentsURL = NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)?.URLByAppendingPathComponent("Documents")

        if userDefault.boolForKey("useCloud") && iCloudDocumentsURL != nil  {
            return iCloudDocumentsURL!
        } else {
            return localDocumentsURL
        }
    }
}

Is it the best practice? I am worried problems will occur if one day iCloud isn't available so the local directory will be used instead of the cloud container and will be empty. Thank you.

Romain
  • 649
  • 1
  • 5
  • 18
  • I use the same code as you to delete an iCloud file, but the file does not get deleted. I just wanted to confirm that your delete function actually works for iCloud files in your app? – gbotha Dec 22 '15 at 06:36
  • I have no problem deleting the iCloud files in my app. Sometimes it might take more than 2 minutes to actually see that files are gone from iCloud container. – Romain Dec 23 '15 at 10:22
  • 1
    Thanks. I found out that delete is working for me. However, when I try to update a file already in iCloud, I end up just getting a new file with the same name with a number after it. So, I then decided to first check if file is in iCloud, if so then delete and then add the new file with the same name. That still causes a duplicate to be saved, and the original file is there ( not deleted). However, when I just try to delete a file, it is deleted. Strange....not sure why this is happening. – gbotha Dec 24 '15 at 20:51
  • MerryXmas. It depends how you write your data. In my case I use NSKeyedArchiver.archiveRootObject because my object inheriting from NSCoding. [Here is an explanation](http://sketchytech.blogspot.fr/2015/06/swift-and-nscoding-keeping-it-simple.html). This let you save a custom object to an Array in a stored file. To update my data which is an array of objects, I load the URL of this file from iCloud container (or local dir if iCloud not enabled) then I append new data to this array and finally I save the new array to the filesystem with archiveRootObject that automatically write the file – Romain Dec 26 '15 at 00:24
  • Otherwise if you have a lot of file to write or update you could use UIDocument class to manage writing, it will also avoid conflict automatically if you try to write from different device as the same time. Contact me in PM if If you need, also I would appreciate if you vote for my answer, the one at the bottom, Thank ;) – Romain Dec 26 '15 at 00:38
  • How would I PM you? I don't think Stackoverflow offers that option. – gbotha Dec 26 '15 at 05:24
  • Oh right, I have added my Tweeter handle on my profile – Romain Dec 26 '15 at 09:56

3 Answers3

20

Thanks to the comment above and with further readings, I've find a way to solve my problem.

Here is how I decided to do it:

  • iCloud will be activated by default (if possible)
  • The user can use an UISwitch to disable/enable iCloud in the App
  • When the user disable iCloud, all the iCloud files will be transferred locally
  • When the user enable iCloud, all the local files will be transferred in the iCloud Ubiquity container
  • No data merging

Like this data will not be lost.

I guess almost everyone will use iCloud and everything will be transparent and painless. Anyway the files I sync are pretty small so it should work fine (so far it does).

I have 5 simples methods:

  1. Method to check if iCloud is available
  2. Method to return the Document URL according to user choice (iCloud OR Local)
  3. Method to delete all files in a Directory (files used by the app)
  4. Method to move files from local dir to iCloud container
  5. Method to move fies from iCloud container to local dir

Here is my class that handle the issue

class CloudDataManager {

static let sharedInstance = CloudDataManager() // Singleton

struct DocumentsDirectory {
    static let localDocumentsURL: NSURL? = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: .UserDomainMask).last! as NSURL
   static let iCloudDocumentsURL: NSURL? = NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)?.URLByAppendingPathComponent("Documents")

}


// Return the Document directory (Cloud OR Local)
// To do in a background thread

func getDocumentDiretoryURL() -> NSURL {
    print(DocumentsDirectory.iCloudDocumentsURL)
    print(DocumentsDirectory.localDocumentsURL)
    if userDefault.boolForKey("useCloud") && isCloudEnabled()  {
        return DocumentsDirectory.iCloudDocumentsURL!
    } else {
        return DocumentsDirectory.localDocumentsURL!
    }
}

// Return true if iCloud is enabled

func isCloudEnabled() -> Bool {
    if DocumentsDirectory.iCloudDocumentsURL != nil { return true }
    else { return false }
}

// Delete All files at URL

func deleteFilesInDirectory(url: NSURL?) {
    let fileManager = NSFileManager.defaultManager()
    let enumerator = fileManager.enumeratorAtPath(url!.path!)
    while let file = enumerator?.nextObject() as? String {

        do {
            try fileManager.removeItemAtURL(url!.URLByAppendingPathComponent(file))
            print("Files deleted")
        } catch let error as NSError {
            print("Failed deleting files : \(error)")
        }
    }
}

// Move local files to iCloud
// iCloud will be cleared before any operation
// No data merging

func moveFileToCloud() {
    if isCloudEnabled() {
        deleteFilesInDirectory(DocumentsDirectory.iCloudDocumentsURL!) // Clear destination
        let fileManager = NSFileManager.defaultManager()
        let enumerator = fileManager.enumeratorAtPath(DocumentsDirectory.localDocumentsURL!.path!)
        while let file = enumerator?.nextObject() as? String {

            do {
                try fileManager.setUbiquitous(true,
                    itemAtURL: DocumentsDirectory.localDocumentsURL!.URLByAppendingPathComponent(file),
                    destinationURL: DocumentsDirectory.iCloudDocumentsURL!.URLByAppendingPathComponent(file))
                print("Moved to iCloud")
            } catch let error as NSError {
                print("Failed to move file to Cloud : \(error)")
            }
        }
    }
}

// Move iCloud files to local directory
// Local dir will be cleared
// No data merging

func moveFileToLocal() {
    if isCloudEnabled() {
        deleteFilesInDirectory(DocumentsDirectory.localDocumentsURL!)
        let fileManager = NSFileManager.defaultManager()
        let enumerator = fileManager.enumeratorAtPath(DocumentsDirectory.iCloudDocumentsURL!.path!)
        while let file = enumerator?.nextObject() as? String {

            do {
                try fileManager.setUbiquitous(false,
                    itemAtURL: DocumentsDirectory.iCloudDocumentsURL!.URLByAppendingPathComponent(file),
                    destinationURL: DocumentsDirectory.localDocumentsURL!.URLByAppendingPathComponent(file))
                print("Moved to local dir")
            } catch let error as NSError {
                print("Failed to move file to local dir : \(error)")
            }
        }
    }
}



}
Romain
  • 649
  • 1
  • 5
  • 18
  • 1
    If a user happens to turn off your access to iCloud, either by disabling iCloud or by disabling it for your specific app, how does the app know to move its files to local (especially if it is not open in the background at the time), or are they stranded there in iCloud? – SAHM Dec 04 '17 at 16:42
  • file are not showing in iCloud any specific issue.? – Surya Prakash Kushawah Dec 13 '17 at 13:59
  • I'm not able to copy iColud image to my Local storage.Can you please help me with this? – janu kansagra Apr 05 '18 at 10:13
  • can you share more snapcode to show how to implement your above code for copy file to local to iCloud drive ? – Himanshu Moradiya Nov 29 '18 at 05:02
  • About the `deleteFilesInDirectory` method: shouldn't you use `fileManager.setUbiquitous(false.....` first to remove them from the cloud and then delete them locally? Looking at your code it seems that iCloud won't be notified of the changes and other devices won't get the update that files were removed. – n00b Jun 14 '20 at 02:08
  • Hello @Romain, I have the same things used but the container name is wrong displayed in iCloud manage account from setting, any idea? – S K Aug 03 '23 at 05:32
7

for those who wants to use SWIFT 3: NOTE: Instead of moving the data I just do copy. But the destination path is cleared before copy data there..

class CloudDataManager {

    static let sharedInstance = CloudDataManager() // Singleton

    struct DocumentsDirectory {
        static let localDocumentsURL = FileManager.default.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: .userDomainMask).last!
        static let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
    }


    // Return the Document directory (Cloud OR Local)
    // To do in a background thread

    func getDocumentDiretoryURL() -> URL {
        if isCloudEnabled()  {
            return DocumentsDirectory.iCloudDocumentsURL!
        } else {
            return DocumentsDirectory.localDocumentsURL
        }
    }

    // Return true if iCloud is enabled

    func isCloudEnabled() -> Bool {
        if DocumentsDirectory.iCloudDocumentsURL != nil { return true }
        else { return false }
    }

    // Delete All files at URL

    func deleteFilesInDirectory(url: URL?) {
        let fileManager = FileManager.default
        let enumerator = fileManager.enumerator(atPath: url!.path)
        while let file = enumerator?.nextObject() as? String {

            do {
                try fileManager.removeItem(at: url!.appendingPathComponent(file))
                print("Files deleted")
            } catch let error as NSError {
                print("Failed deleting files : \(error)")
            }
        }
    }

    // Copy local files to iCloud
    // iCloud will be cleared before any operation
    // No data merging

    func copyFileToCloud() {
        if isCloudEnabled() {
            deleteFilesInDirectory(url: DocumentsDirectory.iCloudDocumentsURL!) // Clear all files in iCloud Doc Dir
            let fileManager = FileManager.default
            let enumerator = fileManager.enumerator(atPath: DocumentsDirectory.localDocumentsURL.path)
            while let file = enumerator?.nextObject() as? String {

                do {
                    try fileManager.copyItem(at: DocumentsDirectory.localDocumentsURL.appendingPathComponent(file), to: DocumentsDirectory.iCloudDocumentsURL!.appendingPathComponent(file))

                    print("Copied to iCloud")
                } catch let error as NSError {
                    print("Failed to move file to Cloud : \(error)")
                }
            }
        }
    }

    // Copy iCloud files to local directory
    // Local dir will be cleared
    // No data merging

    func copyFileToLocal() {
        if isCloudEnabled() {
            deleteFilesInDirectory(url: DocumentsDirectory.localDocumentsURL)
            let fileManager = FileManager.default
            let enumerator = fileManager.enumerator(atPath: DocumentsDirectory.iCloudDocumentsURL!.path)
            while let file = enumerator?.nextObject() as? String {

                do {
                    try fileManager.copyItem(at: DocumentsDirectory.iCloudDocumentsURL!.appendingPathComponent(file), to: DocumentsDirectory.localDocumentsURL.appendingPathComponent(file))

                    print("Moved to local dir")
                } catch let error as NSError {
                    print("Failed to move file to local dir : \(error)")
                }
            }
        }
    }



}
Yaroslav Dukal
  • 3,894
  • 29
  • 36
  • I do not think it's the way how Apple wants you to do it. Here is [iCloud File Management Documentation](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/iCloud/iCloud.html#//apple_ref/doc/uid/TP40010672-CH12-SW1) and here some [Document-Based App Programming guide from Apple](https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/DocumentBasedAppPGiOS/ManageDocumentLifeCycle/ManageDocumentLifeCycle.html#//apple_ref/doc/uid/TP40011149-CH4-SW8) – peetadelic Jul 06 '23 at 04:07
  • One more comment, [iCloud File Management](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/iCloud/iCloud.html#//apple_ref/doc/uid/TP40010672-CH12-SW1) seems to be overall designing of iCloud experience, for example editing file on Macbook and opening on iPhone later. The other guide I provided, seems to be just for iOS purpose. – peetadelic Jul 06 '23 at 04:25
  • this code is from 2017. so for sure it's outdated now. – Yaroslav Dukal Jul 07 '23 at 22:57
2

Check this link: iCloud basics and code sample

If the information that you are storing are simple, it's better to use NSUserDefaults. You don't want to ask iCloud for basic information.

Community
  • 1
  • 1
m.aibin
  • 3,528
  • 4
  • 28
  • 47
  • Thanks, I use UserDefaults to check if the user wants to use iCloud. I am investigating more by checking the link you gave me. – Romain Nov 24 '15 at 07:04