Problem updating to Swift 4.2

I am trying to migrate an existing project from Swift 4 to Swift 4.2, and don´t know how to modify some piece of the code.


I have the following class to keep track of the different CKRecordZone names and their CKServerChangeTokens.


import Foundation
import CloudKit

class CloudKitFetchChangeToken{

    private var dictionaryFetchChangeToken : [String : CKServerChangeToken]?{
        get {
            guard let defaults = UserDefaults(suiteName: "group.com.company.AppName") else { fatalError("Can not get UserDefaults for suiteName" ) }
             
            guard let tokenData = defaults.dictionaryFetchChangeToken else { return nil }

            // Using SWIFT 4 
     // Warning  SWIFT 4.2:  'unarchiveObject(with:)' was deprecated in iOS 12.0: Use +unarchivedObjectOfClass:fromData:error: instead
            return NSKeyedUnarchiver.unarchiveObject(with: tokenData) as? [String : CKServerChangeToken] 
        }
        
        set {
            guard let newValue = newValue else {
                guard let defaults = UserDefaults(suiteName: "group.com.company.AppName") else { fatalError("Can not get UserDefaults for suiteName" ) }
                defaults.dictionaryFetchChangeToken = nil
                return
            }
            
            // Using SWIFT 4
            //let data = NSKeyedArchiver.archivedData(withRootObject: newValue)  
            
             // Using SWIFT 4.2
            let coder = NSKeyedArchiver(requiringSecureCoding: true)
            coder.encode(newValue, forKey: NSKeyedArchiveRootObjectKey)
            let data = coder.encodedData

            guard let defaults = UserDefaults(suiteName: "group.com.company.AppName") else { fatalError("Can not get UserDefaults for suiteName" ) }
  
            defaults.dictionaryFetchChangeToken = data
        }
    }
    
    private let queue = DispatchQueue(label: "com.AppName.CloudKitFetchChangeToken.queue")  

    public subscript(key: String) -> CKServerChangeToken? {
        get {
            return queue.sync { 
                return dictionaryFetchChangeToken?[key]
            }
        }
        set(newValue) {
            queue.sync {
                dictionaryFetchChangeToken?[key] = newValue
            }
        }
    }
    
    var isEmpty: Bool {
        return dictionaryFetchChangeToken?.count == 0
    }
    
    var existStorage : Bool{
        return dictionaryFetchChangeToken != nil
    }
    
    func createSorage(){
        dictionaryFetchChangeToken = [:]
    }
}

My problem is with var dictionaryFetchChangeToken, I think I managed ok the change of the sintax from Swift 4 to Swift 4.2 in the set (), but

I don´t know how to do it in the get() part of the code.


Warning SWIFT 4.2: 'unarchiveObject(with:)' was deprecated in iOS 12.0: Use +unarchivedObjectOfClass:fromData:error: instead.


In Swift Dictionary is a Generic Structure, and the previous method is waiting for a class.

Accepted Reply

I could have found my mistake without your `UserDefaults.dictionaryFetchChangeToken`.


Please try changing this line:

return try NSKeyedUnarchiver.unarchivedObject(ofClass: NSDictionary.self, from: tokenData) as? [String : CKServerChangeToken] 

To:

return try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(tokenData) as? [String : CKServerChangeToken]


I recently wrote similar code and I thought I remembered it, but that was wrong...


(Addition)

This may also work.

return try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSDictionary.self, CKServerChangeToken.self], from: tokenData) as? [String : CKServerChangeToken]

Replies

In iOS 11, some new methods were added to `NSKeyedUnarchiver` and `NSKeyedArchiver`, which throws Error instead of raising Exception on invalid input.


Direct alternative to this line:

return NSKeyedUnarchiver.unarchiveObject(with: tokenData) as? [String : CKServerChangeToken]


would be this:

return try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSDictionary.self, from: tokenData) as? [String : CKServerChangeToken]

If you do not want to ignore errors silently, the code above would be:

