Saving to iCloud in Swift remaining compatible with iOS7

Hullo, I am trying to implement iCloud support for a Dictionary in Swift without resorting to CloudKit in oder to retain compatibility with iOS 7. This is the Swift code I employ in a UIDocument subclass:


override init(fileURL: NSURL){
    super.init(fileURL: fileURL)
}
convenience init() {
    let ubiq = NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)
    var finalURL:NSURL?
    if (ubiq != nil){
        let ubiquitousPackage = ubiq!.URLByAppendingPathComponent("Documents")
        finalURL = ubiquitousPackage.URLByAppendingPathComponent("favorite")
        self.init(fileURL: finalURL!)
        self.loadDocumentFromiCloud()
    } else {
        let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
        let documentsDirectory = paths[0];
        let filePath = String(format:"%@/%@", documentsDirectory, "favorite")
        finalURL=NSURL(string: filePath)
        self.init(fileURL: finalURL!)
        let dict=NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? Dictionary<String, palinaModel>
        if (dict != nil) {
            favoriteStops=dict!
        }
    }
}

Yet, when I execute it on the simulator, where of course iCloud is not available and the control goes to the 'else' case, I get a crash when calling super.init(fileURL: fileURL) with the value obtained in that function that in a test case is:


/Users/.../Library/Developer/CoreSimulator/Devices/B5469736-F27B-4E14-837A-7B9771D9A1A7/data/Containers/Data/Application/B2F7AADB-CF71-4E27-AAD7-5F1455C5DD9E/Documents/favorite


That does in fact seem not much of a url; how do I create a URL to pass to the UIDocument init(fileUrl:) in the case iCloud is not available? Otherwise is there some technique by which to store a Dictionary on iCloud without using Cloudkit?

Replies

Maybe you need this:

        finalURL=NSURL(fileURLWithPath: filePath)


Or, this would work:

        let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
        let documentsURL = urls[0]
        finalURL = documentsURL.URLByAppendingPathComponent("favorite")

Yes:

finalURL=NSURL(fileURLWithPath: filePath)

does not crash. I wonder whther the dictionary is saved indeed, but the fact the crash did not happen is promising enough. What would be the purpose of the other chunk of code?

You have no need to get `paths`, if you work on URLs. The three lines can replace the corresponding lines in your original code.

Yes, but in this way how do I get the contents? As you see in:


let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
let documentsDirectory = paths[0];
let filePath = String(format:"%@/%@", documentsDirectory, "favorite")
finalURL=NSURL(fileURLWithPath: filePath) 
self.init(fileURL: finalURL!)
let dict=NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? Dictionary<String, palinaModel>
if (dict != nil) {
      favoriteStops=dict!
}

I use them for initializing property favoriteStops. If I use your three lines, how do I get an handle to the stored dictionary?

I have missed that NSKeyedUnarchiver does not have an alternative method to work with URLs.

        if let dict = NSKeyedUnarchiver.unarchiveObjectWithFile(finalURL.path!) as? [String : palinaModel] {
            favoriteStops = dict
        }

So what would be the complete suggestion?

As you like it. Apple is encouraging us to use URL based coding, but not yet forcing.

At any rate, thi is the full class listing, both as a reference and to possibly collect suggestions for improvement:


import UIKit
@objc public protocol displayable:searchableSources {
    var dirty: Bool {get set}
    var alwaysActive: Bool {get set}
    var toHiglight: Bool {get set}
    var timestamp: Int {get set}
    var coordinate: CLLocationCoordinate2D{get set}
    var myStripes: [PalinaStripe] {get set}
    var address: String {get set}
    var overviewHidden: Bool {get set}
    var palina: String {get set}
    var model: palinaModel {get set}
   
    func loadStripesWithCompletion(completionHandler:() -> Void)
    func description()->String
    func segueExecute()
}
public class FavoriteHandler: UIDocument {
    var favoriteStops=Dictionary<String, palinaModel>()
    let kFILENAME = "favorite"
    func documentsURLForUbiquitousURL(ubiquitousURL:NSURL)->NSURL {
        return ubiquitousURL.URLByAppendingPathComponent("Documents")
    }
   
    override init(fileURL: NSURL){
        super.init(fileURL: fileURL)
    }
   
