Skip to content

SwiftyLab/NSCodable

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

NSCodable

A Swift package that bridges NSKeyedUnarchiver with Swift's Codable system, allowing you to decode data archived with NSKeyedArchiver into Swift Codable types.

Overview

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.

Features

  • âś… Decodes data from NSKeyedArchiver into Codable types
  • âś… 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

Installation

Add this package to your Package.swift file:

dependencies: [
    .package(url: "https://github.com/yourusername/NSCodable.git", from: "1.0.0")
]

Usage

Basic Usage

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

Decoding Legacy NSCoding Objects

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)

Nested NSCoding Objects

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)

Decoder Options

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")

Options Properties

  • 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 through NSCoderBridge.

  • rootKey: An optional String specifying the key for the root object in the archive (defaults to NSKeyedArchiveRootObjectKey). Set to nil to decode directly without expecting a root key wrapper.

User Info

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)

Collections Support

Arrays

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

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

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)

Complex Nested Collections

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)

How It Works

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.

Supported Types

  • 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> where T is Hashable
    • Dictionaries: [K: V] where K is String and V can be:
      • Primitives (Int, String, Bool, Double, etc.)
      • Arrays or nested arrays
      • Other dictionaries (arbitrary nesting depth)
      • Complex NSCoding objects (via mappedClasses)
  • Complex Nested Collections: Arrays of dictionaries, dictionaries of arrays, dictionaries of dictionaries, etc.
  • Nested Types: Any Codable type
  • Legacy NSCoding Objects: Via mappedClasses and NSCoderBridge, including deeply nested structures

Limitations

  • Usage of allKeys in KeyedDecodingContainer requires CaseIterable for CodingKey, as NSKeyedUnarchiver doesn't provide runtime key enumeration
  • The decoder is designed specifically for reading NSKeyedArchiver data

Requirements

  • Swift 6.1+
  • macOS 14.0+ / iOS 17.0+ / tvOS 17.0+ / watchOS 10.0+

License

MIT License

Contributing

Contributions are welcome! For technical details about the internal architecture, development setup, and guidelines, see CONTRIBUTING.md.

About

Brings NSCoding functionality to Swift's Codable

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages