Search code examples
arraysswiftdata-structurescore-datastruct

How to delete and update structure type array in Core Data iOS Swift?


How to delete and update structure type array in Core Data iOS Sswift? I am saving to core data like this. I need to delete and update selected cell containing values

let projectsInfo = NSEntityDescription.insertNewObject(forEntityName:"ItemsInfo", into: delegate.persistentContainer.viewContext) as! ItemsInfo
let auditArray:[String:[lendingData]] = ["allcreditData":SaveWitnessData.shared.LendingDataArray]
let jsonData = try! JSONEncoder().encode(auditArray) projectsInfo.values = jsonData
delegate.saveContext()

And my struct is like

struct lendingData : Codable {
    let userName : String
    let amount : String
    let date : String
    let type : String
    var witnessDetails : [witnessData]
}

Solution

  • Option 1.

    Using a class and NSSecureCoding is the best way of doing this. The most flexible.

    ///To See the whole thing in action you have to follow a few steps
    ///Step 1. Create an new SwiftUI project with CoreData
    ///Step 2. Copy all the code in Option 1 into a `.swift` file
    ///Step 3. Go to the `Persistence.swift` file
    ///         Place these 2 lines
    ///            `WitnessDataTransformer.register()`
    ///            `LendingDataTransformer.register()`
    ///         Just under `container = NSPersistentCloudKitContainer(name: "YourAppName")
    ///Step 4. Go to the CoreData model
    ///         Select the `Item` Entity
    ///         Add a `lendingData` attribute of type `Transformable`
    ///         Update the `Transformer` and `Custom Class` in the `Data Model Inspector` as shown
    ///Step 5. You should see the View on Canvas in this point
    

    Photo for Step 4

    enter image description here

    Code

    import SwiftUI
    //struct and class should start with an uppercase
    //You need secureCoding not codable
    //You have to change to class because NSSecurecoding is not available for a struct -https://developer.apple.com/documentation/foundation/nssecurecoding
    public class LendingData : NSObject, Identifiable, ObservableObject{
        public let id: String
        @Published var userName : String
        @Published var amount : String
        @Published var date : String
        @Published var type : String
        //WitnessData needs to conform to secure coding as well
        @Published var witnessDetails : [WitnessData]
    
        static func sample() -> LendingData {
            LendingData(id: UUID().uuidString, userName: "sample name", amount: "10.00", date: "\(Date())", type: "sample type", witnessDetails: [WitnessData.sample(), WitnessData.sample()])
        }
        static func blank() -> LendingData {
            LendingData(id: UUID().uuidString, userName: "", amount: "", date: "", type: "", witnessDetails: [])
        }
        public enum CodingKeys: String, CodingKey {
            case id
            case userName
            case amount
            case date
            case type
            case witnessDetails
        }
        public init(id: String, userName : String, amount : String, date : String, type : String, witnessDetails : [WitnessData]) {
            self.id = id
            self.userName = userName
            self.amount = amount
            self.date = date
            self.type = type
            self.witnessDetails = witnessDetails
        }
        public required init?(coder: NSCoder) {
            id = coder.decodeObject(forKey: CodingKeys.id.rawValue) as! String
            userName = coder.decodeObject(forKey: CodingKeys.userName.rawValue) as! String
            amount = coder.decodeObject(forKey: CodingKeys.amount.rawValue) as! String
            date = coder.decodeObject(forKey: CodingKeys.date.rawValue) as! String
            type = coder.decodeObject(forKey: CodingKeys.type.rawValue) as! String
            witnessDetails = coder.decodeArrayOfObjects(ofClass: WitnessData.self, forKey: CodingKeys.witnessDetails.rawValue)  ?? []
        }
    }
    extension LendingData: NSSecureCoding{
        public static var supportsSecureCoding: Bool{
            return true
        }
        public func encode(with coder: NSCoder) {
            coder.encode(id, forKey: CodingKeys.id.rawValue)
            coder.encode(userName, forKey: CodingKeys.userName.rawValue)
            coder.encode(amount, forKey: CodingKeys.amount.rawValue)
            coder.encode(date, forKey: CodingKeys.date.rawValue)
            coder.encode(type, forKey: CodingKeys.type.rawValue)
            coder.encode(witnessDetails, forKey: CodingKeys.witnessDetails.rawValue)
        }
    }
    ///MUST CALL LendingDataTransformer.register() right after creating the Persistent Container before setup and loading store
    @objc(LendingDataTransformer)
    public final class LendingDataTransformer: NSSecureUnarchiveFromDataTransformer {
        public static let name = NSValueTransformerName(rawValue: String(describing: LendingDataTransformer.self))
        public override static var allowedTopLevelClasses: [AnyClass] {
            return [LendingData.self, NSString.self, NSArray.self, WitnessData.self]
        }
    
        //Register before CoreData setup starts
        @objc dynamic
        public static func register() {
            let transformer = LendingDataTransformer()
            ValueTransformer.setValueTransformer(transformer, forName: name)
        }
    }
    //You have to change to class because NSSecurecoding is not available for a struct -https://developer.apple.com/documentation/foundation/nssecurecoding
    public class WitnessData: NSObject, Identifiable, ObservableObject{
        public let id: String
        //This is just a sample since you did not provide the struct
        //Add your variables to
        //    the class,
        //    the CodingKeys,
        //    init?(coder: NSCoder),
        //    encode(with coder: NSCoder), and
        //    init(id: String, name : String).
        //  Just follow the pattern.
        @Published var name: String
    
        static func sample() -> WitnessData{
            WitnessData(id: UUID().uuidString, name: UUID().uuidString)
        }
        static func blank() -> WitnessData{
            WitnessData(id: UUID().uuidString, name: "")
        }
        public enum CodingKeys: String, CodingKey {
            case id
            case name
        }
        public init(id: String, name : String) {
            self.id = id
            self.name = name
    
        }
        public required init?(coder: NSCoder) {
            id = coder.decodeObject(forKey: CodingKeys.id.rawValue) as? String ?? ""
            name = coder.decodeObject(forKey: CodingKeys.name.rawValue) as? String ?? ""
        }
    }
    extension WitnessData: NSSecureCoding{
        public static var supportsSecureCoding: Bool{
            return true
        }
        public func encode(with coder: NSCoder) {
            coder.encode(id, forKey: CodingKeys.id.rawValue)
            coder.encode(name, forKey: CodingKeys.name.rawValue)
        }
    }
    ///MUST CALL WitnessDataTransformer.register() right after creating the Persistent Container before setup and loading store
    @objc(WitnessDataTransformer)
    public final class WitnessDataTransformer: NSSecureUnarchiveFromDataTransformer {
        public static let name = NSValueTransformerName(rawValue: String(describing: WitnessDataTransformer.self))
        public override static var allowedTopLevelClasses: [AnyClass] {
            return [WitnessData.self, NSString.self, NSArray.self]
        }
        //Register before CoreData setup starts
        @objc dynamic
        public static func register() {
    
            let transformer = WitnessDataTransformer()
            ValueTransformer.setValueTransformer(transformer, forName: name)
        }
    }
    

    The below SwiftUI code works for both option 1 or option 2

    ///This is just a sample View
    struct LendingDataView: View {
        //You will need the original ObservableObject if you want to be able to show changes
        //SwiftUI depends on being told that there are chagnes so it can reload Views
        @ObservedObject var item: Item
        var body: some View {
            if item.lendingData != nil{
                List{
                    TextField("username",text: $item.lendingData.bound.userName)
                    TextField("amount",text: $item.lendingData.bound.amount)
                    TextField("date",text: $item.lendingData.bound.date)
                    TextField("type",text: $item.lendingData.bound.type)
                    Section(content: {
                        ForEach($item.lendingData.bound.witnessDetails, content: { $witness in
                            HStack{
                                TextField("name",text: $witness.name)
                                Spacer()
                                //For deleting by object
                                Image(systemName: "trash")
                                    .foregroundColor(.red)
                                    .onTapGesture {
                                        let idx = item.lendingData!.witnessDetails.firstIndex(where: {
                                            $0.id == witness.id
                                        })
                                        if idx != nil{
                                            item.lendingData!.witnessDetails.remove(at: idx!)
                                        }
                                        //Because you are so far down the line you have to tell the ObservableObject there is a change
                                        //If you dont you won't see the new items until something happens to trigger a refresh
                                        //item.objectWillChange.send()
                                        item.objectWillChange.send()
                                    }
                            }
                        })
                        //For deleting by index
                            .onDelete(perform: { indexSet in
                                for idx in indexSet{
                                    item.lendingData!.witnessDetails.remove(at: idx)
                                }
                            })
                    }, header: {
                        HStack{
                            Text("Witness Data")
                            Button(action: {
                                item.lendingData!.witnessDetails.append(WitnessData.blank())
                                //Because you are so far down the line you have to tell the ObservableObject there is a change
                                //If you dont you won't see the new items until something happens to trigger a refresh
                                item.objectWillChange.send()
                            }, label: {
                                Image(systemName: "plus")
                            })
                        }
                    })
                }
            }else{
                VStack{
                    Text("no lending data")
                    Button(action: {
                        item.lendingData = LendingData.blank()
                    }, label: {
                        Image(systemName: "plus")
                    })
                }
            }
        }
    }
    //Standard Preview
    struct LendingDataView_Previews: PreviewProvider {
        //Use the preview container
        static let context = PersistenceController.preview.container.viewContext
        static var sampleItem = Item(context: context)
        static var previews: some View {
            LendingDataView(item: sampleItem)
        }
    }
    
    extension Optional where Wrapped == LendingData {
        var _bound: LendingData? {
            get {
                return self
            }
            set {
                self = newValue
            }
        }
        var bound: LendingData {
            get {
                return _bound ?? LendingData.blank()
            }
            set {
                _bound = newValue
            }
        }
    }
    

    Like I said at the start class is the safest way but you can use the struct.

    Option 2

    Just add an an attribute named lendingDataJSON of Type String? INSTEAD of the lendingData of type Transformable

    struct LendingData : Codable, Identifiable{
        let id: String
        var userName : String
        var amount : String
        var date : String
        var type : String
        var witnessDetails : [WitnessData]
        
        static func sample() -> LendingData {
            LendingData(id: UUID().uuidString, userName: "sample name", amount: "10.00", date: "\(Date())", type: "sample type", witnessDetails: [WitnessData.sample(), WitnessData.sample()])
        }
        static func blank() -> LendingData {
            LendingData(id: UUID().uuidString, userName: "", amount: "", date: "", type: "", witnessDetails: [])
        }
    }
    struct WitnessData: Codable, Identifiable{
        let id: String
        var name: String
        static func sample() -> WitnessData{
            WitnessData( id: UUID().uuidString, name: UUID().uuidString)
        }
        static func blank() -> WitnessData{
            WitnessData( id: UUID().uuidString, name: "")
        }
    }
    //The App's CoreData Model will need an attibute
    // named lendingDataJSON of Type String
    extension Item{
        //This computed property should be the only way that the app alters the LendingData
        //If you use the lendingDataJSON directly you can corrupt all of it
        var lendingData: LendingData?{
            get{
                let decoder = JSONDecoder()
                if let obj = try? decoder.decode(LendingData.self, from: self.lendingDataJSON?.data(using: .utf8) ?? Data()) {
                    return obj
                }else{
                    return nil
                }
            }
            set{
                let encoder = JSONEncoder()
                encoder.outputFormatting = .prettyPrinted
                if let encoded = try? encoder.encode(newValue) {
                    self.lendingDataJSON = String(data: encoded, encoding: .utf8) ?? ""
                }
            }
        }
    }
    

    All the View code will work the same with the class option or with the struct option