    convenience init() {
        let ubiq = NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)
        var finalURL:NSURL?
        if (ubiq != nil){
            let ubiquitousPackage = ubiq!.URLByAppendingPathComponent("Documents")
            finalURL = ubiquitousPackage.URLByAppendingPathComponent("favorite")
            self.init(fileURL: finalURL!)
            print(fileURL)
            self.loadDocumentFromiCloud()
        } else {
            let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
            let documentsDirectory = paths[0];
            let filePath = String(format:"%@/%@", documentsDirectory, "favorite")
            finalURL=NSURL(fileURLWithPath: filePath) 
            self.init(fileURL: finalURL!)
            print(fileURL)
            let dict=NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? Dictionary<String, palinaModel>
            if (dict != nil) {
                favoriteStops=dict!
            }
        }
       
    }
   
    public class func sharedFavoritesHandler() -> FavoriteHandler {
        struct Static {
            static let instance : FavoriteHandler = FavoriteHandler()
        }
        return Static.instance
    }
   
    func loadDocumentFromiCloud() {
        let sentQuery = NSMetadataQuery()
        var array = [AnyObject]()
        array.append(NSMetadataQueryUbiquitousDocumentsScope)
        sentQuery.searchScopes=array
        let pred = NSPredicate(format: "%K == %@", NSMetadataItemFSNameKey, kFILENAME)
        sentQuery.predicate=pred
        NSNotificationCenter.defaultCenter().addObserver(self, selector:Selector("queryDidFinishGathering:"), name:NSMetadataQueryDidFinishGatheringNotification, object:sentQuery)
        sentQuery.startQuery()
    }
   
    func queryDidFinishGathering(notification:NSNotification ) {
        if let receivedQuery = notification.object {
            receivedQuery.disableUpdates()
            receivedQuery.stopQuery()
   
            NSNotificationCenter.defaultCenter().removeObserver(self, name:NSMetadataQueryDidFinishGatheringNotification,object:receivedQuery)
    /
            if (receivedQuery.results.count == 0) {
                self.save()  /
            }
            self.loadData(receivedQuery as! NSMetadataQuery)
        }
    }
   
    func saveLocally(){
        let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
        let documentsDirectory = paths[0];
        let filePath = String(format:"%@/%@", documentsDirectory, "favorite")
        NSKeyedArchiver.archiveRootObject(self.favoriteStops, toFile:filePath)
    }
   
    func save(){
        print(fileURL)
        self.saveToURL(fileURL, forSaveOperation: .ForOverwriting) {[unowned self] (success) -> Void in
            if (success) {
                self.openWithCompletionHandler{(BOOL openSuccess) in
                }
            } else {
                self.saveLocally()
            }
        }
    }
    func loadData(query:NSMetadataQuery) {
        if (query.resultCount == 1) {
            let item = query.resultAtIndex(0)
            let url = item.valueForAttribute(NSMetadataItemURLKey) as! NSURL
            let request = NSURLRequest(URL:url)
            var response: NSURLResponse?
            do {
                let GETReply = try NSURLConnection.sendSynchronousRequest(request, returningResponse:&response)
                let dict=NSKeyedUnarchiver.unarchiveObjectWithData(GETReply) as! Dictionary<String, palinaModel>
                favoriteStops=dict
                openWithCompletionHandler{(BOOL success)->Void in
                }
            } catch{
            }
        }

    }
   
    func insertStop(key: String, model:palinaModel){
        self.favoriteStops[key]=model;
    }
}

Also when I execute:

    func save(){
        print(fileURL)
        self.saveToURL(fileURL, forSaveOperation: .ForOverwriting) {[unowned self] (success) -> Void in
            if (success) {
                self.openWithCompletionHandler{(BOOL openSuccess) in
                }
            } else {
                self.saveLocally()
            }
        }
    }

I have crash:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'The default implementation of -[UIDocument writeContents:toURL:forSaveOperation:originalContentsURL:error: only understands contents of type NSFileWrapper or NSData, not (null). You must override one of the write methods to support custom content types'

notwithstanding when I print the fileURL I get:

file:///Users/fbartolom/Library/Developer/CoreSimulator/Devices/B5469736-F27B-4E14-837A-7B9771D9A1A7/data/Containers/Data/Application/AC12B83A-E7C9-49E0-803A-CFE35F838E1A/Documents/favorite