do {
    return try NSKeyedUnarchiver.unarchivedObject(ofClass: NSDictionary.self, from: tokenData) as? [String : CKServerChangeToken]
} catch {
    print(error)
    return nil
}

And for your `set` code,

Direct alternative to this line:

let data = NSKeyedArchiver.archivedData(withRootObject: newValue)


Would be something like this:

guard let data = try? NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: true) else {
    fatalError("archivedData failed")
}

Or:

do {
    let data = try NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: true)
    //... use data inside this code block
} catch {
    fatalError("archivedData failed with error: \(error)")
}


In Swift Dictionary is a Generic Structure, and the previous method is waiting for a class.


Pass `NSDictionary.self`.

`NSKeyedArchiver` and `NSKeyedUnarchiver` uses `NSDictionary` for Swift `Dictionary` while archiving/unarchiving.

Thank you very much, I really apreciate your detailed answer, have a nice day!

Hi OOPer, I followed your approach, but now I am getting this error in the get{ } of dictionaryFetchChangeToken :


     do {
                return try NSKeyedUnarchiver.unarchivedObject(ofClass: NSDictionary.self, from: tokenData) as? [String : CKServerChangeToken]
            } catch {
                print(error)
                /*
                 Error Domain=NSCocoaErrorDomain Code=4864 "value for key 'NS.objects' was of unexpected class 'CKServerChangeToken'. Allowed classes are '{(
                 NSDictionary
                 )}'." UserInfo={NSDebugDescription=value for key 'NS.objects' was of unexpected class 'CKServerChangeToken'. Allowed classes are '{(
                 NSDictionary
                 )}'.}
                */
                return nil
            }

In the set { } prior to:


     guard let data = try? NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: true) else {
                fatalError("archivedData failed")
            }


I print in the console newValue, and get:


po newValue

▿ 1 element

▿ 0 : 2 elements

- key : "7A72KACD"

- value : <CKServerChangeToken: 0x600000546500; data=AQAAAAAAAAAIf/////////9qgapowFdF0b2yxdsEOx/U>


Why is this happening?

Maybe it's something related to your another `dictionaryFetchChangeToken`, `UserDefaults.dictionaryFetchChangeToken`.

Please show the definition of it, and the full code of your latest definition of `CloudKitFetchChangeToken.dictionaryFetchChangeToken`.

I could have found my mistake without your `UserDefaults.dictionaryFetchChangeToken`.


Please try changing this line:

return try NSKeyedUnarchiver.unarchivedObject(ofClass: NSDictionary.self, from: tokenData) as? [String : CKServerChangeToken] 

To:

return try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(tokenData) as? [String : CKServerChangeToken]


I recently wrote similar code and I thought I remembered it, but that was wrong...


(Addition)

This may also work.

return try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSDictionary.self, CKServerChangeToken.self], from: tokenData) as? [String : CKServerChangeToken]

I think both of your solutions are right, I tried both of them, an the previous error do not arise again.


By the way this is the code of UserDefaults.dictionaryFetchChangeToken.


import Foundation

struct UserDefaultsKeys {
    static let dictionaryFetchChangeToken = "dictionaryFetchChangeToken"
}

extension UserDefaults {
    var dictionaryFetchChangeToken : Data?  {
        get {
            return  data(forKey: UserDefaultsKeys.dictionaryFetchChangeToken)
        }
        set { set(newValue, forKey: UserDefaultsKeys.dictionaryFetchChangeToken) }
    }
}


Thank you very much.

In fact, `unarchiveTopLevelObjectWithData` is exactly the method I used in a code I wrote 4 months ago.

(Sorry, I shoud have dug it out before I write my first code for you.)


Though, the latter `unarchivedObject(ofClasses:from:)` may be a preferred method when you work with data archived with `requiringSecureCoding: true`. (Maybe that may not be a big issue when you get the archived data only from `UserDefaults`.)

Just that the Apple's documentation is so poor...


Anyway, with more related code given, more easily verified code shown. Thank you.

I will need to spend some time in get deep insights from your reply, to fully understand what is going on.


Thanks again.