0

In the following code I have a List of Cars and each Car from that list has its own list of Services, I can add and delete Cars without a problem by calling carViewModel.addNewCar(make:String, model:String) and carViewModel.deleteCar(at indexSet:IndexSet).

Car.swift

    import RealmSwift
    final class Car: Object, ObjectKeyIdentifiable{

        @objc dynamic var make: String = ""
        @objc dynamic var model: String = ""
        // creation date, ID etc.
        dynamic var services = List<CarService>()
    }

CarList.swift

    import RealmSwift

    final class CarList: Object, ObjectKeyIdentifiable{
        
        @objc dynamic var name: String = ""
        // creation date, ID etc.
        var cars = RealmSwift.List<Car>()
    }

CarService.swift

    import RealmSwift

    final class CarService: Object, ObjectKeyIdentifiable{

        @objc dynamic var serviceName: String = ""
        // creation date, ID etc.
    }

View Model

    import RealmSwift

    class CarViewModel: ObservableObject{
        @Published var cars = List<Car>()
        @Published var selectedCarList: CarList? = nil

        var token: NotificationToken? = nil
        
        init(){
            // Create a the default lists if they don't already exist.
            createDefaultCarList()
            createDefaultServiceList()
            
            // Initialize the SelectedCarList and the cars variables items from the Default Car List.
            if let list = realm?.objects(CarList.self).first{
                self.selectedCarList = list
                self.cars = list.cars
            }
            
            token = selectedCarList?.observe({ [unowned self] (changes) in
                switch changes{
                case .error(_): break
                case.change(_, _):self.objectWillChange.send()
                case.deleted: self.selectedCarList = nil
                }
            })
        }
        
        func addNewCar(make:String, model:String){
            if let realm = selectedCarList?.realm{
                try? realm.write{
                    let car = Car()
                    car.make = make
                    car.model = model
                    selectedCarList?.cars.append(car)
                }
            }
        }
        
        func deleteCar(at indexSet:IndexSet){
            if let index = indexSet.first,
            let realm = cars[index].realm{
                try? realm.write{
                    realm.delete(cars[index])
                }
            }
        }
        
        func addService(toCar: Car, serviceName: String){
            try? realm?.write{
                let service = CarService()
                service.serviceName = serviceName
                
                toCar.services.append(service)
            }
        }
        
        /// Creates the Default Car List if it doesn't already exists otherwise just prints the error.
        func createDefaultCarList(){
            do {
                if (realm?.objects(CarList.self).first) == nil{
                    try realm?.write({
                        let defaultList = CarList()
                        defaultList.name = "Default Car List"
                        realm?.add(defaultList)
                    })
                }
            }catch let error{
                print(error.localizedDescription)
            }
        }
        
        /// Creates the Default Serivice List if it doesn't already exists otherwise just prints the error.
        func createDefaultServiceList(){
            do {
                if (realm?.objects(ServiceList.self).first) == nil{
                    try realm?.write({
                        let defaultList = ServiceList()
                        defaultList.listName = "Default Service List"
                        realm?.add(defaultList)
                    })
                }
            }catch let error{
                print(error.localizedDescription)
            }
        }
    }

My issue is adding or deleting Services to existing Cars. When I call carViewModel.addService(toCar: Car, serviceName: String) I get the error below...

Calling the addService() method.

    struct NewServiceFormView: View {
        @ObservedObject var carViewModel: CarViewModel
        @State var selectedCar:Car // pass from other cars view
        
        var body: some View {
            NavigationView {
            Form {
                // fields
            }
            .navigationBarItems( trailing:Button("Save", action: addNewCar))
            }
        }

        func addNewCar(){
            carViewModel.addService(toCar: selectedCar, serviceName: "Oil Change")
        }
    }

Error

"Cannot modify managed RLMArray outside of a write transaction."

I can add new Services by explicitly selecting a Car from the cars list. I don't get any errors but the UI doesn't update; I don't see the newly added Service until the app is relaunched.

No errors doing it this way but the UI doesn't update.

carViewModel.addService(toCar: carViewModel.cars[1], serviceName: "Rotors")

How can I properly watch, delete and add Services to existing Cars?

EDIT: Added the following code per Mahan's request.

View to present the NewServiceFormView

    struct CarServicesView: View {
        @State var selectedCar:Car // a car from parent view
        @ObservedObject var carViewModel: CarViewModel
        
        var body: some View {
            VStack{

                List {
                    Section(header: Text("Services: \(selectedCar.services.count)")) {
                        ForEach(selectedCar.services) { service in
                        }
                    }
                }
                .listStyle(GroupedListStyle())
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button(action: openNewServiceForm) {
                            Image(systemName: "plus")
                        }
                    }
                }
            }.sheet(isPresented: $newServiceFormIsPresented){
                NewServiceFormView(carViewModel: carViewModel, selectedCar: selectedCar)
            }
        }
        
        func openNewServiceForm() {
        newServiceFormIsPresented.toggle()
        }
    }
fs_tigre
  • 10,650
  • 13
  • 73
  • 146
  • 1
    Could you include view that you have used `NewServiceFormView` in? – mahan Aug 16 '21 at 13:51
  • Well in the parent view I just presented the `NewServiceFormView ` `.sheet(isPresented: $newServiceFormIsPresented){ NewServiceFormView(carViewModel: carViewModel, selectedCar: selectedCar) }` Thanks. – fs_tigre Aug 16 '21 at 13:55
  • @mahan - See updated code. Thanks. – fs_tigre Aug 16 '21 at 14:02

1 Answers1

1

One issue is how the Realm objects are being observed - remember they are ObjC objects under the hood so you need to use Realm observers. So this

@ObservedObject var carViewModel: CarViewModel

should be this

@ObservedRealmObject var carViewModel: CarViewModel

See the documentation for observedRealmObject

Also, keep in mind if you're observing a Results, the same thing applies, use

@ObservedResults

as shown in the documentation

Jay
  • 34,438
  • 18
  • 52
  • 81
  • Hmm, unfortunately it wasn't as simple as replacing `@ObservedObject` with `@ObservedRealmObject`. I'm currently using the Swift/Combine `@StateObject` and `ObservedObject` so I need to play a little bit more to see how to use the recently added `@ObservedResults` and the `@ObservedRealmObject` wrappers. Thanks! – fs_tigre Aug 16 '21 at 19:53