I've been playing around with SwiftUI a bit and have been writing a small meal planner/todo list style app.
I was able to get Realm working with SwiftUI and wrote a small wrapper object to get Realm change notifications to update the UI.
This works great for adding items and the UI gets properly updated. However, when deleting an item using swipe to delete or other methods, I get an index out of bounds error from Realm.
Here's some code:
ContentView:
struct ContentView : View {
@EnvironmentObject var userData: MealObject
@State var draftName: String = ""
@State var isEditing: Bool = false
@State var isTyping: Bool = false
var body: some View {
List {
HStack {
TextField($draftName, placeholder: Text("Add meal..."), onEditingChanged: { editing in
self.isTyping = editing
},
onCommit: {
self.createMeal()
})
if isTyping {
Button(action: { self.createMeal() }) {
Text("Add")
}
}
}
ForEach(self.userData.meals) { meal in
NavigationLink(destination: DetailMealView(ingredientsObject: IngredientsObject(meal: meal))) {
MealRow(name: meal.name)
}
}.onDelete(perform: delete)
}
.navigationBarTitle(Text("Meals"))
}
func delete(at offsets: IndexSet) {
guard let index = offsets.first else {
return
}
let mealToDelete = userData.meals[index]
Meal.delete(meal: mealToDelete)
print("Meals after delete: (self.userData.meals)")
}
}
And the MealObject wrapper class:
final class MealObject: BindableObject {
let willChange = PassthroughSubject<MealObject, Never>()
private var token: NotificationToken!
var meals: Results<Meal>
init() {
self.meals = Meal.all()
lateInit()
}
func lateInit() {
token = meals.observe { changes in
self.willChange.send(self)
}
}
deinit {
token.invalidate()
}
}
I was able to narrow the issue down to
ForEach(self.userData.meals) { meal in
NavigationLink(destination: DetailMealView(ingredientsObject: IngredientsObject(meal: meal))) {
MealRow(name: meal.name)
}
}
It seems like self.userData.meals isn't updating, even though when checking the change notification in MealObject it shows the correct deletions and the meals variable in MealObject correctly updates as well.
*Edit: Also to add, the deletion does actually happen and when launching the app again, the deleted item is gone. It seems like SwiftUI gets confused about the state and tries to access the deleted item after willChange gets called.
*Edit 2: Found one workaround for now, I implemented a method checking whether the object currently exists in Realm:
static func objectExists(id: String, in realm: Realm = try! Realm()) -> Bool {
return realm.object(ofType: Meal.self, forPrimaryKey: id) != nil
}
Called like this
ForEach(self.userData.meals) { meal in
if Meal.objectExists(id: meal.id) {
NavigationLink(destination: DetailMealView(ingredientsObject: IngredientsObject(meal: meal))) {
MealRow(name: meal.name)
}
}
}.onDelete(perform: delete)
Not very pretty but it gets the job done until I find the real cause for the crash.
See Question&Answers more detail:
os