Implementeer aangepaste codering en decodering voor elk waardetype om gewone geneste waarden met een recursieve Swift-enum te behouden. In deze walkthrough bouwen we een synthetisch Mac-appmodel dat volledig in het geheugen heen en terug wordt omgezet via Foundation-objecten, zonder wrappers voor enum-cases toe te voegen.

Waarom verandert Codable-synthese de representatie?

Stel dat een Mac-desktopapp een kleine lay-outpreview heeft met labels, numerieke waarden, schakelaars, lijsten en geneste groepen. Een recursieve enum kan die waarden nauwkeurig beschrijven. De concrete taak hier is om die enum om te zetten in een gewone Foundation-objectgraaf en deze opnieuw op te bouwen met behoud van de ondersteunde waardetypen.

Automatische conformance levert niet de vereiste representatie op. Swift’s geïmplementeerde ontwerp voor enumsynthese plaatst de naam van de case in een buitenste keyed container en plaatst de geassocieerde waarden vervolgens in een andere container. Waarden zonder label krijgen gegenereerde sleutels zoals _0. Daardoor krijgt een string-case structurele wrappers in plaats van een gewone string te worden. Deze regels zijn beschreven in Codable synthesis for enums with associated values.

Je kunt die wrappers hernoemen met CodingKeys, maar met synthese behoud je het structurele model. De ondersteunde oplossing is om zelf init(from:) en encode(to:) te implementeren. Apple documenteert handmatige conformance expliciet voor typen waarvan de Swift-structuur verschilt van hun gecodeerde structuur in Encoding and Decoding Custom Types.

Recursie en representatie zijn afzonderlijke keuzes. Swift’s indirect-modifier maakt recursieve enum-opslag mogelijk; aangepaste coderingsmethoden bepalen de gecodeerde vorm. De taalhandleiding legt uit hoe je indirect vóór de enumdeclaratie plaatst om indirectie toe te passen op de cases met geassocieerde waarden, in Enumerations.

Wat moet de gewone representatie precies behouden?

Gebruik voor dit voorbeeld het volgende contract. Het ondersteunt bewust een kleine verzameling waardetypen die geschikt is voor een synthetische lay-outpreview. De gedocumenteerde conversieregels van Foundation staan strings, getallen, arrays, dictionaries en null-objecten toe, vereisen string-sleutels in dictionaries en sluiten niet-eindige getallen uit. Zie JSONSerialization.

Enum-case Gewone waarde Compatibiliteitsvereiste
null NSNull Behoud een expliciete null-waarde
bool Booleaanse waarde Houd deze gescheiden van numerieke nul en één
number Eindige Double-waarde Accepteer de precisiebeperkingen van Double
string String Behoud tekst zonder numerieke conversie
array Array van gewone waarden Behoud de volgorde van elementen en lege arrays
object Dictionary met string-sleutels Behoud sleutels, geneste waarden en lege objecten

Compatibiliteit betekent hier dat deze categorieën en hun waarden behouden blijven binnen de genoemde numerieke beperkingen. Het is niet nodig om een bepaalde Foundation-getalklasse of de volgorde van dictionary-sleutels te behouden. Ook een expliciete null-vermelding moet behouden blijven: een object met "subtitle": NSNull() heeft een andere modelbetekenis dan een object zonder de sleutel subtitle.

Bepaal voordat je dit contract overneemt of getallen daadwerkelijk één Double-case kunnen delen. Apple documenteert Double als een 64-bits drijvendekommagetaltype en biedt initialisatoren voor exacte conversie, omdat gewone conversies kunnen afronden. Als je model grote gehele getallen exact moet verwerken, kies dan een expliciet numeriek beleid voordat je ze converteert. Het toevoegen van een integer-case vereist ook een decoderingregel, omdat gewone numerieke waarden geen Swift-enumcase-identiteit bevatten. Zie Double.

Hoe implementeer je de recursieve enum?

Gebruik een actuele Mac-Swift-omgeving met Foundation. De volgende drie codeblokken vormen samen, in deze volgorde, één compleet voorbeeld op topniveau: de enum, conversiefuncties en een uitvoerbare controlefunctie. Alle voorbeeldwaarden worden in het geheugen aangemaakt. Swift was niet beschikbaar in de onderzoeksomgeving, dus het voorbeeld is nagekeken maar niet uitgevoerd.

De decoder controleert eerst op null en probeert daarna de ondersteunde primitieve typen. Apple’s SingleValueDecodingContainer biedt deze bewerkingen, waaronder decodeNil(). De lokale helper behandelt alleen een type mismatch als reden om een andere primitieve waarde te proberen; andere decodeerfouten worden doorgegeven.

import Foundation

