Home > Enterprise >  SwiftUI: ViewModel making app crash when loading a view
SwiftUI: ViewModel making app crash when loading a view

Time:02-22

I want to rename an item in a ForEach list. When i try to load the EditListView for a selected list the entire app crashes.

This is a SwiftUI macOS app and the items are saved using CoreData.

enter image description here

The crash happens as soon as you click on "Edit List" for any of the list items.

enter image description here

It doesn't crash if i remove this view model var listVM: MyListViewModel from the EditListViewModel.

Here's the EditListView

struct EditListView: View {
    let name: String
    @Binding var isVisible: Bool
    var list: MyListViewModel
    @ObservedObject var editListVM: EditListViewModel
   
    init(name: String,list: MyListViewModel, isVisible: Binding<Bool> ) {
        self.list = list
        editListVM = EditListViewModel(listVM: list)
        _isVisible = isVisible
        self.name = name
    }
    
    var body: some View {
        VStack {
            Text(name)
            Button(action: {
            editListItemVM.update()
            }) {
                Text("Update List Name")
            }
            
            Button(action: {
              self.isVisible = false
            }) {
                Text("Cancel")
            }
            }......
      

EditListViewModel

class EditListViewModel: ObservableObject {
    
    var listVM: MyListViewModel
    @Published var name: String = ""
 
    
    init(listVM: MyListViewModel) {
        self.listVM = listVM
        name = listVM.name

        } 
        func update(){
          ....} 

  
}

MyListViewModel

struct MyListViewModel: Identifiable {
    
    private let myList: MyList
    
    init(myList: MyList) {
        self.myList = myList
    }
    
    var id: NSManagedObjectID {
        myList.objectID
    }
    
    var name: String {
        myList.name ?? ""
    }     
}

MyList Model

@objc(MyList)
public class MyList: NSManagedObject, BaseModel {
    
    static var all: NSFetchRequest<MyList> {
        let request: NSFetchRequest<MyList> = MyList.fetchRequest()
        request.sortDescriptors = []
        return request
    } 
    
}

extension MyList {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<MyList> {
        return NSFetchRequest<MyList>(entityName: "MyList")
    }

    @NSManaged public var name: String?


    }

extension MyList : Identifiable {

}

Here's the Main View

struct MyListsView: View {
    
    @StateObject var vm: MyListsViewModel      
    @State private var showPopover: Bool = false
    
    init(vm: MyListsViewModel) {
        _vm = StateObject(wrappedValue: vm)
    }             
            List {
                Text("My Lists")
                ForEach(vm.myLists) { myList in
                    
                    NavigationLink {
                        MyListItemsHeaderView(name: myList.name)
                                                  
                            .sheet(isPresented: $showPopover) {
                                EditListView(name: myList.name, list: MyListViewModel(myList: MyList()), isVisible: $showPopover)
                                   }
                    }                                                                       
                    }.contextMenu {
                        Button {
                            showPopover = true
                            // Show the EditListView
                     
                        } label: {
                            Label("Edit List", systemImage: "pen.circle")
                            
                        }...... 

CodePudding user response:

First get rid of your view model objects we don't use those in SwiftUI. We use the View struct and the property wrappers like @FetchRequest make the struct behave like an object. It looks like this:

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>

    var body: some View {
        NavigationView {
            List {
                ForEach(items) { item in
                    ItemView(item: item)

I recommend looking at Xcode's app template with core data checked.

Then for editing you can use .sheet(item: like this:

struct ItemEditor: View {
    @ObservedObject var item: Item // this is the scratch pad item
    @Environment(\.managedObjectContext) private var context
    @Environment(\.dismiss) private var dismiss // causes body to run
    let onSave: () -> Void
    @State var errorMessage: String?
    
    var body: some View {
        NavigationView {
            Form {
                Text(item.timestamp!, formatter: itemFormatter)
                if let errorMessage = errorMessage {
                    Text(errorMessage)
                }
                Button("Update Time") {
                    item.timestamp = Date()
                }
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Save") {
                        // first save the scratch pad context then call the handler which will save the view context.
                        do {
                            try context.save()
                            errorMessage = nil
                            onSave()
                        } catch {
                            let nsError = error as NSError
                            errorMessage  = "Unresolved error \(nsError), \(nsError.userInfo)"
                        }
                    }
                }
            }
        }
    }
}

struct ItemEditorConfig: Identifiable {
    let id = UUID()
    let context: NSManagedObjectContext
    let item: Item
    
    init(viewContext: NSManagedObjectContext, objectID: NSManagedObjectID) {
        // create the scratch pad context
        context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        context.parent = viewContext
        // load the item into the scratch pad
        item = context.object(with: objectID) as! Item
    }
}

struct EditItemButton: View {
    let itemObjectID: NSManagedObjectID
    @Environment(\.managedObjectContext) private var viewContext
    @State var itemEditorConfig: ItemEditorConfig?
    
    var body: some View {
        Button(action: edit) {
            Text("Edit")
        }
        .sheet(item: $itemEditorConfig, onDismiss: didDismiss) { config in
            ItemEditor(item: config.item) {
                do {
                    try viewContext.save()
                } catch {
                    // Replace this implementation with code to handle the error appropriately.
                    // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                    let nsError = error as NSError
                    fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
                }
                itemEditorConfig = nil // dismiss the sheet
            }
            .environment(\.managedObjectContext, config.context)
        }
    }
    
    func edit() {
        itemEditorConfig = ItemEditorConfig(viewContext: viewContext, objectID: itemObjectID)
    }
    
    func didDismiss() {
        // Handle the dismissing action.
    }
}

struct ItemView: View {
    @ObservedObject var item: Item
    var body: some View {
        Text("Item at \(item.timestamp!, formatter: itemFormatter)")
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                EditItemButton(itemObjectID: item.objectID)
            }
        }
    }
}

CodePudding user response:

the params for EditListView in the main view were incorrect.

Fixed it with the following params:

  .sheet(isPresented: $showPopover) {
                                EditListView(name: myList.name, list: myList, isVisible: $showPopover)
                                   }
  • Related