CoreData Transformable of custom NSObject array

Hi

I have spent a day not figuring out how can I implement the following:

I wrote a class that contains an int and a string

public class GrammarUnit : NSObject, NSSecureCoding {
    public static var supportsSecureCoding: Bool = true
    
    public func encode(with coder: NSCoder) {
        coder.encode(body, forKey: Key.body.rawValue)
        coder.encode(style, forKey: Key.style.rawValue)
    }
    
    public required convenience init?(coder decoder: NSCoder) {
        //let mBody = decoder.decodeObject(forKey: Key.body.rawValue) as! String
        //let mStyle = decoder.decodeInt64(forKey: Key.style.rawValue)
        let mBody = decoder.decodeObject(of: NSString.self, forKey: Key.body.rawValue) as String? ?? ""
        let mStyle = decoder.decodeInteger(forKey: Key.style.rawValue)
        
        self.init(body: mBody, style: Int(mStyle))
    }
    
    
    var body : String
    var style : Int = 0
    
    enum Key: String, CodingKey {
    case body = "body"
    case style = "style"
    }
    
    init(body: String, style: Int) {
        self.body = body
        self.style = style
    }
    

}

Then In coreData manager I created an entity as follow:

From what I have searched, it says that I need to create a subClass of transformer and so I did:

@objc(GrammarUnitValueTransformer)
final class GrammarUnitValueTransformer: NSSecureUnarchiveFromDataTransformer {

    // The name of the transformer. This is what we will use to register the transformer `ValueTransformer.setValueTrandformer(_"forName:)`.
    static let name = NSValueTransformerName(rawValue: String(describing: GrammarUnitValueTransformer.self))

    // Our class `Test` should in the allowed class list. (This is what the unarchiver uses to check for the right class)
    override static var allowedTopLevelClasses: [AnyClass] {
        return [GrammarUnitValueTransformer.self]
    }

    /// Registers the transformer.
    public static func register() {
        let transformer = GrammarUnitValueTransformer()
        ValueTransformer.setValueTransformer(transformer, forName: name)
    }
}

and then I initialized the static register function just before the persistence controller is initialized:

I am testing it by adding an instance:

Button {
                
                let apl = GrammarComponents(context: self.context)
                apl.id = UUID().uuidString
                apl.components = GrammarUnit(body: "apple", style: 0)
                
                
                do {
                    try context.save()
                } catch {
                    print(error.localizedDescription)
                }
                
            } label: {
                Text("save")
            }

but I am receiving error upon executing the save

You might ask why I just don’t put the String and Int as property of that Entity.
Originally, the field was an array, as I need the pair in sequence
image

I was just testing perhaps that the valueTransformer I created was for single instance
I also make sure I regenerated the CoreData class entity definition before testing

I tried also the solution as suggested here:

I change it to:

public class GrammarUnits: NSObject, NSSecureCoding {
    public static var supportsSecureCoding: Bool = true
    
    public var grammarUnits : [GrammarUnit] = []
    
    enum Key:String {
            case grammarUnits = "grammarUnits"
        }
    
    init(grammarUnits: [GrammarUnit]) {
        self.grammarUnits = grammarUnits
    }
    
    public func encode(with coder: NSCoder) {
        coder.encode(grammarUnits, forKey: Key.grammarUnits.rawValue)
    }
    
    public required convenience init?(coder decoder: NSCoder) {
        let untis = decoder.decodeObject(of: NSArray.self, forKey: Key.grammarUnits.rawValue) as! [GrammarUnit]
        self.init(grammarUnits: untis)
    }
    
}

public class GrammarUnit : NSObject, NSCoding {
    
    public var body : String
    public var style : Int = 0
    
    enum Key: String {
    case body = "body"
    case style = "style"
    }
    
    init(body: String, style: Int) {
        self.body = body
        self.style = style
    }
    
    public func encode(with coder: NSCoder) {
        coder.encode(body, forKey: Key.body.rawValue)
        coder.encode(style, forKey: Key.style.rawValue)
    }
    
    public required convenience init?(coder decoder: NSCoder) {
        let mBody = decoder.decodeObject(forKey: Key.body.rawValue) as! String
        let mStyle = decoder.decodeInteger(forKey: Key.style.rawValue)
        self.init(body: mBody, style: Int(mStyle))
    }
    
}

Then for the valueTransformer, I tried both:

        return [NSArray.self]
    }
override static var allowedTopLevelClasses: [AnyClass] {
        return [GrammarUnits]
    }

then again I tested, but still I encountering same errors

Please help, Im kinda stuck

Thanks in advance

Hi,

just an update regarding this issue:
For those who may encounter this problem, after days of digging, I was able to solve it using this resource:

In some of the solution offered in the net, it says you have to execute static register method of the transformer class before initializing the persistence controller but it doest specifically tell where to put it.

In the article above, I found out that It was registered in the AppDeligate didFinishLaunchingWithOptions of the application

