Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

archive array of optional structs with NSCoding in Swift?

I've done a lot of NSCoding archiving in Obj-C, but I'm not sure how it handles structs in Swift, nor arrays with optional values. Here is my code:

public struct SquareCoords {
    var x: Int, y: Int
}

and here is the class which I need to store:

public class Player: NSCoding {
    var playerNum: Int
    var name = ""
    private var moveHistory: [SquareCoords?] = []

    init (playerNum: Int, name: String) {
        self.playerNum = playerNum
        self.name = name
    }

    public required init(coder aDecoder: NSCoder!) {
        playerNum = aDecoder.decodeIntegerForKey("playerNumKey")
        name = aDecoder.decodeObjectForKey("nameKey") as String
        moveHistory = aDecoder.decodeObjectForKey("moveHistoryKey") as [SquareCoords?]
    }

    public func encodeWithCoder(aCoder: NSCoder!) {
        aCoder.encodeInteger(playerNum, forKey: "playerNumKey")
        aCoder.encodeObject(name, forKey: "nameKey")
        aCoder.encodeObject(moveHistory, forKey: "moveHistoryKey")
    }
...

On the last line of the coder init, I get the following error message in XCode:

'AnyObject' is not convertible to [SquareCoords?]'

and on the last line of encodeWithEncoder:

Extra argument 'forKey' in call

Can anyone get me moving in the right direction?

like image 530
Chuck Smith Avatar asked Aug 14 '14 14:08

Chuck Smith


3 Answers

In The Swift Programming Language, Apple states:

Swift provides two special type aliases for working with non-specific types:
- AnyObject can represent an instance of any class type.
- Any can represent an instance of any type at all, including function types.

Knowing that, type SquareCoords (Swift Structure) and type [SquareCoords] (Swift array of Swift Structure) can't conform to protocol AnyObject.

On the other hand, decodeObjectForKey: requires a parameter that conforms to protocol AnyObject, and encodeObject:forKey: returns AnyObject. Thus, the two following lines can't compile:

moveHistory = aDecoder.decodeObjectForKey("moveHistoryKey") as [SquareCoords?]
aCoder.encodeObject(moveHistory, forKey: "moveHistoryKey")

Therefore, unless you find a way to make SquareCoords conform to protocol AnyObject (I don't know if it's possible), you will have to transform SquareCoords from Swift Structure to Class.

PS: At this point, you may ask: "OK, but how is it possible that type String - that is in fact a Swift Struct - can conform to protocol AnyObject?" Well, that's because String is bridged seamlessly to Foundation’s NSString class (Array, Dictionary are bridged to NSArray and NSDictionary the same way). Read this blog post if you want to have a better look at it.

like image 135
Imanou Petit Avatar answered Sep 30 '22 08:09

Imanou Petit


I'm not sure what the exact problem is, but if you use NSMutableArray rather than a Swift array the problem resolves:

public struct SquareCoords {
    var x: Int, y: Int
}



public class Player: NSCoding {
    var playerNum: Int
    var name = ""
    var moveHistory: NSMutableArray = NSMutableArray()

    init (playerNum: Int, name: String) {
        self.playerNum = playerNum
        self.name = name
    }

    public required init(coder aDecoder: NSCoder!) {
        playerNum = aDecoder.decodeIntegerForKey("playerNumKey")
        name = aDecoder.decodeObjectForKey("nameKey") as String
        moveHistory = aDecoder.decodeObjectForKey("moveHistoryKey") as NSMutableArray
    }