perhaps it is the document content to be nil. How do I set it so that the writeContents function finds it and is able to save it?

saveToURL:forSaveOperation:completionHandler:

The default implementation of this method first calls the

contentsForType:error:
method synchronously on the calling queue to get the document data to save.


You need to override the contentsForType:error: method and return NSData or NSFileWrapper representing your document.

I actually change all my implementation by following the turorial at:

http://code.tutsplus.com/tutorials/working-with-icloud-document-storage--pre-37952

Thsi is what the class turned to:

public class FavoriteStopDocument: UIDocument {
    var favoriteStop:palinaModel?
    var number:String?
    let kArchiveKey = "Favorites"
  
    override init(fileURL: NSURL) {
        super.init(fileURL: fileURL)
    }
  
    override public func loadFromContents(contents: AnyObject, ofType typeName: String?) throws {
        var optionalStop:palinaModel?
        if let dataContents = contents as? NSData{
            let unarchiver = NSKeyedUnarchiver(forReadingWithData: dataContents)
            optionalStop = (unarchiver.decodeObjectForKey(kArchiveKey) as? palinaModel)
            unarchiver.finishDecoding()
        }
        if optionalStop != nil{
            favoriteStop=optionalStop!
        }  else {
        }
    }
  
    public override func contentsForType(typeName: String) throws -> AnyObject {
        let data = NSMutableData()
        let archiver = NSKeyedArchiver(forWritingWithMutableData: data)
        archiver.encodeObject(self.favoriteStop, forKey:kArchiveKey)
        archiver.finishEncoding()
        return data;
    }
}


Of course I also had to create a new content provider handling the UIDcoument subclass:

class FavoritesContentProvider: NSObject {
    var query: NSMetadataQuery!
    var favoriteStops = Array<FavoriteStopDocument>()
   
    class func sharedFavoritesContentProvider() -> FavoritesContentProvider {
        struct Static {
            static let instance : FavoritesContentProvider = FavoritesContentProvider()
        }
        return Static.instance
    }
   
    override init(){
        super.init()
        loadfavorites()
    }
   
    func loadfavorites(){
        let baseURL = NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)
        if baseURL != nil {
            self.query = NSMetadataQuery()
            self.query.searchScopes=NSArray(object:NSMetadataQueryUbiquitousDocumentsScope) as [AnyObject]
           
            let predicate = NSPredicate(format:"%K like '*'", NSMetadataItemFSNameKey)
            self.query.predicate=predicate
           
            let nc = NSNotificationCenter.defaultCenter()
            nc.addObserver(self, selector:Selector("queryDidFinish:"), name:NSMetadataQueryDidFinishGatheringNotification, object:self.query)
            nc.addObserver(self, selector:Selector("queryDidUpdate:"), name:NSMetadataQueryDidUpdateNotification, object:self.query)
            self.query.startQuery()
        }
    }
   
    func queryDidFinish(notification: NSNotification ) {
        let theQuery = notification.object!
   
    /
        theQuery.disableUpdates()
   
    /
        theQuery.stopQuery()
   
    /
        self.favoriteStops.removeAll()
   
        theQuery.enumerateResultsUsingBlock {[unowned self] (obj, idx, stop) -> Void in
            /
            let item=obj as! NSMetadataItem
            let documentURL = item.valueForAttribute(NSMetadataItemURLKey)
            let document = FavoriteStopDocument(fileURL:documentURL as! NSURL)
   
            document.openWithCompletionHandler({(success) in
                if (success) {
                    self.favoriteStops.append(document)
                }
            })
            NSNotificationCenter.defaultCenter().removeObserver(self)
        }
    }
   
    func newFavorite(favorite: palinaModel) {
        if indexForPalina(favorite) != nil {
            print("stop already favorite")
            return;
        }
    /
        let baseURL = NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)
   
        if (baseURL != nil) {
            let documentsURL = baseURL!.URLByAppendingPathComponent("Documents")
            let documentURL = documentsURL.URLByAppendingPathComponent(String(format:"Stop_%@-%f", favorite.palina, NSDate()))
   
            let document = FavoriteStopDocument(fileURL:documentURL)
            document.favoriteStop = favorite;
   
    /
            self.favoriteStops.append(document)
   
    /
   
            document.saveToURL(documentURL, forSaveOperation:.ForCreating, completionHandler:{(success) in
                if (success) {
                    print("Save succeeded.");
                } else {
                    print("Save failed.");
                }
            })
        }
    }
   
    func indexForPalina(model:palinaModel)->Int?{
        for var i=0; i<favoriteStops.count; i++ {
            let element=favoriteStops[i]
            if element.favoriteStop?.palina==model.palina{
                return i
            }
        }
        return nil
    }
   
    func deleteFavoriteAtIndex(index: Int){
        let document=favoriteStops[index]
        do {
            try NSFileManager.defaultManager().removeItemAtURL(document.fileURL)
        } catch let error as NSError{
            print("An error occurred while trying to delete document. Error %@ with user info %@.", error, error.userInfo);
        }
        /
        self.favoriteStops.removeAtIndex(index)
    }
}

