1

I have a SwiftData @Model that contains a struct which contains an array. Why do I get the following runtime error when I fetch the model and access the struct?

Could not cast value of type '_NSInlineData' (0x11d856fe0) to 'NSArray' (0x1dabccb60).

import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    
    var body: some View {
        Text("SwiftData array in struct decoding issue")
            .onAppear(perform: addItem)
    }
    
    private func addItem() {
        let lead1 = Person(skills: [])
        let project1 = Project(lead: lead1)
        let _ = project1.lead // Runtime error!
    }
}

@Model
final class Project {
    var lead: Person
    
    init(lead: Person) {
        self.lead = lead
    }
}

struct Person: Codable {
    let skills: [String]
}

Related information:

  • It does not matter whether the array is empty or not.
  • It also happens with an Int array.
  • It also happens with a Set.
  • However, an array of a custom struct decodes fine for me and so do dictionaries.
  • If you change let skills to var skills the error becomes "Could not cast value of type '__NSCFData' (0x122862d40) to 'NSArray' (0x1dabcd470)". Be sure to delete the database first.
  • I cannot make Person a @Model because it comes from a library and needs to be Sendable.
  • Tested with macOS 14.0 and Xcode 15.0.
6
  • 1
    Side note, you could even do let _ = project1.lead before the insert and it should crash for the same reason. Commented Oct 12, 2023 at 11:44
  • 1
    I think I'm missing something since I don't use SwiftData, but only CoreData, BUT, if you add init(from decoder:) with let data = try container.decode(Data.self, forKey: .skills); self.skills = try JSONDecoder().decode([String].self, from: data) it works... Strange though... Commented Oct 12, 2023 at 11:56
  • It seems to be at the same time a bug and intended, it's unclear. See hackingwithswift.com/quick-start/swiftdata/… Commented Oct 12, 2023 at 16:01
  • The important message being: "However, there is a catch: if you use collections of value types, e.g. [String], SwiftData will save that directly inside a single property too. Right now it’s encoded as binary property list data, which means you can’t use the contents of your array in a predicate. If you attempt to do so, your app will just crash at runtime. So, please be very careful. Of course, this behavior is an implementation detail of SwiftData, and is subject to change at any point in the future – do try it yourself before coming to a final conclusion." If it's "let skills: String", itok Commented Oct 12, 2023 at 16:02
  • @Larme Comparison in #Predicate is limited, true. Unfortunate but understandable. But why does decoding fail here outside a predicate? I think your workaround shouldn't be needed. Commented Oct 12, 2023 at 16:23

1 Answer 1

1

This definitely seems like a bug, as I have run into this with Apple's own classes that worked with previous versions of data storage. Here is what I did to overcome this issue with swift data. Hopefully they will solve this in a future release.

  1. Add an attribute to the property with transformation information. (Note: there is someone on the apple developer who claims to have gotten this working another simpler way without the "by: ****" portion of the attribute, but I couldn't get that to work.)

  2. Create the transform class. (Looking at this, this really could be generalized even more, such that it could be used for anything that qualifies as Codable, but I haven't put the effort into it, since I only needed it once so far.) <<Again, given how generic this could be I honestly don't understand why it this is required...hence, likely bug>>

  3. Register the transformation class.

Step 1:

@Attribute(.transformable(by: MyXfmr.self)) var list: TroubleSomeStructYouCannotChange

Step 2:

class MyXfmr: ValueTransformer {
    static let name = NSValueTransformerName(rawValue: "MyXfmr")
    private static let encoder = JSONEncoder()
    private static let decoder = JSONDecoder()
    
    override func transformedValue(_ value: Any?) -> Any? {
        guard let fas = value as? TroubleSomeStructYouCannotChange else { return nil }
        
        do {
            return try MyXfmr.encoder.encode(fas)
        } catch {
            return nil
        }
    }
    
    override func reverseTransformedValue(_ value: Any?) -> Any? {
        guard let data = value as? Data else { return nil }
        
        do {
            return try MyXfmr.decoder.decode(TroubleSomeStructYouCannotChange.self, from: data)
        } catch {

            return nil
        }
    }
    
    override public class func transformedValueClass() -> AnyClass {
        return NSData.self
    }
    
    override public class func allowsReverseTransformation() -> Bool {
        return true
    }
    
    static func register() {
        ValueTransformer.setValueTransformer(MyXfmr(), forName: name)
    }
}

Step 3:

In my App Start, I created this function:

private func getModelTypes() -> [any PersistentModel.Type] {
    MyXfmr.register()
    
    return [FirstModel.self, SecondModel.self]
}

and then updated my call to modelContainer to as follows

.modelContainer(for: getModelTypes(), isAutosaveEnabled: true, isUndoEnabled: false)
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.