indirect enum PlainValue: Codable, Equatable {
    case null
    case bool(Bool)
    case number(Double)
    case string(String)
    case array([PlainValue])
    case object([String: PlainValue])

    init(from decoder: Decoder) throws {
        let scalar = try decoder.singleValueContainer()

        if scalar.decodeNil() {
            self = .null
            return
        }

        func matching<T: Decodable>(_ type: T.Type) throws -> T? {
            do {
                return try scalar.decode(type)
            } catch DecodingError.typeMismatch(_, _) {
                return nil
            }
        }

        if let value = try matching(Bool.self) {
            self = .bool(value)
            return
        }
        if let value = try matching(String.self) {
            self = .string(value)
            return
        }
        if let value = try matching(Double.self) {
            guard value.isFinite else {
                throw DecodingError.dataCorrupted(.init(
                    codingPath: decoder.codingPath,
                    debugDescription: "Expected a finite number."
                ))
            }
            self = .number(value)
            return
        }

        var sequence: UnkeyedDecodingContainer?
        do {
            sequence = try decoder.unkeyedContainer()
        } catch DecodingError.typeMismatch(_, _) {
            sequence = nil
        }

        if var items = sequence {
            var values: [PlainValue] = []
            while !items.isAtEnd {
                values.append(try items.decode(PlainValue.self))
            }
            self = .array(values)
            return
        }

        self = .object(try scalar.decode([String: PlainValue].self))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()

        switch self {
        case .null:
            try container.encodeNil()
        case .bool(let value):
            try container.encode(value)
        case .number(let value):
            guard value.isFinite else {
                throw EncodingError.invalidValue(value, .init(
                    codingPath: encoder.codingPath,
                    debugDescription: "Expected a finite number."
                ))
            }
            try container.encode(value)
        case .string(let value):
            try container.encode(value)
        case .array(let values):
            try container.encode(values)
        case .object(let values):
            try container.encode(values)
        }
    }
}

Elke coderingsvertakking schrijft de payload rechtstreeks weg. De null-vertakking roept expliciet encodeNil() aan en de collecties vertakken de verwerking naar de coderingsimplementaties van arrays en dictionaries. Apple documenteert zowel primitieve als generieke coderingsbewerkingen in SingleValueEncodingContainer en de conformance van collecties in Encoding and Decoding Custom Types.

De detectie van arrays staat bewust los van het decoderen van array-elementen. Het aanvragen van een unkeyed container kan een type mismatch veroorzaken als de huidige waarde geen array is. Nadat een array is herkend, wordt elk element met de gewone try gedecodeerd, zodat een fout bij een element wordt doorgegeven in plaats van een niet-gerelateerde poging om een object te decoderen te starten. Het gedocumenteerde gedrag wordt beschreven in unkeyedContainer() en UnkeyedDecodingContainer.

Deze implementatie is bedoeld voor Foundation’s JSON-codeerpad en de genoemde verzameling waardetypen. Test elke andere encoder of decoder aan de hand van hetzelfde contract voordat je die gebruikt; conformance met Codable alleen garandeert geen identiek gedrag voor verschillende representatieformaten.

Hoe converteer je volledig in het geheugen naar Foundation-objecten?

Foundation biedt twee nuttige grenzen: JSONEncoder maakt Data van een codeerbare waarde en JSONDecoder reconstrueert een decodeerbare waarde uit Data. JSONSerialization converteert tussen die tussenrepresentatie en Foundation-objecten. Door deze gedocumenteerde bewerkingen te combineren ontstaat de volgende brug in het geheugen. Zie JSONEncoder, JSONDecoder en JSONSerialization.

func foundationObject(from value: PlainValue) throws -> Any {
    let bytes = try JSONEncoder().encode(value)
    return try JSONSerialization.jsonObject(
        with: bytes,
        options: [.fragmentsAllowed]
    )
}

func plainValue(from object: Any) throws -> PlainValue {
    let bytes = try JSONSerialization.data(
        withJSONObject: object,
        options: [.fragmentsAllowed]
    )
    return try JSONDecoder().decode(PlainValue.self, from: bytes)
}

De tussenliggende Data blijft in het geheugen. Deze functies voeren serialisatiewerk uit; het zijn geen zero-copycasts tussen de enum en Foundation-collecties. Voer de conversie uit op de grens waar je desktopfunctie daadwerkelijk een gewone objectgraaf nodig heeft en behoud de enum voor getypeerde modelbewerkingen.

Beide .fragmentsAllowed-opties zijn van belang, omdat een recursieve waarde een scalaire root kan hebben. Een string, Booleaanse waarde, getal of null op het hoogste niveau moet op dezelfde manier worden behandeld als een geneste waarde. Foundation documenteert dit gedrag afzonderlijk voor ReadingOptions.fragmentsAllowed en WritingOptions.fragmentsAllowed.

