ValueTransformer currently crashes XCode SwiftUI preview

I have a working ValueTransformer that runs fine in simulator/device, but crashes in SwiftUI Preview. Even though they are the same code.

Here is my code


import Foundation

final class StringBoolDictTransformer: ValueTransformer {
    override func transformedValue(_ value: Any?) -> Any? {
        guard let stringBoolDict = value as? [String: Bool] else { return nil }
        let nsDict = NSMutableDictionary()
        for (key, bool) in stringBoolDict {
            nsDict[key] = NSNumber(value: bool)
        }
        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: nsDict, requiringSecureCoding: true)
            return data
        } catch {
            debugPrint("Unable to convert [Date: Bool] to a persistable form: \(error.localizedDescription)")
            return nil
        }
    }

    override func reverseTransformedValue(_ value: Any?) -> Any? {
        guard let data = value as? Data else { return nil }
        do {
            guard let nsDict = try NSKeyedUnarchiver.unarchivedDictionary(ofKeyClass: NSString.self, objectClass: NSNumber.self, from: data) else {
                return nil
            }
            var result = [String: Bool]()
            for (key, value) in nsDict {
                result[key as String] = value.boolValue
            }
            return result
        } catch {
            debugPrint("Unable to convert persisted Data to [Date: Bool]: \(error.localizedDescription)")
            return nil
        }
    }

    override class func allowsReverseTransformation() -> Bool {
        true
    }

    override class func transformedValueClass() -> AnyClass {
        NSDictionary.self
    }
}

and here is the container

public struct SwiftDataManager {
    public static let shared = SwiftDataManager()

    public var sharedModelContainer: ModelContainer

    init() {
        ValueTransformer.setValueTransformer(
            StringBoolDictTransformer(), forName: NSValueTransformerName("StringBoolDictTransformer")
        )
        let schema = Schema([,
            Plan.self
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            sharedModelContainer = try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }
}

and the model

@Model
final class Plan {
    @Attribute(.transformable(by: StringBoolDictTransformer.self))
    var dict: [String: Bool] = [:]
}

I would get that container and pass it in appdelegate and it works fine. I would get that container and pass it inside a #Preview and it would crash with the following:

Runtime: iOS 17.5 (21F79) - DeviceType: iPhone 15 Pro
        CoreFoundation:
            *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unacceptable type of value for attribute: property = "dict"; desired type = NSDictionary; given type = _NSInlineData; value = {length = 2, bytes = 0x7b7d}.'
        libsystem_c.dylib:
            abort() called

Version 16.0 (16A242d)

Answered by DTS Engineer in 810490022

If the intent is really to persist a [String: Bool] dictionary, you don't need to use transformable attribute. The following code should just work (on both iOS 17 and 18) because SwiftData supports (most) Codable types:

@Model
final class Plan {
    var dict: [String: Bool] = [:]
    ...
}

If your code snippet is to demonstrate that using SwiftData transformable attributes triggers a crash on iOS 17, I am pretty sure that is a bug, which has been fixed on iOS 18. I don't see any workaround unfortunately – To support iOS 17, you might consider avoiding using transformable attributes.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

After some more investigation turns out this code crashes in preview because my Previews were ran in iOS17.

So I guess the question remains, why does this code works in iOS18 but not in iOS17 even though it doesn't use any iOS18 specific APIs?

Accepted Answer

If the intent is really to persist a [String: Bool] dictionary, you don't need to use transformable attribute. The following code should just work (on both iOS 17 and 18) because SwiftData supports (most) Codable types:

@Model
final class Plan {
    var dict: [String: Bool] = [:]
    ...
}

If your code snippet is to demonstrate that using SwiftData transformable attributes triggers a crash on iOS 17, I am pretty sure that is a bug, which has been fixed on iOS 18. I don't see any workaround unfortunately – To support iOS 17, you might consider avoiding using transformable attributes.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

ValueTransformer currently crashes XCode SwiftUI preview
 
 
Q