0

I have a function which saves primitives and objects confirming to NSCoding to UserDefaults.

func set(_ value: Any?, forKey key: String) {
    if let value = value {
        if (value is NSCoding) {
            let encodedValue = NSKeyedArchiver.archivedData(withRootObject: value)
            UserDefaults.standard.set(encodedValue, forKey: key)
        } else {
            print("Storage: failed to save value for key \(key). Value must confirm to NSCoding protocol")
        }
    } else {
        UserDefaults.standard.removeObject(forKey: key)
    }
}

I want to get rid of the conformance check inside the function. So the function must accept only NSCoding values.

I’ve tried to change the function's signature to this:

func set(_ value: NSCoding?, forKey key: String)

And this:

func set<T: Any>(_ value: T?, forKey key: String) where T: NSCoding 

However, I get a compile error in every function call.

In the first case I get:

Argument type ‘***’ does not conform to expected type 'NSCoding?'

In the second:

Cannot convert value of type '***' to expected argument type '_?'

What should I change to make it work?

AtulParmar
  • 4,358
  • 1
  • 24
  • 45
BadCodeDeveloper
  • 455
  • 5
  • 24
  • That first attempt seems right. The error makes it clear that you are trying to pass a value that doesn’t conform to NSCoding. You can’t do that once you change the parameter from Any to NSCoding. – rmaddy Feb 01 '19 at 12:23
  • I don't understand why `Int` value doesn't confirm to `NSCoding` but passes check inside function `if (value is NSCoding)` – BadCodeDeveloper Feb 01 '19 at 12:31
  • The archive method you're calling doesn't require NSCoding so why do you need it? – Joakim Danielson Feb 01 '19 at 12:37
  • To be honest, I'm not sure. I'm working with legacy obj-c code which had check for `encodeWithCoder` selector – BadCodeDeveloper Feb 01 '19 at 12:49

2 Answers2

1

There seems no issue in method declaration itself

func set<T: Any>(_ value: T?, forKey key: String) where T: NSCoding {
    if let value = value {
            let encodedValue = NSKeyedArchiver.archivedData(withRootObject: value)
            UserDefaults.standard.set(encodedValue, forKey: key)
    } else {
        UserDefaults.standard.removeObject(forKey: key)
    }
}

But the compilation error points out that the value you are passing to this function might not be compatible with NSCoding

example calling the above function with

self.set("abcd", forKey: "test")

Gives error

Cannot convert value of type 'String' to expected argument type '_?'

On the other hand calling it as

self.set(NSString(string: "abcd"), forKey: "test")

Works fine. So make sure you pass correct argument :)

EDIT 1:

Based on OP's comment tested the Generics method with Int

    let test: Int = 100
    self.set(test, forKey: "test")

Results in same error, boxing it to NSNumber though

    let test: Int = 100
    self.set(NSNumber(value: test), forKey: "test")

EDIT 2:

Answering OP's comment

But why String values pass if (value is NSCoding) check in my original function?

    let a: Any = "abcd"
    if a is NSCoding {
        debugPrint("am here")
    }

Thats because your any passes the NSCoding check and in your case no matter what value you pass to function you type cast it to Any and Any passes through the check :)

Hence its working in your first method but fails with generics :) Hope it helps

Sandeep Bhandari
  • 19,999
  • 5
  • 45
  • 78
1

Maybe you can skip NSCoding all together?

func set(_ value: Any?, forKey key: String) throws {
    if let value = value {
        let encodedValue = try NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: true)
        UserDefaults.standard.set(encodedValue, forKey: key)
    } else {
        UserDefaults.standard.removeObject(forKey: key)
    }
}

(Note that I change to non-deprecated method but feel free to change back)

Example

do { 
    try set("abc", forKey: "k1")
} catch {
    print(error)
}
Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52