I have not yet tested it, but it at least compiles. Any comment?

Yet, when iCloud is not available and so baseURL turns nil, nothing is saved, and so I am back at the drawing board. How to also manage this case?

I refactored the code to have separate classes for the UIDcument subclasa and the handler; I think I managed the saving part for when iCloud is available and when it is not with code:

func newFavorite(favorite: palinaModel) ->Bool {
        if indexForPalina(favorite) != nil {
            print("stop already favorite")
            return false;
        }
    /
        let baseURL = NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)
        var favoriteURL:NSURL!
        if (baseURL != nil) {
            let favoritesURL = baseURL!.URLByAppendingPathComponent("Favorite")
            favoriteURL = favoritesURL.URLByAppendingPathComponent(String(format:"Stop_%@", favorite.palina))
        } else {
            let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
            let documentsDirectory = paths[0];
            let filePath = String(format:"%@/%@", documentsDirectory, "Favorite")
            favoriteURL=NSURL(fileURLWithPath: filePath)
        }
        let document = FavoriteStopDocument(fileURL:favoriteURL)
        document.favoriteStop = favorite;

    /
        self.favoriteStops.append(document)

        document.saveToURL(favoriteURL, forSaveOperation:.ForCreating, completionHandler:{(success) in
            if (success) {
                print("Save succeeded.");
            } else {
                print("Save failed.");
            }
        })
        return true
    }

Yet I am unclear about how to retrieve the data when iCloud is not active; this is the code stub for the function:


func loadfavorites(){
        let baseURL = NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)
        if baseURL != nil {
            self.query = NSMetadataQuery()
            self.query.searchScopes=NSArray(object:NSMetadataQueryUbiquitousDocumentsScope) as [AnyObject]
          
            let predicate = NSPredicate(format:"%K like '*'", NSMetadataItemFSNameKey)
            self.query.predicate=predicate
          
            let nc = NSNotificationCenter.defaultCenter()
            nc.addObserver(self, selector:Selector("queryDidFinish:"), name:NSMetadataQueryDidFinishGatheringNotification, object:self.query)
            nc.addObserver(self, selector:Selector("queryDidUpdate:"), name:NSMetadataQueryDidUpdateNotification, object:self.query)
            self.query.startQuery()
        } else {
            let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
            let documentsDirectory = paths[0];
            let filePath = String(format:"%@/%@", documentsDirectory, "Favorite")
            // how to retrieve the objects stored in the local directory?
    }

In particular I am left with a directory reference; ho do I recover the specific files and rebuild the UIDocuments from them?

Perhaps I may list the elements of the directory (what is the command?) and then call:


let document = FavoriteStopDocument(fileURL:favoriteURL)

on each of specific paths?

This is my final class, not working without iCloud and failing on saveToUrl with iCloud: any hint?

import UIKit
class FavoritesContentProvider: NSObject {
    var query: NSMetadataQuery!
    var favoriteElements = Array<FavoriteStopDocument>()
   
    class func sharedFavoritesContentProvider() -> FavoritesContentProvider {
        struct Static {
            static let instance : FavoritesContentProvider = FavoritesContentProvider()
        }
        return Static.instance
    }
   
    override init(){
        super.init()
        loadfavorites()
    }
   
