Encoding/decoding an integer

Although I've had no trouble persisting data with NSCoder in the past, I've encountered a problem after making the contents of the file conditional on certain data being initialized. Apologies for the length of the code - I wasn't sure how much would be relevant. The encoder looks like this:


func encode(with aCoder: NSCoder)

{

aCoder.encode(fID, forKey: "fID")

aCoder.encode(fDucats, forKey: "fDucats")

aCoder.encode(fHelpers, forKey: "fHelpers")

aCoder.encode(fClues, forKey: "fClues")

aCoder.encode(fMaps, forKey: "fMaps")

aCoder.encode(fPermits, forKey: "fPermits")

aCoder.encode(fExplorerID, forKey: "gExplorer")

aCoder.encode(fSurveyID, forKey: "gCampfire")

aCoder.encode(fHomesiteID, forKey: "gHomesite")

aCoder.encode(gSites, forKey: "gSites")

if fSurveyID >= 0

{

let total = gSurvey?.fTotalWaypoints

aCoder.encode(total, forKey: "fTotalWaypoints") // BREAKPOINT

aCoder.encode(gSurvey?.fWaypointsSet, forKey: "fWaypointsSet")

aCoder.encode(gSurvey?.fCampfireID, forKey: "fCampfireID")

aCoder.encode(gSurvey?.fWaypointIDs, forKey: "fWaypointIDs")

}

}


Using the debugger, I've determined that the value of total (an Int?) was 4 at the breakpoint.

The decoder looks like this:


required convenience init(coder aDecoder: NSCoder)

{

let id = aDecoder.decodeObject(forKey: "fID") as? String

let ducats = aDecoder.decodeInteger(forKey: "fDucats")

let helpers = aDecoder.decodeInteger(forKey: "fHelpers")

let clues = aDecoder.decodeInteger(forKey: "fClues")

let maps = aDecoder.decodeInteger(forKey: "fMaps")

let permits = aDecoder.decodeInteger(forKey: "fPermits")

let explorer = aDecoder.decodeInteger(forKey: "gExplorer")

let campfire = aDecoder.decodeInteger(forKey: "gCampfire")

let homesite = aDecoder.decodeInteger(forKey: "gHomesite")

self.init()


self.fID = id!

self.fDucats = ducats

self.fHelpers = helpers

self.fClues = clues

self.fMaps = maps

self.fPermits = permits

self.fExplorerID = explorer

self.fSurveyID = campfire

self.fHomesiteID = homesite

gSites = (aDecoder.decodeObject(forKey: "gSites") as? [Site])!

gExplorer = explorer < 0 ? nil : gSites[explorer]

gHomesite = homesite < 0 ? nil : gSites[homesite]

if fSurveyID >= 0

{

let total = aDecoder.decodeInteger(forKey: "fTotalWaypoints")

let unset = aDecoder.decodeInteger(forKey: "fWaypointsSet")

let cfire = aDecoder.decodeInteger(forKey: "fCampfireID")

gSurvey = Survey()

gSurvey?.fTotalWaypoints = total

gSurvey?.fWaypointsSet = unset

gSurvey?.fCampfireID = cfire

gSurvey?.fWaypointIDs = (aDecoder.decodeObject(forKey: "fWaypointIDs") as? [Int])!

}

}


At the initialization of total, the app crashes with "value for key (fTotalWaypoints) is not an integer number".

The file has already been saved several times when fSurveyID < 0, and I have been assuming that those earlier versions are cleanly overwritten each time.


Any help greatly appreciated,

Steve.

Accepted Answer

The crash sounds like the correct outcome, because you didn't save an Int as key "fTotalWaypoints", but an Int?. These are different types.


The problem here is similar to the one in your earlier post — you're mis-using the "?" operator, and mis-using optionals more generally.