    public func encodeWithCoder(aCoder: NSCoder!) {
        aCoder.encodeInteger(playerNum, forKey: "playerNumKey")
        aCoder.encodeObject(name, forKey: "nameKey")
        aCoder.encodeObject(moveHistory, forKey: "moveHistoryKey")
    }
}

It seems to be the case that when aDecoder.decodeObjectForKey returns an implicitly unwrapped AnyObject this won't cast to a SquareCoords array.

Been playing with this a little further and I noticed it may have something to do with the use of a struct. (you're creating an array of structs which are value types.) This is a bit of a guess but I noticed if a class type is used for SquareCoords there is no issue, e.g.

public class SquareCoords {
    var x: Int = 0, y: Int = 0
}



public class Player: NSCoding {
    var playerNum: Int
    var name = ""
    private var moveHistory: [SquareCoords] = [SquareCoords]()

init (playerNum: Int, name: String) {
    self.playerNum = playerNum
    self.name = name
}

public required init(coder aDecoder: NSCoder!) {
    playerNum = aDecoder.decodeIntegerForKey("playerNumKey")
    name = aDecoder.decodeObjectForKey("nameKey") as String
    moveHistory = aDecoder.decodeObjectForKey("moveHistoryKey") as [SquareCoords]
}

public func encodeWithCoder(aCoder: NSCoder!) {
    aCoder.encodeInteger(playerNum, forKey: "playerNumKey")
    aCoder.encodeObject(name, forKey: "nameKey")
    aCoder.encodeObject(moveHistory, forKey: "moveHistoryKey")
    }
}

Maybe the cast from AnyObject fails to a struct array for some reason. - I'm sure someone else can provide more insight, hope this helps somewhat! Swift can be tempestuous :D

like image 27
Woodstock Avatar answered Sep 30 '22 09:09

Woodstock


The other answers given here solves your problem. But, I faced a similar issue recently while trying to archive my Swift structures and came up with an interesting way to solve this. NSCoding doesn't support structures.

Essentially, the following method involves converting the structure properties into dictionary elements. But, it does that elegantly using protocols. All you need to do is define a protocol which implements two methods which aid in Dictionary-fying and un-Dictionary-fying your structure. The advantage of using protocols and generics is that it works when a structure is a property of another structure. This nesting could be to any depth.

I call the protocol 'Dictionariable', which indicates that anything that conforms to the protocol can be converted to a dictionary. The definition goes as below.

protocol Dictionariable {
    func dictionaryRepresentation() -> NSDictionary
    init?(dictionaryRepresentation: NSDictionary?)
}

Now, consider a structure 'Movie'

struct Movie {
    let name: String
    let director: String
    let releaseYear: Int
}

Let me extend the structure and make it conform to 'Dictionariable' protocol.

extension Movie: Dictionariable {

    func dictionaryRepresentation() -> NSDictionary {
        let representation: [String: AnyObject] = [
            "name": name,
            "director": director,
            "releaseYear": releaseYear
        ]
        return representation
    }

    init?(dictionaryRepresentation: NSDictionary?) {
        guard let values = dictionaryRepresentation else {return nil}
        if let name = values["name"] as? String,
            director = values["director"] as? String,
            releaseYear = values["releaseYear"] as? Int {
                self.name = name
                self.director = director
                self.releaseYear = releaseYear
        } else {
            return nil
        }
    }
}

Basically, we now have a way to safely convert a structure to a dictionary. I say safe because we implement how the dictionary is formed, individual to every structure. As long as that implementation is right, the functionality would work. The way to get back the structure from the dictionary is by using a failable initialiser. It has to be failable because file corruptions and other reasons could make the structure's instantiation from an archive incomplete. This may never happen, but, it's safer that it's failable.

func extractStructuresFromArchive<T: Dictionariable>() -> [T] {
    guard let encodedArray = NSKeyedUnarchiver.unarchiveObjectWithFile(path()) as? [AnyObject] else {return []}
    return encodedArray.map{$0 as? NSDictionary}.flatMap{T(dictionaryRepresentation: $0)}
}

func archiveStructureInstances<T: Dictionariable>(structures: [T]) {
    let encodedValues = structures.map{$0.dictionaryRepresentation()}
    NSKeyedArchiver.archiveRootObject(encodedValues, toFile: path())
}

//Method to get path to encode stuctures to
func path() -> String {
    let documentsPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).first
    let path = documentsPath?.stringByAppendingString("/Movie")
    return path!
}

The two above methods can archive and unarchive an array of 'Movie' structure. All you need to take care is the implementation of the 'Dictionariable' protocol to each of your structures that needs to be archived.

Check out this blogpost I wrote comparing three ways to archive and unarchive swift structures. There is a more detailed implementation and a playground file of the above explained code which you can run and test in the link.

like image 31
Vishal V. Shekkar Avatar answered Sep 30 '22 08:09

Vishal V. Shekkar