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) // 30You 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.