    func createDocumentFromURL(favoriteUrl:NSURL)->FavoriteStopDocument{
        let document = FavoriteStopDocument(fileURL:favoriteUrl)
        return document
    }
   
    func pathComponent()->String{
        return "Favorite"
    }
   
    func getFilePath() -> String{
        let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
        let documentsDirectory = paths[0];
        return documentsDirectory
    }
   
    func loadfavorites(){
        let baseURL = NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)
        if baseURL != nil {
            self.query = NSMetadataQuery()
            self.query.searchScopes=NSArray(object:NSMetadataQueryUbiquitousDocumentsScope) as [AnyObject]
           
            let predicate = NSPredicate(format:"%K like '*'", NSMetadataItemFSNameKey)
            self.query.predicate=predicate
           
            let nc = NSNotificationCenter.defaultCenter()
            nc.addObserver(self, selector:Selector("queryDidFinish:"), name:NSMetadataQueryDidFinishGatheringNotification, object:self.query)
            nc.addObserver(self, selector:Selector("queryDidUpdate:"), name:NSMetadataQueryDidUpdateNotification, object:self.query)
            self.query.startQuery()
        } else {
            let manager = NSFileManager.defaultManager()
            do {
                let filePath=getFilePath()
                print("path="+filePath)
                let fileList = try manager.contentsOfDirectoryAtPath(filePath)
                for s:String in fileList{
                    print(s)
                    let documentURL=NSURL(fileURLWithPath: filePath)
                    let favoriteUrl=documentURL.URLByAppendingPathComponent(s)
                    let document=createDocumentFromURL(favoriteUrl)
                    self.favoriteElements.append(document)
                }
            }catch let error as NSError{
                print("An error occurred while trying to load documents. Error %@ with user info %@.", error, error.userInfo);
            }
        }
    }
   
    func queryDidFinish(notification: NSNotification ) {
        /
        let theQuery=notification.object  as! NSMetadataQuery
    /
        theQuery.disableUpdates()
   
    /
        theQuery.stopQuery()
   
    /
        self.favoriteElements.removeAll()
   
        theQuery.enumerateResultsUsingBlock {[unowned self] (obj, idx, stop) -> Void in
            /
            let item=obj as! NSMetadataItem
            let documentURL = item.valueForAttribute(NSMetadataItemURLKey)
            let document = FavoriteStopDocument(fileURL:documentURL as! NSURL)
   
            document.openWithCompletionHandler({(success) in
                if (success) {
                    self.favoriteElements.append(document)
                }
            })
        }
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }
   
    func newFavorite(favorite: palinaModel) ->Bool {
        if indexForPalina(favorite) != nil {
            print("stop already favorite")
            return false;
        }
    /
        let baseURL = NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)
        var favoriteURL:NSURL!
        if (baseURL != nil) {
            let favoritesURL = baseURL!.URLByAppendingPathComponent(pathComponent())
            favoriteURL = favoritesURL.URLByAppendingPathComponent(String(format:"Stop_%@", favorite.palina))
        } else {
            let filePath = getFilePath()
            favoriteURL=NSURL(fileURLWithPath: filePath)
        }
        let document = FavoriteStopDocument(fileURL:favoriteURL, favorite:favorite)
   
    /
        self.favoriteElements.append(document)
        print("favoriteUrl=" + favoriteURL.path!+" document="+document.favoriteStop!.palina)
        document.saveToURL(favoriteURL, forSaveOperation:.ForCreating, completionHandler:{(success) in
            if (success) {
                print("Save succeeded.");
            } else {
                print("Save failed.");
            }
        })
        return true
    }
   
    func indexForPalina(model:palinaModel)->Int?{
        for var i=0; i<favoriteElements.count; i++ {
            let element=favoriteElements[i]
            if element.favoriteStop?.palina==model.palina{
                return i
            }
        }
        return nil
    }
   
    func deleteFavoriteAtIndex(index: Int){
        let document=favoriteElements[index]
        do {
            try NSFileManager.defaultManager().removeItemAtURL(document.fileURL)
        } catch let error as NSError{
            print("An error occurred while trying to delete document. Error %@ with user info %@.", error, error.userInfo);
        }
        /
        self.favoriteElements.removeAtIndex(index)
    }
}