3

So I've just converted a small app from Swift 2.2 to Swift 3. I've gotten rid of the usual errors and bits of mop up required after the auto converter but I've got a run time issue that I can't work out.

I've got a custom class that I am saving to NSUserDefaults with the NSCoding protocol. When I try to decode the encoded object from NSUserDefaults it fails on the guard let duration = decoder.decodeObject(forKey: "duration") as? Int line as duration is printing as nil. Decoding the title string works fine however so at least that line of the encode function is working correctly.

This worked fine in 2.2 and I can't find anything indicating that Swift 3 made changes to the NSCoding. Any help would be much appreciated.

class TimerModel: NSObject, NSCoding, AVAudioPlayerDelegate {

    var name: String
    var active: Bool
    var paused: Bool
    var duration: Int
    var remainingWhenPaused: Int?
    var timerEndTime: Date?
    var timerStartTime: Date?
    var audioAlert: AlertNoise
    var UUID: String
    var colorScheme: BaseColor
    var alarmRepetitions: Int
    var timerRepetitions: Int
    var currentTimerRepetition: Int
    var audioPlaying: Bool
    var player: AVAudioPlayer = AVAudioPlayer()
    var countDownTimer: Timer = Timer()
    var delegate: timerProtocol? = nil

    init(withName name: String, duration: Int, UUID: String, color: BaseColor, alertNoise: AlertNoise, timerRepetitions: Int, alarmRepetitions: Int) {
        self.name = name
        self.active = false
        self.paused = false
        self.duration = duration
        self.UUID = UUID
        self.audioAlert = alertNoise
        self.colorScheme = color
        self.alarmRepetitions = alarmRepetitions
        self.audioPlaying = false
        self.timerRepetitions = timerRepetitions
        self.currentTimerRepetition = 0

        super.init()
    }

    convenience override init() {
        self.init(withName: "Tap Timer 1", duration: 10, UUID: Foundation.UUID().uuidString, color: .Red, alertNoise: .ChurchBell, timerRepetitions: 1, alarmRepetitions: 0)
    }

    // MARK: NSCoding

    required convenience init? (coder decoder: NSCoder) {
        print("in init coder:")
        print("Name: \(decoder.decodeObject(forKey: "name"))")
        print("Duration: \(decoder.decodeObject(forKey: "duration"))")
        guard let name = decoder.decodeObject(forKey: "name") as? String
        else {
            print("init coder name guard failed")
            return nil
        }
        guard let duration = decoder.decodeObject(forKey: "duration") as? Int
        else {
            print("init coder duration guard failed")
            print("duration: \(decoder.decodeObject(forKey: "duration"))")
            return nil
        }
        guard let audioAlertRawValue = decoder.decodeObject(forKey: "audioAlert") as? String
        else {
            print("init coder audioAlert guard failed")
            return nil
        }
        guard let UUID = decoder.decodeObject(forKey: "UUID") as? String
        else {
            print("init coder UUID guard failed")
            return nil
        }
        guard let colorSchemeRawValue = decoder.decodeObject(forKey: "colorScheme") as? String
        else {
            print("init coder colorScheme guard failed")
            return nil
        }
        guard let alarmRepetitions = decoder.decodeObject(forKey: "alarmRepetitions") as? Int
        else {
            print("init coder alarmRepetitions guard failed")
            return nil
        }
        guard let timerRepetitions = decoder.decodeObject(forKey: "timerRepetitions") as? Int
        else {
            print("init coder timerRepetitions guard failed")
            return nil
        }

        guard let audioAlert = AlertNoise(rawValue: audioAlertRawValue)
            else{
                print("No AlertNoise rawValue case found")
                return nil
        }
        guard let colorScheme = BaseColor(rawValue: colorSchemeRawValue)
            else{
                print("No BaseColor rawValue case found")
                return nil
        }

        print("initCoder guards passed, initing timer")
        print("\(name), \(duration), \(UUID), \(colorScheme), \(audioAlert), \(timerRepetitions), \(alarmRepetitions)")

        self.init(withName: name, duration: duration, UUID: UUID, color: colorScheme, alertNoise: audioAlert, timerRepetitions: timerRepetitions, alarmRepetitions: alarmRepetitions)
    }

    func encode(with aCoder: NSCoder) {

        aCoder.encode(self.name, forKey: "name")
        aCoder.encode(self.duration, forKey: "duration")
        aCoder.encode(self.audioAlert.rawValue, forKey: "audioAlert")
        aCoder.encode(self.UUID, forKey: "UUID")
        aCoder.encode(self.colorScheme.rawValue, forKey: "colorScheme")
        aCoder.encode(self.alarmRepetitions, forKey: "alarmRepetitions")
        aCoder.encode(self.timerRepetitions, forKey: "timerRepetitions")
    }
SimonBarker
  • 1,384
  • 2
  • 16
  • 31

2 Answers2

6

So it seems the solution is simple if a little unintuitive.

So I encoded the class ivars with the general method encode(self.ivar, forKey: "keyName") however if that ivar is an int it needs to be decoded with decodeInteger(forKey: "keyName") - this entails getting rid of the guard statements as well since this method return an non-optional. Seems odd to have to decode with the integer specific method if it was decoded with the generalist method - this wasn't the case in Swift 2.2.

SimonBarker
  • 1,384
  • 2
  • 16
  • 31
  • 1
    "Seems odd to have to decode with the integer specific method if it was decoded with the generalist method" This is causing me so many headaches. – George Nov 12 '16 at 03:24
  • Agreed @George, feels like a bug but to be honest I', not going to go through the faff of submitting a report as Apple probably won't address it – SimonBarker Nov 12 '16 at 11:41
  • 1
    According to my observations decodeObject() decodes fine for Int? but has to be changed to decodeInteger() if you have Int. Now, I wonder about all other non-optional types. – Vitalii Jan 03 '17 at 14:53
1

Excellent answer by SimonBarker and it solved the same issue I had. I eventually applied his solution to my own code and revised it too so that encoding is done with the generalist method. You can "force" encoding with the generalist method by using:

func encode(_ objv: Any?, forKey key: String)

So in your code you could use:

aCoder.encode(self.name as Any?, forKey: "name")

That way self.name is encoded as an object and does not break your existing code, namely: decoder.decodeObject(forKey: "name") as? String

This might not be the most elegant solution, but at least it worked for me without the need to change code that worked beautifully in Swift 2.3, but which was broken in Swift 3...

JohnV
  • 981
  • 2
  • 8
  • 18