At first it did not work, but I found out that the override does not trigger so I studied how to create an app delegate. Then I tried again, and finally can add item to my entity without an error.

I think this issue is solved.

@mj1240

Funny you should post this just now. I have been taking a look at this project of yours and I read that very same article last night. What I did on your version was to update the “Transformer” field in your Recipe Data Model.xcdatamodeld for both directions and highlights with NSSecureUnarchiveFromDataTransformer

Project runs fine.

Hi Chris,

What makes it different from the previous project is,
instead of using [String] as target of transformed class, I used an NSObject custom based class as target:

here is the actual code:

//
//  DevelopmentView.swift
//  JapaneseCompanion
//
//  Created by Michael Javier on 11/25/22.
//

import SwiftUI
import CoreData

enum Locations : String, Equatable, CaseIterable, NSCodableEnum {
    
    func int() -> String {
        return self.rawValue
    }
    
    init(defaultValue: Any) {
        self = .NotSet
    }
    
    case NotSet,
        Office,
        Gym,
        Church,
        School,
        Home,
        Kitchen,
        Hotel,
        Nature,
        Terminal,
        Hospital,
        Market,
        Restaurant,
        Other
    
    static var asArray: [Locations] {return self.allCases}
    
    func asInt() -> Int {
        return Locations.asArray.firstIndex(of: self)!
    }
}

enum Expressions : String, CaseIterable, NSCodableEnum, Codable {
    func int() -> String {
        return self.rawValue
    }
    
    init(defaultValue: Any) {
        self = .unclassified
    }
    case unclassified,
         comparison_contrast,
         suggestion_options,
         possibility_chance,
         forecast_guess,
         hearsay_reference,
         reason,
         conclude,
         warning,
         frustration,
         disappointment,
         compliment,
         expectation_hopes_dreams,
         instruction,
         favor_help,
         direction,
         verification_confirmation,
         report,
         excuse,
         worry_problem_fear,
         changes,
         nostalgia,
         agreement,
         encourage_support,
         objection_disbelief,
         mathematical_numbers,
         other
    
    static var asArray: [Expressions] {return self.allCases}
    
    func asInt() -> Int {
        return Expressions.asArray.firstIndex(of: self)!
    }
}

public class SJParentValueTransformer<T: NSCoding & NSObject>: ValueTransformer {

    public override class func transformedValueClass() ->
        AnyClass{T.self }
    public override class func allowsReverseTransformation() ->
        Bool { true }

    public override func transformedValue(_ value: Any?) -> Any? {
        guard let value = value as? T else { return nil }
        return try?
         NSKeyedArchiver.archivedData(withRootObject: value,
         requiringSecureCoding: true)
    }

    public override func reverseTransformedValue(_ value: Any?) ->
    Any? {
         guard let data = value as? NSData else { return nil }
         let result = try? NSKeyedUnarchiver.unarchivedObject(
             ofClass: T.self,
             from: data as Data
         )
         return result
    }

    public static var transformerName: NSValueTransformerName {
        let className = "\(T.self.classForCoder())"

        return NSValueTransformerName("\(className)Transformer")

    }

    public static func registerTransformer() {
        let transformer = SJParentValueTransformer<T>()
        ValueTransformer.setValueTransformer(transformer, forName:
        transformerName)
     }

}

public class TrialObj : NSObject, Codable, NSSecureCoding  {
    public static var supportsSecureCoding: Bool = true
    
    public func encode(with coder: NSCoder) {
        coder.encode(id, forKey:  "id")
        coder.encode(place, forKey: "place")
        coder.encode(expressions, forKey: "expressions")
    }
    
    public required init?(coder: NSCoder) {
        self.id = UUID()
        self.place = .Church
        self.expressions = [Expressions.direction, Expressions.compliment]
        //name = coder.decodeObject(of: NSString.self, forKey: "name") as String? ?? ""
        //last_name = coder.decodeObject(of: NSString.self, forKey: "last_name") as String? ?? ""
    }
    
    
    var id = UUID()
    var place : Locations
    var expressions : [Expressions]
    
    enum CodingKeys: CodingKey {
            case id
            case place
            case expressions
    }
    
    init(p : Locations, exp : [Expressions]){
        self.place = p
        self.expressions = exp
        self.id = UUID()
    }
    
    required convenience public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        let sPlace = try container.decode(Locations.RawValue.self, forKey: .place)
        let mPlace = Locations(rawValue: sPlace) ?? Locations.NotSet
        
        let mExpressions = try container.decode([Expressions].self, forKey: .expressions)
        
        self.init(p: mPlace, exp: mExpressions)
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.id, forKey: .id)
        try container.encode(self.place.rawValue, forKey: .place)
        try container.encode(self.expressions, forKey: .expressions)
        
    }
    
    
}

