archive array of optional structs with NSCoding in Swift?
Asked Answered
B

4

19

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?

Bravar answered 14/8, 2014 at 14:50 Comment(0)
W
10

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

Windgall answered 16/8, 2014 at 21:20 Comment(5)
The reason the cast fails is that AnyObject can only be an instance of a class not a struct/enum/tuple.Chong
@Chong Thanks! - so if I understand correctly decodeObjectForKey returns an AnyObject, and we CAN'T cast this to a Swift array of structs? -- what would you suggest is the best way to handle this? -- Using NSMutableArray? or using a class instead of a struct?Windgall
@John Thank you... this put me on the right path to the solution! I've determined now that non-moves (player runs out of time for that move) will simply be at (-1, -1), since that's off the board instead of using an array of optionals. Plus, I changed SquareCoords into a class as recommended above. It still feels like a hack, but I think this is the cleanest I'll be able to make my code given the way NSCoding works.Bravar
@Chuck my Pleasure - glad I could help. So the problem is basically the incompatibility of value types like structs with NSCoding right?Windgall
@ChuckSmith looking at this question: https://mcmap.net/q/665764/-encoding-c-structs - It looks like value types don't play well with NSCoding - I understand that Swift.String is a value type however it's toll free bridged to it's NSString equivalent, and Swift.Int to NSNumber.Windgall
A
27

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.

Attu answered 19/8, 2014 at 21:54 Comment(0)
W
10

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

Windgall answered 16/8, 2014 at 21:20 Comment(5)
The reason the cast fails is that AnyObject can only be an instance of a class not a struct/enum/tuple.Chong
@Chong Thanks! - so if I understand correctly decodeObjectForKey returns an AnyObject, and we CAN'T cast this to a Swift array of structs? -- what would you suggest is the best way to handle this? -- Using NSMutableArray? or using a class instead of a struct?Windgall
@John Thank you... this put me on the right path to the solution! I've determined now that non-moves (player runs out of time for that move) will simply be at (-1, -1), since that's off the board instead of using an array of optionals. Plus, I changed SquareCoords into a class as recommended above. It still feels like a hack, but I think this is the cleanest I'll be able to make my code given the way NSCoding works.Bravar
@Chuck my Pleasure - glad I could help. So the problem is basically the incompatibility of value types like structs with NSCoding right?Windgall
@ChuckSmith looking at this question: https://mcmap.net/q/665764/-encoding-c-structs - It looks like value types don't play well with NSCoding - I understand that Swift.String is a value type however it's toll free bridged to it's NSString equivalent, and Swift.Int to NSNumber.Windgall
D
8

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.

Desjardins answered 28/2, 2016 at 18:15 Comment(2)
Yes @vishal-v-shekkar! You win at saving structs onto the filesystem in Swift. I've been looking at doing this in different ways without having to use an NSObject and tried the same thing with NSData but then I had to insert separator characters and this is by far the best. Thanks again, nice work :)Benbena
Well, thank you, @HenryHeleine. Inserting separator characters and reading them back is too much work.Desjardins
M
0

Besides that NSCoding doesn't support structs as mentioned above, my suggestion is your class should also inherit from NSObject in order to be able to encode and decode itself and its properties.

Mello answered 20/1, 2016 at 20:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.