30

Since upgrading to Swift 4.2 I've found that many of the NSKeyedUnarchiver and NSKeyedArchiver methods have been deprecated and we must now use the type method static func unarchivedObject<DecodedObjectType>(ofClass: DecodedObjectType.Type, from: Data) -> DecodedObjectType? to unarchive data.

I have managed to successfully archive an Array of my bespoke class WidgetData, which is an NSObject subclass:

private static func archiveWidgetDataArray(widgetDataArray : [WidgetData]) -> NSData {

    guard let data = try? NSKeyedArchiver.archivedData(withRootObject: widgetDataArray as Array, requiringSecureCoding: false) as NSData
        else { fatalError("Can't encode data") }

    return data

}

The problem comes when I try to unarchive this data:

static func loadWidgetDataArray() -> [WidgetData]? {

    if isKeyPresentInUserDefaults(key: USER_DEFAULTS_KEY_WIDGET_DATA) {

        if let unarchivedObject = UserDefaults.standard.object(forKey: USER_DEFAULTS_KEY_WIDGET_DATA) as? Data {

            //THIS FUNCTION HAS NOW BEEN DEPRECATED:
            //return NSKeyedUnarchiver.unarchiveObject(with: unarchivedObject as Data) as? [WidgetData]

            guard let nsArray = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSArray.self, from: unarchivedObject as Data) else {
                fatalError("loadWidgetDataArray - Can't encode data")
            }

            guard let array = nsArray as? Array<WidgetData> else {
                fatalError("loadWidgetDataArray - Can't get Array")
            }

            return array

        }

    }

    return nil

}

But this fails, as using Array.self instead of NSArray.self is disallowed. What am I doing wrong and how can I fix this to unarchive my Array?

Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
Geoff H
  • 3,107
  • 1
  • 28
  • 53
  • Check this for a more swifty solution https://stackoverflow.com/a/51460950/5820010 – Shehata Gamal Jul 23 '18 at 21:53
  • What "fails"? What output do you get? You aren't using `Array.self` anywhere. – Paulw11 Jul 23 '18 at 22:34
  • When changed to Array.self instead of NSArray.self, the pre-compiler complains: Incorrect argument label in call (have 'ofClass:from:', expected 'ofClasses:from:') Replace 'ofClass' with 'ofClasses'. Which implies Array.self can't be used. When I use NSArray.self it compiles without issue & runs. But gets caught by the fatal error as the resulting 'nsArray' is nil. – Geoff H Jul 23 '18 at 22:38
  • 1
    So, what happens when you use `NSKeyedUnarchiver.unarchivedObject(ofClasses: [Array.self], from: unarchivedObject as Data)`? Also, instead of using `try?` use `do/try/catch` so that you can print the actual error that occurred. – Paulw11 Jul 23 '18 at 22:45
  • It results in the following complaint: Cannot convert value of type 'Array.Type' to expected element type 'AnyObject.Type' Insert ' as! AnyObject.Type' – Geoff H Jul 23 '18 at 22:59
  • 2
    guard let unarchivedFavorites = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(favoritesData!) else { return } self.channelFavorites = unarchivedFavorites as! [ChannelFavorite] – user1828845 Oct 23 '18 at 09:30

7 Answers7

47

You can use unarchiveTopLevelObjectWithData(_:) to unarchive the data archived by archivedData(withRootObject:requiringSecureCoding:). (I believe this is not deprecated yet.)

But before showing some code, you should better:

  • Avoid using NSData, use Data instead

  • Avoid using try? which disposes error info useful for debugging

  • Remove all unneeded casts


Try this:

private static func archiveWidgetDataArray(widgetDataArray : [WidgetData]) -> Data {
    do {
        let data = try NSKeyedArchiver.archivedData(withRootObject: widgetDataArray, requiringSecureCoding: false)

        return data
    } catch {
        fatalError("Can't encode data: \(error)")
    }

}

static func loadWidgetDataArray() -> [WidgetData]? {
    guard
        isKeyPresentInUserDefaults(key: USER_DEFAULTS_KEY_WIDGET_DATA), //<- Do you really need this line?
        let unarchivedObject = UserDefaults.standard.data(forKey: USER_DEFAULTS_KEY_WIDGET_DATA)
    else {
        return nil
    }
    do {
        guard let array = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(unarchivedObject) as? [WidgetData] else {
            fatalError("loadWidgetDataArray - Can't get Array")
        }
        return array
    } catch {
        fatalError("loadWidgetDataArray - Can't encode data: \(error)")
    }
}

But if you are making a new app, you should better consider using Codable.

OOPer
  • 47,149
  • 6
  • 107
  • 142
  • That hit the nail on the head. Thank you so much. Yes, I'm making a new app. If you could shed a little light on Codable.. You got my attention. Where should I start with it & why is it important. – Geoff H Jul 23 '18 at 23:03
  • 2
    @GeoffH, it's native Swift way of serializing (archiving). (For example, it works with `[WidgetData].self`, you have no need to care about `NSArray` or some other `NS`-things.) Better start with searching "swift codable" and you can find many good articles. – OOPer Jul 23 '18 at 23:08
  • Will do. Thank you so much for your help. – Geoff H Jul 23 '18 at 23:10
  • Perfect answer. Thank you so much. – Umair_UAS Jan 05 '21 at 15:37
  • how we can do for let dictionary = NSKeyedUnarchiver.unarchiveObject(with: data) as? [String: Element]? its deprecated now and typealias Element = [String: Any] – Sanoj Kashyap Jul 07 '23 at 15:02