De omgekeerde functie accepteert Any, omdat dat het invoertype van de Foundation-API is, maar het ondersteunde domein blijft het contract hierboven. Conversiefouten worden doorgegeven aan de aanroeper. Vervang een mislukte conversie niet door een leeg object en verwijder geen elementen die niet kunnen worden geconverteerd; daarmee zou het model ongemerkt veranderen.

Welke controles tonen compatibiliteit aan?

Een roundtrip op zichzelf kan het oorspronkelijke probleem missen: met synthese gecodeerde enums kunnen hun eigen gewrapte structuur succesvol heen en terug omzetten. Controleer zowel de gelijkheid van de enum als de gewone vorm. De volgende functie bouwt een onafhankelijke Foundation-graaf op, controleert het verwachte enummodel, controleert de daadwerkelijke rootsleutels van de uitvoer en test elke categorie op rootniveau.

func runExample() throws {
    let plain: [String: Any] = [
        "title": "Tile preview",
        "enabled": true,
        "spacing": 8.5,
        "subtitle": NSNull(),
        "tiles": [
            ["label": "Circle", "size": 24.0],
            ["label": "Square", "size": 32.0]
        ]
    ]

    let expected: PlainValue = .object([
        "title": .string("Tile preview"),
        "enabled": .bool(true),
        "spacing": .number(8.5),
        "subtitle": .null,
        "tiles": .array([
            .object([
                "label": .string("Circle"),
                "size": .number(24)
            ]),
            .object([
                "label": .string("Square"),
                "size": .number(32)
            ])
        ])
    ])

    let model = try plainValue(from: plain)
    precondition(model == expected)

    let output = try foundationObject(from: model)
    guard let object = output as? [String: Any] else {
        preconditionFailure("Expected a plain object root.")
    }
    precondition(Set(object.keys) == Set(plain.keys))
    precondition(object["title"] as? String == "Tile preview")
    precondition(object["subtitle"] is NSNull)
    precondition((object["tiles"] as? [Any])?.count == 2)

    let restored = try plainValue(from: output)
    precondition(restored == expected)

    let roots: [PlainValue] = [
        .null, .bool(false), .bool(true),
        .number(0), .number(1), .number(8.5),
        .string("1"), .array([]), .object([:]),
        expected
    ]
    for root in roots {
        let graph = try foundationObject(from: root)
        let decoded = try plainValue(from: graph)
        precondition(decoded == root)
    }

    do {
        _ = try foundationObject(from: .number(.infinity))
        preconditionFailure("Expected nonfinite number rejection.")
    } catch EncodingError.invalidValue(_, _) {
        // Expected: this model requires finite numbers.
    }

    print("All compatibility checks passed.")
}

try runExample()

Het verwachte succesbericht is All compatibility checks passed. De controle van de rootsleutels signaleert een toegevoegde buitenste case-wrapper. Het onafhankelijke verwachte model signaleert een verkeerde caseselectie, waaronder Booleaanse waarde versus getal en tekst die op een getal lijkt versus een getal. De null-controle bevestigt dat subtitle aanwezig blijft, terwijl de rootcontroles scalaire fragments testen en lege arrays onderscheiden van lege objecten.

Pas deze diagnostische volgorde voor een bestaand desktopmodel aan de synthetische waarden ervan aan: controleer eerst de verwachte enum, inspecteer daarna de gewone vorm, reconstrueer vervolgens de enum en test tot slot de grensgevallen. Geef tussenresultaten namen, zodat een mislukte controle laat zien bij welke conversie de waarde is veranderd.

Wat zijn de beperkingen van deze aanpak?

Het numerieke contract is de voornaamste beperking. Eén Double-case kan niet garanderen dat elke numerieke Foundation-invoer exact behouden blijft. Als exacte conversie van integers van belang is, controleer dan of de waarden representeerbaar zijn voordat ze dit model ingaan of herontwerp de numerieke cases en hun decoderingregels. Succesvolle serialisatie bewijst niet dat getallen exact behouden zijn. Apple’s Double documentation maakt onderscheid tussen afgeronde en exacte conversies.

Het voorbeeld wijst niet-eindige getallen ook expliciet af in beide coderingsrichtingen. Zo blijft de grens van de enum in overeenstemming met de gedocumenteerde beperkingen van de gewone Foundation-representatie. Een recursieve structuur betekent geen onbeperkte nestingsdiepte, dus houd synthetische modelbomen redelijk klein en geef conversiefouten door aan de aanroepende functie.

Voor de lay-outpreviewtaak is het eindresultaat een getypeerde recursieve enum waarvan de aangepaste codering gewone strings, Booleaanse waarden, eindige getallen, null-waarden, arrays en objecten met string-sleutels behoudt. Gebruik vormcontroles naast roundtripcontroles wanneer je de cases of conversielogica wijzigt; samen maken ze de bedoelde representatie concreet en controleerbaar.

Bronnen