A Swift package that bridges NSKeyedUnarchiver with Swift's Codable system, allowing you to decode data archived with NSKeyedArchiver into Swift Codable types.
NSDecoder is a custom decoder that implements Swift's Decoder protocol, enabling you to decode NSKeyedArchiver data into any Swift type that conforms to Codable.
- ✅ Decodes data from
NSKeyedArchiverintoCodabletypes - ✅ Supports all primitive types (Bool, Int, Float, Double, String, etc.)
- ✅ Supports nested structures with
NSCoderBridge - ✅ Supports arrays, including nested arrays (e.g.,
[[Int]],[[[String]]]) - ✅ Supports sets of primitives and hashable types
- ✅ Supports dictionaries with arbitrary nesting (e.g.,
[String: [Int]],[String: [String: T]]) - ✅ Supports complex nested collections (arrays of dictionaries, dictionaries of arrays, etc.)
- ✅ Supports legacy NSCoding object mapping with complex nested structures
- ✅ Supports keyed and unkeyed containers
- ✅ Supports custom decoder user info
- ✅ Full Swift 6.1 compatibility with strict concurrency
Add this package to your Package.swift file:
dependencies: [ .package(url: "https://github.com/yourusername/NSCodable.git", from: "1.0.0") ]
import Foundation import NSCodable // Define your Codable type struct Person: Codable { let name: String let age: Int let email: String } // Create archived data using NSKeyedArchiver let archiver = NSKeyedArchiver(requiringSecureCoding: false) archiver.encode("John Doe", forKey: "name") archiver.encode(30, forKey: "age") archiver.encode("[email protected]", forKey: "email") archiver.finishEncoding() let data = archiver.encodedData // Decode using NSDecoder let decoder = NSDecoder() decoder.options.rootKey = nil let person = try decoder.decode(Person.self, from: data) print(person.name) // "John Doe" print(person.age) // 30
You can decode data from legacy NSCoding objects into modern Codable structs using the mappedClasses option:
// Legacy NSCoding class @objc(LegacyLocation) class LegacyLocation: NSObject, NSCoding { let latitude: Double let longitude: Double let name: String init(latitude: Double, longitude: Double, name: String) { self.latitude = latitude self.longitude = longitude self.name = name super.init() } required init?(coder: NSCoder) { self.latitude = coder.decodeDouble(forKey: "latitude") self.longitude = coder.decodeDouble(forKey: "longitude") self.name = coder.decodeObject(forKey: "name") as? String ?? "" super.init() } func encode(with coder: NSCoder) { coder.encode(latitude, forKey: "latitude") coder.encode(longitude, forKey: "longitude") coder.encode(name, forKey: "name") } } // Modern Codable struct struct Location: Codable { let latitude: Double let longitude: Double let name: String } // Create and archive legacy object let legacyLocation = LegacyLocation(latitude: 37.7749, longitude: -122.4194, name: "SF") let data = try NSKeyedArchiver.archivedData( withRootObject: legacyLocation, requiringSecureCoding: false ) // Decode into modern Codable struct - map the legacy class let options = NSDecoder.Options(mappedClasses: ["LegacyLocation"]) let decoder = NSDecoder(options: options) let modernLocation = try decoder.decode(Location.self, from: data)
The decoder automatically handles nested NSCoding objects through NSCoderBridge. You can nest structures arbitrarily deep:
@objc(LegacyAddress) class LegacyAddress: NSObject, NSCoding { let street: String let city: String // ... NSCoding implementation } @objc(LegacyPerson) class LegacyPerson: NSObject, NSCoding { let name: String let address: LegacyAddress // Nested NSCoding object let tags: [String] // Arrays work too // ... NSCoding implementation } // Modern structs struct Address: Codable { let street: String let city: String } struct Person: Codable { let name: String let address: Address let tags: [String] } // Decode with mapped classes let options = NSDecoder.Options(mappedClasses: Set([ "LegacyPerson", "LegacyAddress" ])) let decoder = NSDecoder(options: options) let modern = try decoder.decode(Person.self, from: legacyData)
NSDecoder supports configuration through the Options type:
// Create options with mapped classes var options = NSDecoder.Options() options.mappedClasses.insert("LegacyProduct") // Initialize decoder with options let decoder = NSDecoder(options: options) // Or modify options after initialization let decoder = NSDecoder() decoder.options.mappedClasses.insert("MyClass")
-
mappedClasses: A
Set<String>containing legacy NSCoding class names that should be mapped to Codable types during decoding. Add the class names (from@objc(ClassName)or the Swift class name) to enable automatic bridging throughNSCoderBridge. -
rootKey: An optional
Stringspecifying the key for the root object in the archive (defaults toNSKeyedArchiveRootObjectKey). Set tonilto decode directly without expecting a root key wrapper.
You can pass custom information to your decoder using the userInfo dictionary:
struct Item: Codable { let name: String init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.name = try container.decode(String.self, forKey: .name) // Access custom user info if let context = decoder.userInfo[CodingUserInfoKey(rawValue: "context")!] as? String { print("Context: \(context)") } } private enum CodingKeys: String, CodingKey { case name } } let decoder = NSDecoder() decoder.userInfo[CodingUserInfoKey(rawValue: "context")!] = "custom-context" let item = try decoder.decode(Item.self, from: data)
NSDecoder fully supports arrays, including nested arrays of arbitrary depth and arrays of complex NSCoding objects:
struct Matrix: Codable { let data: [[Int]] // 2D array let cube: [[[Double]]] // 3D array } // Archives with nested arrays work seamlessly let decoder = NSDecoder(options: options) let matrix = try decoder.decode(Matrix.self, from: archivedData)
Sets of primitives and hashable types are supported:
struct Tags: Codable { let keywords: Set<String> let priorities: Set<Int> } let decoder = NSDecoder(options: options) let tags = try decoder.decode(Tags.self, from: archivedData)
Dictionaries with arbitrary nesting levels are fully supported, including primitive and complex object values:
// Dictionaries with primitive values struct Config: Codable { let settings: [String: String] let counts: [String: Int] let flags: [String: Bool] } // Dictionaries of arrays struct Schedule: Codable { let tasks: [String: [String]] let priorities: [String: [Double]] } // Nested dictionaries (3 levels deep) struct DeepConfig: Codable { let config: [String: [String: [String: String]]] } // Dictionaries with complex NSCoding object values struct Workboard: Codable { let columns: [String: [Task]] // Dictionary of arrays of objects } struct Monitoring: Codable { let services: [String: [String: Metric]] // Dictionary of dictionaries of objects } let decoder = NSDecoder(options: options) let config = try decoder.decode(Config.self, from: archivedData)
NSDecoder handles complex nested structures combining arrays, dictionaries, and sets:
// Array of dictionaries struct EventLog: Codable { let events: [[String: String]] } // Dictionary of arrays (with complex objects) struct TaskBoard: Codable { let columns: [String: [Task]] } // Array of dictionaries of arrays struct Course: Codable { let modules: [[String: [String]]] } // All combinations work seamlessly with NSCoding object mapping let options = NSDecoder.Options(mappedClasses: ["LegacyTask", "LegacyEvent"]) let decoder = NSDecoder(options: options) let data = try decoder.decode(YourType.self, from: legacyData)
NSCodable uses an internal bridging mechanism to convert legacy NSCoding objects into modern Codable types. When you specify class names in options.mappedClasses, the decoder automatically intercepts those classes during decoding and converts them to your Codable types.
This happens transparently - you don't need to interact with the internal components. Just specify the legacy class names in options.mappedClasses and the decoder handles the rest.
For technical details about the implementation, see CONTRIBUTING.md.
- Booleans:
Bool - Integers:
Int,Int8,Int16,Int32,Int64,UInt,UInt8,UInt16,UInt32,UInt64 - Floating Point:
Float,Double - Strings:
String - Data:
Data - Collections:
- Arrays:
[T]including nested arrays like[[Int]],[[[String]]], and arrays of NSCoding objects - Sets:
Set<T>whereTisHashable - Dictionaries:
[K: V]whereKisStringandVcan be:- Primitives (Int, String, Bool, Double, etc.)
- Arrays or nested arrays
- Other dictionaries (arbitrary nesting depth)
- Complex NSCoding objects (via
mappedClasses)
- Arrays:
- Complex Nested Collections: Arrays of dictionaries, dictionaries of arrays, dictionaries of dictionaries, etc.
- Nested Types: Any
Codabletype - Legacy NSCoding Objects: Via
mappedClassesandNSCoderBridge, including deeply nested structures
- Usage of
allKeysinKeyedDecodingContainerrequiresCaseIterableforCodingKey, asNSKeyedUnarchiverdoesn't provide runtime key enumeration - The decoder is designed specifically for reading
NSKeyedArchiverdata
- Swift 6.1+
- macOS 14.0+ / iOS 17.0+ / tvOS 17.0+ / watchOS 10.0+
MIT License
Contributions are welcome! For technical details about the internal architecture, development setup, and guidelines, see CONTRIBUTING.md.