24
unarchiveTopLevelObjectWithData(_:) 

is deprecated as well. So to unarchive data without secure coding you need to:

  1. Create NSKeyedUnarchiver with init(forReadingFrom: Data)
  2. Set requiresSecureCoding of created unarchiver to false.
  3. Call decodeObject(of: [AnyClass]?, forKey: String) -> Any? to get your object, just use proper class and NSKeyedArchiveRootObjectKeyas key.
Maciej S
  • 792
  • 1
  • 7
  • 16
  • For anyone archiving data using NSKeyedArchiver (with or without requiringSecureCoding), this is the answer. Thank you! – smakus Apr 11 '23 at 21:41
11

As unarchiveTopLevelObjectWithData is also deprecated after iOS 14.3 only the Hopreeeenjust's answer is correct now.

But if you don't need NSSecureCoding you also can use answer of Maciej S

It is very easy to use it, by adding extension to NSCoding protocol:

extension NSCoding where Self: NSObject {
    static func unsecureUnarchived(from data: Data) -> Self? {
        do {
            let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
            unarchiver.requiresSecureCoding = false
            let obj = unarchiver.decodeObject(of: self, forKey: NSKeyedArchiveRootObjectKey)
            if let error = unarchiver.error {
                print("Error:\(error)")
            }
            return obj
        } catch {
            print("Error:\(error)")
        }
        return nil
    }
}

With this extension to unarchive e.g. NSArray you only need:

let myArray = NSArray.unsecureUnarchived(from: data)

For Objective C use NSObject category:

+ (instancetype)unsecureUnarchivedFromData:(NSData *)data {
NSError * err = nil;
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData: data error: &err];
unarchiver.requiresSecureCoding = NO;
id res = [unarchiver decodeObjectOfClass:self forKey:NSKeyedArchiveRootObjectKey];
err = err ?: unarchiver.error;
if (err != nil) {
    NSLog(@"NSKeyedUnarchiver unarchivedObject error: %@", err);
}
return  res;

}

Note that if the requiresSecureCoding is false, class of unarchived object is not actually checked and objective c code returns valid result even if it is called from wrong class. And swift code when called from wrong class returns nil (because of optional casting), but without error.

Vladimir
  • 7,670
  • 8
  • 28
  • 42
  • I cannot stress how much I am gratified that this answer exists. Given that the accepted answer suggests something that is also going to be deprecated, having an answer that will hopefully work indefinitely is extremely useful. – tcobbs Oct 29 '21 at 00:15
  • This is the best – Denis Kutlubaev Feb 17 '22 at 15:18
5

You are likely looking for this:

if let widgetsData = UserDefaults.standard.data(forKey: USER_DEFAULTS_KEY_WIDGET_DATA) {
        if let widgets = (try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, WidgetData.self], from: widgetsData)) as? [WidgetData] {
            // your code
        }
    }
Hopreeeenjust
  • 257
  • 4
  • 5
  • 1
    I could resolve a same problem with your approach using ‘[NSArray.self, WidgetData.self]’. However I cound not find a public document about your approach. Do you know document url? – Mao Nishi Dec 06 '18 at 08:29
5

Swift 5- IOS 13

guard let mainData = UserDefaults.standard.object(forKey: "eventDetail") as? NSData
else {
    print(" data not found in UserDefaults")
    return
}
do {
    guard let finalArray =
    try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(mainData as Data) as? [EventDetail]
    else {
        return
    }
    self.eventDetail = finalArray
}
Milo
  • 3,365
  • 9
  • 30
  • 44
4
 if #available(iOS 12.0, *) {
        guard let unarchivedFavorites = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(favoritesData!)
            else {
                return
        }
        self.channelFavorites = unarchivedFavorites as! [ChannelFavorite]
    } else {
        if let unarchivedFavorites = NSKeyedUnarchiver.unarchiveObject(with: favoritesData!) as? [ChannelFavorite] {
            self.channelFavorites = unarchivedFavorites
        }

// Achieving data

 if #available(iOS 12.0, *) {
            // use iOS 12-only feature
            do {
                let data = try NSKeyedArchiver.archivedData(withRootObject: channelFavorites, requiringSecureCoding: false)
                UserDefaults.standard.set(data, forKey: "channelFavorites")
            } catch {
                return
            }
        } else {
            // handle older versions
            let data = NSKeyedArchiver.archivedData(withRootObject: channelFavorites)
            UserDefaults.standard.set(data, forKey: "channelFavorites")
        }

This is the way I have updated my code and its working for me

user1828845
  • 1,304
  • 1
  • 10
  • 17
  • 1
    Your availability macros don't make sense. In the first code block, why is it there? `unarchiveTopLevelObjectWithData()` was introduced in iOS 9, and deprecated in iOS 12. – Ben Kennedy Apr 23 '19 at 18:27
-1

This is just slightly different from Hopreeeenjust answer above. Anyway, I like his answer as it's simple and uses the syntax that Apple recommends.

    if let archivedData = UserDefaults.standard.object(forKey: "myKey") as? Data {
        do {
            if let myArray = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, NSString.self], from: archivedData) as? [String] {
             myPreviouslyDefinedArray = myArray
            }
        } catch {
             print("read error: \(error.localizedDescription)")
        }
    }
slicerdicer
  • 155
  • 1
  • 10