0

I am working on my first Swift iOS app, having trouble serializing and saving an object whose JSON I fetch from the server. I am using Gloss, a lightweight JSON-parsing library which defines a Decodable protocol through which an instance can be instantiated from JSON. My intention is to load a thing from JSON (a type alias for [String : AnyObject]) by first extracting its id, and then check whether I already have a local archived copy. If I do, unarchive this and get the image. If not, make an asynchronous request for the image file.

The problem is that Thing.localArchiveExists(id) always returns false. Things are successfully instantiated but they always re-fetch the image. I have checked on the file system and confirmed that no archive files are being written. However, I am not seeing "ERROR. Could not archive", which suggests to me that the save succeeded. Am I missing something about how to archive and save NSCoder objects? Thanks!

Here is my implementation of the Decodable protocol:

// MARK: Decodable protocol
// When a thing is loaded from JSON, we load its image from archive if possible.
required init?(json: JSON) {
    guard let id: Int = "id" <~~ json else { return nil}

    if Thing.localArchiveExists(id) {
        guard let savedThing = NSKeyedUnarchiver.unarchiveObjectWithFile(Thing.archiveFilePath(id)) as? Thing else { return nil }
        self.id = savedThing.id
        self.name = savedThing.name
        self.image = savedThing.image
        self.imageUrl = savedThing.imageUrl
        super.init()
        print("Loaded Thing \(self.name) from archive")
    }
    else {
        guard let name: String = "name" <~~ json else { return nil}
        guard let imageUrl: NSURL = "url" <~~ json else { return nil}
        self.id = id
        self.name = name
        self.imageUrl = imageUrl
        super.init()
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
            let data = NSData(contentsOfURL: imageUrl)
            dispatch_async(dispatch_get_main_queue(), {
                self.image = UIImage(data: data!)
                guard self.save() else {
                    print("ERROR. Could not archive")
                    return
                }
                print("Loaded Thing \(self.name) from server")
            })
        }
    }
}

Here are relevant parts of the Thing class:

// MARK: Properties
var id: Int?
var name: String
var imageUrl: NSURL?
var image: UIImage?

// MARK: Archiving Paths
static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("things")

// MARK: Types
struct PropertyKey {
    static let nameKey = "name"
    static let imageKey = "image"
    static let imageUrlKey = "imageUrl"
    static let idKey = "id"
}

// Returns the file URL at which a Thing with the given ID should be saved.
class func archiveFilePath(id: Int) -> String {
    return Thing.ArchiveURL.URLByAppendingPathComponent("thing\(id)").absoluteString
}

// Checks whether an archived copy of a Thing with the given ID exists.
class func localArchiveExists(id: Int) -> Bool {
    let fileManager = NSFileManager.defaultManager()
    return fileManager.fileExistsAtPath(Thing.archiveFilePath(id))
}

// MARK: NSCoding
func encodeWithCoder(coder: NSCoder) {
    coder.encodeObject(name, forKey: PropertyKey.nameKey)
    if image != nil {
        coder.encodeObject(image!, forKey: PropertyKey.imageKey)
    }
    if imageUrl != nil {
        coder.encodeObject(imageUrl!, forKey: PropertyKey.imageUrlKey)
    }
    coder.encodeInteger(id!, forKey: PropertyKey.idKey)
}

required convenience init?(coder aDecoder: NSCoder) {
    let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
    let image = aDecoder.decodeObjectForKey(PropertyKey.imageKey) as? UIImage
    let imageUrl = aDecoder.decodeObjectForKey(PropertyKey.imageUrlKey) as? NSURL
    let id = aDecoder.decodeIntegerForKey(PropertyKey.idKey) 

    // Must call designated initializer.
    self.init(name: name, image: image, imageUrl: imageUrl, id: id)
}

func save() -> Bool {        
    // For some reason I can't archive to file.
    return NSKeyedArchiver.archiveRootObject(self, toFile: Thing.archiveFilePath(self.id!))
}  
cproctor
  • 175
  • 8
  • 1
    ... presumably `Thing` declares its conformance to `NSCoding`? – Tommy May 17 '16 at 17:56
  • The problem is the struct inside your class. It is not coding compliant – Leo Dabus May 17 '16 at 18:09
  • @Tommy, yes, Thing is declared: `class Thing: NSObject, NSCoding, Glossy {` – cproctor May 17 '16 at 20:07
  • @LeoDabus, I tried removing the struct, and I still have the same problem. Doesn't seem like the struct should be the problem, since I'm not encoding it. Any other ideas? Here's [the full class](https://github.com/cproctor/emoto_ios/blob/master/Emoto/Emoto.swift) if it helps. Thanks! – cproctor May 17 '16 at 20:13

1 Answers1

0

I figured out my problem: the save failed because I had not yet created the directory in which I was trying to save my Thing.

func save() -> Bool {        
    let archivedData = NSKeyedArchiver.archivedDataWithRootObject(self)
    do {
        try NSFileManager.defaultManager().createDirectoryAtURL(Thing.ArchiveURL, withIntermediateDirectories: true, attributes: [:])
        try archivedData.writeToFile(Thing.archiveFilePath(self.id!), options: [])
        return true
    } catch {
        print(error)
        return false
    }
}
cproctor
  • 175
  • 8