Note that you (apparently) have two interrelated conditions for whether a "Survey" object exists: fSurveyID >= 0 and gSurvey != nil. In your encoder, finding that fSurveyID >= 0, you know that gSurvey is not nil, and so you don't need to conditionalize the access. A first (incorrect!) attempt to fix this problem would look like this:


        if fSurveyID >= 0
        {
            aCoder.encode(gSurvey.fTotalWaypoints, forKey: "fTotalWaypoints") // no need for intermediate "total"
            aCoder.encode(gSurvey.fWaypointsSet, forKey: "fWaypointsSet")
            aCoder.encode(gSurvey.fCampfireID, forKey: "fCampfireID")
            aCoder.encode(gSurvey.fWaypointIDs, forKey: "fWaypointIDs")
        }


But you tried this, and found it won't compile, right? The compiler tells you that "gSurvey" is of optional type. Adding the "?" to every reference silences the error, but it is the wrong thing to do. Instead, you need to do something along this line:


        if fSurveyID >= 0
        {
            let survey = gSurvey! // it can't be nil here, right? so this won't crash
            aCoder.encode(survey.fTotalWaypoints, forKey: "fTotalWaypoints")
            aCoder.encode(survey.fWaypointsSet, forKey: "fWaypointsSet")
            aCoder.encode(survey.fCampfireID, forKey: "fCampfireID")
            aCoder.encode(survey.fWaypointIDs, forKey: "fWaypointIDs")
        }


Now the "survey" local variable is non-optional, so you are really encoding an Int for "fTotalWaypoints". Another common way of doing basically the same thing is to use a compound "if" test involving an "if let" construct:


        if fSurveyID >= 0, let survey = gSurvey
        {
            aCoder.encode(survey.fTotalWaypoints, forKey: "fTotalWaypoints")
            aCoder.encode(survey.fWaypointsSet, forKey: "fWaypointsSet")
            aCoder.encode(survey.fCampfireID, forKey: "fCampfireID")
            aCoder.encode(survey.fWaypointIDs, forKey: "fWaypointIDs")
        }


The comma in the first line means "logical and", and the "let" strips the optionality from "gSurvey".


There's a similar problem in your decoder. Inside the "if", since you know you created a Survey object, you don't have to check for its subsequent existence. You'll have something like this:


        if fSurveyID >= 0
        {
            let total = aDecoder.decodeInteger(forKey: "fTotalWaypoints")
            let unset = aDecoder.decodeInteger(forKey: "fWaypointsSet")
            let cfire = aDecoder.decodeInteger(forKey: "fCampfireID")
            let survey = Survey()
            gSurvey = survey
            survey.fTotalWaypoints =  total
            survey.fWaypointsSet =    unset
            survey.fCampfireID =      cfire
            survey.fWaypointIDs = aDecoder.decodeObject(forKey: "fWaypointIDs") as! [Int] // slightly simpler than what you did, but means the same thing
        }


The takeaway here is that, in Swift, you should never solve optionality "problems" by simply throwing in "?" or "!" operators. Instead, you need to consider why an optional variable might be nil, and handle the nil vs. non-nil logical cases at the earliest possible time. That might involve a single "?" or "!" operator somewhere, or an "if let" construction, but repeating such "fixes" on multiple successive lines is almost always an indication that you've gone off track.

The takeaway here is that, in Swift, you should never solve optionality "problems" by simply throwing in "?" or "!" operators. Instead, you need to consider why an optional variable might be nil, and handle the nil vs. non-nil logical cases at the earliest possible time.

Right. By doing this at the earliest possible time you limit the scope of optionality, and thus reduce the overall effort required.

I think of this as working in ‘Swift space’ rather than the much more loosely typed ‘Objective-C space’ (or, worse yet, ‘CF space’). I just wrote a long response that centres around this idea but I’ve been nattering on about it for a while now.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks very much for this tutorial. You're right - being an old-time programmer that is just getting back into it after a long break, I never really got the hang of this new-fangled "wrapping" concept and the corresponding use of ? and !. Usually (as you say) I just plonk them in when the compiler asks for them.


But I think I have a better handle on it now. Thanks again for all the effort you put into the response.

Steve.

Encoding/decoding an integer
 
 
Q