struct DevelopmentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    
    var body: some View {
        VStack {
            Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
            
            Button {
                
                let obj = TrialObj(p: .Gym, exp: [.unclassified, .direction])
                let encoder = JSONEncoder()
                encoder.outputFormatting = .prettyPrinted
                let dataObj = try! encoder.encode(obj)
                let strObj = String(data: dataObj, encoding: .utf8)!
                
                print(strObj)
                
                // from JSON string back to NSObject
                let decoder = JSONDecoder()
                let TrObjData = Data(strObj.utf8)
                do {

                    let dataRevers = try decoder.decode(TrialObj.self, from: TrObjData)
                    print(dataRevers)
                } catch {
                    print(error)
                }
                
            } label: {
                Text("Try")
            }
            
            Button {
                let neItem = Sample(context: viewContext)
                neItem.name = "sample records 3"
                 let g = TrialObj(p: Locations.Home, exp: [.changes, .frustration])
                
                neItem.obj = g
                
                do {
                    
                    try viewContext.save()
                }
                catch {
                    print(error.localizedDescription)
                }
                
            } label: {
                
                Text("Create Record")
                
            }
            
            Button {
                
                let request: NSFetchRequest<Sample> = Sample.fetchRequest()
                
                do {
                    let outpu : [Sample] = try viewContext.fetch(request)
                    
                    print(outpu[0].name ?? "cannot get")
                    
                    if let o = outpu[0].obj {
                        print("got")
                    } else {
                        print("nope")
                    }
                            
                }
                catch {
                    print(error.localizedDescription)
                    
                }
                
                
            } label: {
                Text("Fetch Try")
            }


            
        }
    }
}

struct DevelopmentView_Previews: PreviewProvider {
    static var previews: some View {
        DevelopmentView()
    }
}

protocol NSCodableEnum {
    func int() -> String;
    init?(rawValue:String);
    init(defaultValue:Any)
}

extension NSCoder {
    func encodeEnum(e:Locations, forKey:String) {
        self.encode(e.int(), forKey: forKey)
        
        func decodeEnum(forKey:String) -> Locations {
            let strVal : String = self.decodeObject(forKey: forKey) as! String
            if let t = Locations(rawValue: strVal)
            {
                return t
            } else {
                return Locations.NotSet
            }
            
        }
    
    }
    
    func encodeEnum(e:Expressions, forKey:String) {
        self.encode( e.int(), forKey: forKey)
        
        func decodeEnum(forKey:String) -> Expressions {
            let strVal : String = self.decodeObject(forKey: forKey) as! String
            if let t = Expressions(rawValue: strVal)
            {
                return t
            } else {
                return Expressions.unclassified
            }
            
        }
    
    }
    
    func encodeEnum(e: [Expressions], forKey:String) {
        
        var arrayEncoded = "["
        e.forEach { ex in
            arrayEncoded += "\"\(ex.int())\","
        }
        if arrayEncoded.count > 1 {
            let _ = arrayEncoded.removeLast()
        }
        arrayEncoded += "]"
        
        self.encode( arrayEncoded, forKey: forKey)
        
        func decodeEnum(forKey:String) -> [Expressions] {
            
            var ret : [Expressions] = []
            forKey.forEach { key in
                let strVal : String = self.decodeObject(forKey: forKey) as! String
                if let t = Expressions(rawValue: strVal)
                {
                    ret.append(t)
                }
            }
            return ret
        }
        
    }
    

    
}

then in the app I added a delegate:

//
//  JapaneseCompanionApp.swift
//  JapaneseCompanion
//
//  Created by Michael Javier on 11/11/22.
//

import SwiftUI

@main
struct JapaneseCompanionApp: App {
    
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            DevelopmentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}


class AppDelegate: NSObject, UIApplicationDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        print("finished launching")
        
        SJParentValueTransformer<TrialObj>.registerTransformer()
        
        return true
    }
    
    
}

by applying the solution as suggested in the link I posted above, I can now add without runtime error. upon fetch, I can read that it can added instances of the entity using the stab button I created above. I can read the field “name” correctly as well…

but there is still problem with that, when I read the value of property “obj” I am getting a nil
so that is what I am investigating now.

Although I think its a different issue so I marked this issue as solved, that is if I you will agree.

You posted the RecipeApp project so I thought that the issue was with that but you are talking about a different project. I’m not quite sure why you linked the RecipeList project so I’m a little confused.

Hi Chris,

Im sorry for making you confused, as I am confused too how you think I posted here a question related to recepi app. perhaps I posted long before, but this issue is for different project and doesnt have any link to that Recepi App question.

I checked again, but no matter how I double check it, this Post is all related to Language App i am doing and the challenge I am facing.

But I really do Appreciate you replies.

Thank You

Hi Chris,

I think I was just got gas lighted. Can you point out which part I linked it to Recepe App? so I could clear the confusion.

There was a reference in the original post (it has been edited by you since) that referred to the Recipe List App that Chris Ching enhanced with CoreData. That’s the reason I was referring to that App. It’s OK, I’m guessing that you are now on the right track anyways so no problem.

1 Like