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?
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.
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With