How to delete data from SwiftUI List and Realm - ios

The following code properly displays all of the 'Users' from Realm database in a SwiftUI List. My issue is deleting records when I swipe a row.
When I swipe a row and tap the delete button, I immediately get an uncaught exception error, the List does not update but I know the right item gets deleted from the Realm database since the next time I run the app the selected record doesn't show up.
Here is my code.
SwiftUI Code
import RealmSwift
struct ContentView: View {
#State private var allUsers: Results<User> = realm.objects(User.self)
var body: some View {
VStack{
Text("Second Tab")
List{
ForEach(allUsers, id:\.self) { user in
HStack{
Text(user.name)
Text("\(user.age)")
}
}.onDelete(perform: deleteRow)
}
}
}
private func deleteRow(with indexSet: IndexSet){
indexSet.forEach ({ index in
try! realm.write {
realm.delete(self.allUsers[index])
}
})
}
}
Realm Model
import RealmSwift
class User:Object{
#objc dynamic var name:String = ""
#objc dynamic var age:Int = 0
#objc dynamic var createdAt = NSDate()
#objc dynamic var userID = UUID().uuidString
override static func primaryKey() -> String? {
return "userID"
}
}
ERROR
Terminating app due to uncaught exception 'RLMException', reason: 'Index 4 is out of bounds (must be less than 4).'
Of course, the 4 changes depending on how many items are in the Realm database, in this case, I had 5 records when I swiped and tapped the delete button.
My expectation was that the List was going to update every time the allUsers #State variable changes, I know my issue is not fully understanding how binding works.
What am I doing wrong?

My expectation was that the List was going to update every time the
allUsers #State variable changes
It is correct, but state was not changed... The following should work
private func deleteRow(with indexSet: IndexSet){
indexSet.forEach ({ index in
try! realm.write {
realm.delete(self.allUsers[index])
}
})
self.allUsers = realm.objects(User.self) // << refetch !!
}
Note: the below is just assigning initial state value
#State private var allUsers: Results<User> = realm.objects(User.self)

This is a very common bug when working with Realm. It happens in ''traditional'' view controllers too.
The only solid solution I found, and it has considerably improved applications stability is to always use interface items instead of realm object.
Moreover, in real life, we need to do some processes, load avatar images from server, add some check boxes, selections or whatever, and we often don't need all properties of objects to be displayed. Somehow, it's a MVVM approach.
By the way, you can call realm.write before the loop, not sure, but I think it's good to minimise context switching.
Using this technique, either with SwiftUI or UIViewControllers, will solve RLM crashes for good.
struct ContentView: View {
struct UserItem: Hashable {
var id: String
var name: String
var age: Int
}
private var allUsers: Results<User> = realm.objects(User.self)
#State private var userItems: [UserItem] = []
func updateItems() {
userItems = allUsers.map {
UserItem(id: $0.userID, name: $0.name, age: $0.age)
}
}
var body: some View {
VStack {
Text("Second Tab")
List{
ForEach(userItems, id:\.self) { user in
HStack{
Text(user.name)
Text("\(user.age)")
}
}
.onDelete(perform: deleteRow)
}
}.onAppear() {
updateItems()
}
}
private func deleteRow(with indexSet: IndexSet){
try! realm.write {
indexSet.forEach {
realm.delete(self.allUsers[$0])
}
}
updateItems()
}
}

Related

SwiftUI List Row Context Menu Label Based on Index Array Property

Searched around and have not found an answer. Believe I know what the issue is, but not sure how to resolve it.
I have a swiftUI list that displays a context menu when a certain type of row is selected(not all rows qualify, this works as it should. When the context menu is displayed, the label is generated by the index of the array populating the lists object property.
The context menu selection performs performs a task that should result in the context menu label changing. And sometimes it works, other times it does not. This is resolved by scrolling that particular row off screen and scrolling back too it (has to be far enough away). The Object array is from a singleton data store passed as an environment object.
I believe this is related to the size of the array and the data being lazy loaded in swiftUI lists. I also would use the List selection property for this, but the context menu being populated by the row does not update the lists selected row.
A snippet example of my code is below.
#EnvironmentObject var singletonStore: MyObjectStore
#State private var selectedRow: Int?
var body: some View {
VStack {
List(singletonStore.myArray.indices, id: \.self, selection: $selectedRow) { index in
LazyVGrid(columns: gridColumns) {
ItemGridView(item: $singletonStore.myArray[index], opacityOffset: getRowOpacity(index: index))
}
.contextMenu {
if singletonStore.myArray[index].thisDate == nil {
if singletonStore.myArray[index].thisNeedsDone > 0 {
Button {
selectedRow = index
//these functions will add or remove a users id or initials to the appropriate property, and this updates values in my list view.
if singletonStore.myArray[index].id != nil {
//do this
} else {
//do that
}
} label: {
Label{
//This is where my issue is - even though the items in the list view are updating, the label of the context menu is not updating until the row is reloaded
Text(singletonStore.myArray[index].initials != nil ? "This Label" : "That Label") } icon: {
Image(systemName: "aqi.medium")
}
}
}
}
} //context menu close
} // list close
.listStyle(PlainListStyle())
}
}
My closures may be off, but its because I modified the code significantly on this platform to make is easier to follow. Nothing removed would affect this issue.
if there was a way to have opening the context menu update the lists selected row, that would solve the issue and I could use the selected row for the index of the singletonStores array objects property, but I may be approaching the problem from thinking the index is incorrect when the actual issue is the context menu is not being reloaded with the environment objects new information. Any help is always appreciated!
EDIT:
After some tinkering I further found that the issue must be related to the context menu itself not refreshing its data. I separated my views and used a #Viewbuilder function to return the needed view for the button - however it still does not refresh the context menus data.
EDIT 2:
currently (and subject to change) my SingletonStore class loads the data from another network class and publishes that data in the form of an array
final class SingletonStore: ObservableObject {
static private(set) var shared = singletonStore()
static func reset() {
shared = StagingStore()
}
#Published var myArray: [CustomObject] = []
private func getMyData() {
//uses other class and methods to retrieve and set data
//works and updates view on refresh
}
}
My View is called from a different View that is just a Tab bar controller, that code looks as follows:
struct ContainerView: View {
#StateObject var singletonStore = SingletonStore.shared
var body: some View {
TabView{
GenericView().environmentObject(singletonStore)
.tabItem {
Label("This View", systemImage: "camera.metering.matrix")
}
}
}
I have created a demo project inspired by your sample code above. In order to reproduce the issue I had to improvise some.
List is binded to a collection, when any of the item change, view hierarchy gets built and changes reflects.
Code for reference is as follow. Notice I am calling a view model method from button action, which makes a change in the collection that is binded.
import Foundation
class ContentViewModel: ObservableObject {
#Published var myArray: [Item] = []
init() {
for i in 0...100 {
let obj = Item(id: UUID().uuidString, thisDate: Date.now, thisNeedsDone: i, initials: "That Label")
myArray.append(obj)
}
}
func updateTheRow(item: Item) {
if let indexOfItem = myArray.firstIndex(where: { obj in
obj.id == item.id
})
{
myArray[indexOfItem] = item
}
}
}
struct Item: Identifiable, Equatable, Hashable {
var id: String
var thisDate: Date?
var thisNeedsDone: Int
var initials: String?
}
import SwiftUI
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
let columns = [
GridItem(.adaptive(minimum: 80))
]
var body: some View {
VStack {
List(viewModel.myArray, id: \.self) { item in
LazyVGrid(columns: columns) {
VStack{
Text("My bad")
}
}
.contextMenu {
if item.thisDate != nil {
if item.thisNeedsDone > 0 {
Button {
//these functions will add or remove a users id or initials to the appropriate property, and this updates values in my list view.
var modifiedItem = item
modifiedItem.initials = "Modified Label"
viewModel.updateTheRow(item: modifiedItem)
} label: {
Label{
//This is where my issue is - even though the items in the list view are updating, the label of the context menu is not updating until the row is reloaded
Text(item.initials!) } icon: {
Image(systemName: "aqi.medium")
}
}
}
}
} //context menu close
} // list close
.listStyle(PlainListStyle())
}
}
}

SwiftUI list references deleted NSManagedObjects

When using #FetchRequest to populate a List, SwiftUI will attempt to access a deleted NSManagedObject even after it has been deleted from the store (for example, via swipe action).
This bug is very common and easy to reproduce: simply create a new Xcode project with the default SwiftUI + CoreData template. Then replace ContentView with this code:
struct ContentView: View {
#Environment(\.managedObjectContext) private var moc
#FetchRequest(sortDescriptors: []) private var items: FetchedResults<Item>
var body: some View {
List {
ForEach(items) { item in
ItemRow(item: item)
}
.onDelete {
$0.map{items[$0]}.forEach(moc.delete)
try! moc.save()
}
}
.toolbar {
Button("Add") {
let newItem = Item(context: moc)
newItem.timestamp = Date()
try! moc.save()
}
}
}
}
struct ItemRow: View {
#ObservedObject var item: Item
var body: some View {
Text("\(item.timestamp!)")
}
}
Add a few items to the list, then swipe to delete a row: the app will crash. An ItemRow is attempting to draw with the now-deleted Item.
A common workaround is to wrap the whole subview in a fault check:
struct ItemRow: View {
#ObservedObject var item: Item
var body: some View {
if !item.isFault {
Text("\(item.timestamp!)")
}
}
}
But this is a poor solution and has side effects (objects can be faults when not deleted).
This answer suggests that wrapping the deletion in viewContext.perform{} would work, but the crash still occurs for me.
Any better solutions/workarounds out there?
This does seem to be the result of the ObservedObject switching to a fault as part of the delete operation, while timestamp! is force unwrapped. The force unwrap works for the full object but not the fault.
An approach which does seem to work is to remove the .onDelete action at the ForEach level, and replace it with a swipeAction at the row level:
ForEach(items) { item in
ItemRow(item: item)
.swipeActions {
Button("Delete", role: .destructive) {
viewContext.delete(item)
try? viewContext.save()
}
}
}
In any respect, it shows that even with a simple NSManagedObject like Item, relying on force unwrapping of attributes comes with risks.

SwiftUI Lists + Firebase Firestore, fetching data and then unfetching? (Bug)

I'm not sure what the problem is with my project. Basically, I have a pretty typical data structure: I'm using Firestore for a real-time doc based database, and I have a bunch of different collections and docs and fields. Just very simple, nothing crazy.
I have some model classes, and then some ViewModels to fetch data, filter, add documents to Firestore, and so on.
The app itself is almost 100% SwiftUI, and I'd like to keep it that way, just a challenge for my own development. I've hit a bit of a wall though.
In the app, I have a series of Views with NavigationView Lists that I pass small pieces of data to as you navigate. An example (this is for educational institutions) might be List of Schools > List of Classes > List of Students in Class > List of Grades for Student. Basically the perfect use for a navigation view and a bunch of lists.
The bug:
When I move from one list to the next in the stack, I fetch the firestore data, which loads for a second (enough that the list populates), and then "unloads" back to nothing. Here is some code where this happens (I've cleaned it up to make it as simple as possible):
struct ClassListView: View {
let schoolCode2: String
let schoolName2: String
#ObservedObject private var viewModelThree = ClassViewModel()
var body: some View {
VStack{
List{
if viewModelThree.classes.count > 0{
ForEach(self.viewModelThree.classes) { ownedClass in
NavigationLink(destination: StudentListView()){
Text(ownedClass.className)
}
}
} else {
Text("No Classes Found for \(schoolName2)")
}
}
}
.navigationBarTitle(schoolName2)
.onAppear(){
print("appeared")
self.viewModelThree.fetchData(self.schoolCode2)
}
}
}
So that's the ClassListView that I keep having issues with. For debugging, I added the else Text("No Classes Found") line and it does in fact show. So basically, view loads (this is all in a Nav view from a parent), it fetches the data, which is shown for a second (list populates) and then unloads that data for some reason, leaving me with just the "No classes found".
For more context, here is the code for the ClassViewModel (maybe that's where I'm going wrong?):
struct Classes: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
var schoolCode: String
var className: String
}
enum ClassesCodingKeys: String, CodingKey {
case id
case schoolCode
case className
}
class ClassViewModel: ObservableObject {
#Published var classes = [Classes]()
private var db = Firestore.firestore()
func fetchData(_ schoolCode: String) {
db.collection("testClasses")
.order(by: "className")
.whereField("schoolCode", isEqualTo: schoolCode)
.addSnapshotListener{ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("no docs found")
return
}
self.classes = documents.compactMap{ queryDocumentSnapshot -> Classes? in
return try? queryDocumentSnapshot.data(as: Classes.self)
}
}
}
func addClass(currentClass: Classes){
do {
let _ = try db.collection("testClasses").addDocument(from: currentClass)
}
catch {
print(error)
}
}
}
Most relevant bit is the fetchData() function above.
Maybe the problem is in the view BEFORE this (the parent view?). Here it is:
struct SchoolUserListView: View {
#State private var userId: String?
#EnvironmentObject var session: SessionStore
#ObservedObject private var viewModel = UserTeacherViewModel()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State private var counter = 0
#State private var showingAddSchool: Bool = false
#Environment(\.presentationMode) var presentationMode
func getUser() {
session.listen()
}
var body: some View {
VStack{
List{
if viewModel.teachers.count > 0{
ForEach(viewModel.teachers) { ownedClass in
NavigationLink(destination: ClassListView(schoolName2: ownedClass.schoolName, schoolCode2: ownedClass.schoolCode)){
Text(ownedClass.schoolName)
}
}
} else {
Button(action:{
self.presentationMode.wrappedValue.dismiss()
}){
Text("You have no schools, why not add one?")
}
}
}
.navigationBarTitle("Your Schools")
}
.onAppear(){
self.getUser()
self.userId = self.session.session?.uid
}
.onReceive(timer){ time in
if self.counter == 1 {
self.timer.upstream.connect().cancel()
} else {
print("fetching")
self.viewModel.fetchData(self.userId!)
}
self.counter += 1
}
}
}
And the FURTHER Parent to that View (and in fact the starting view of the app):
struct StartingView: View {
#EnvironmentObject var session: SessionStore
func getUser() {
session.listen()
}
var body: some View {
NavigationView{
VStack{
Text("Welcome!")
Text("Select an option below")
Group{
NavigationLink(destination:
SchoolUserListView()
){
Text("Your Schools")
}
NavigationLink(destination:
SchoolCodeAddView()
){
Text("Add a School")
}
Spacer()
Button(action:{
self.session.signOut()
}){
Text("Sign Out")
}
}
}
}
.onAppear(){
self.getUser()
}
}
}
I know that is a lot, but just so everyone has the order of parents:
StartingView > SchoolUserListView > ClassListView(BUG occurs here)
By the way, SchoolUserListView uses a fetchData method just like ClassListView(where the bug is) and outputs it to a foreach list, same thing, but NO problems? I'm just not sure what the issue is, and any help is greatly appreciated!
Here is a video of the issue:
https://imgur.com/a/kYtow6G
Fixed it!
The issue was the EnvironmentObject for presentationmode being passed in the navigation view. That seems to cause a LOT of undesired behavior.
Be careful passing presentationMode as it seems to cause data to reload (because it is being updated).

Is there a way to load a function with a Navigation Link in Swift?

I'd like to know if there is way to call a function along with a NavigationLink in Swift. I have a detail view for a list of posts but in order to get all of the information for that detailed post view I need to call a fetcher function in order to load a bunch of extra information which I cannot make with the initial call as it would largely increase the time to make the initial request for posts. Something like the following, keep in mind this most definitely isn't how it would look just what I envision as how it would work.
List(self.posts) { result in
NavigationLink(call: PostFetchingFunction(PostID: result.ID) -> destination: DetailedPostView(post: PostFetchingFunction.result)) {
Text("Go to detailed post view")
}
}
As I said, this, most definitely isn't correct Swift code, but just a code visualization of what I'd like to do might be helpful.
You could achieve this using a provider pattern conforming to ObservableObject
1. ContentView
struct ContentView: View {
var body: some View {
NavigationView {
List(0...100, id: \.self) { (index) in
NavigationLink("Show \(index)",
destination: NextView(provider: ItemProvider(id: index)))
}
.navigationBarTitle("List")
}
}
}
Destination is NextView
NextView requires something of type ItemProvider for it's initialization (we'll see this later)
2. NextView
struct NextView: View {
#ObservedObject var provider: ItemProvider
var body: some View {
Text(provider.title)
.navigationBarTitle("Item", displayMode: .inline)
.onAppear {
self.provider.load()
}
}
}
ItemProvider is an #ObservedObject which makes it a listener for changes in order to update the view
.onAppear is where we run a funtion, in this case self.provider.load() to get the provider to begin fetching
3. ItemProvider
class ItemProvider: ObservableObject {
private var id: Int
#Published var title: String = ""
init(id: Int) {
self.id = id
}
func load() {
title = "Loading \(id)"
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
guard let _weakSelf = self else { return }
_weakSelf.title = "Loaded \(_weakSelf.id)"
}
}
}
ItemProvider has to conform to ObservableObject in order to emit changes
Any variables within that are marked #Published will emit a change signal
ItemProvider has a load function that actually does the fetching and if it updates any #Published variables, the connected Views will be notified and will update automatically

Error deleting records from a SwiftUI List and Realm

Has anyone been able to successfully integrate Realm with SwiftUI, especially deleting records/rows from a SwiftUI List? I have tried a few different methods but no matter what I do I get the same error. After reading some related threads I found out that other people have the same issue.
The following code successfully presents all of the items from Realm in a SwiftUI List, I can create new ones and they show up in the List as expected, my issues is when I try to delete records from the List by either manually pressing a button or by left-swiping to delete the selected row, I get an Index is out of bounds error.
Any idea what could be causing the error?
Here is my code:
Realm Model
class Dog: Object {
#objc dynamic var name = ""
#objc dynamic var age = 0
#objc dynamic var createdAt = NSDate()
#objc dynamic var userID = UUID().uuidString
override static func primaryKey() -> String? {
return "userID"
}
}
SwiftUI Code
class BindableResults<Element>: ObservableObject where Element: RealmSwift.RealmCollectionValue {
var results: Results<Element>
private var token: NotificationToken!
init(results: Results<Element>) {
self.results = results
lateInit()
}
func lateInit() {
token = results.observe { [weak self] _ in
self?.objectWillChange.send()
}
}
deinit {
token.invalidate()
}
}
struct DogRow: View {
var dog = Dog()
var body: some View {
HStack {
Text(dog.name)
Text("\(dog.age)")
}
}
}
struct ContentView : View {
#ObservedObject var dogs = BindableResults(results: try! Realm().objects(Dog.self))
var body: some View {
VStack{
List{
ForEach(dogs.results, id: \.name) { dog in
DogRow(dog: dog)
}.onDelete(perform: deleteRow )
}
Button(action: {
try! realm.write {
realm.delete(self.dogs.results[0])
}
}){
Text("Delete User")
}
}
}
private func deleteRow(with indexSet: IndexSet){
indexSet.forEach ({ index in
try! realm.write {
realm.delete(self.dogs.results[index])
}
})
}
}
Error
Terminating app due to uncaught exception ‘RLMException’, reason: ‘Index 23 is out of bounds (must be less than 23).’
Of course, the 23 changes depending on how many items are in the Realm database, in this case, I had 24 records when I swiped and tapped the delete button.
FYI - The error points to the AppDelegate file with a Thread 1: signal SIGABRT.
Here is an example of how i do this. This is without realm operations but i hope u get the idea where you can put the realm stuff. (I also almost never use the realm objects directly but instead convert them to structs or classes.)
import Foundation
import Realm
import Combine
import SwiftUI
struct dogs: Hashable {
let name: String
}
class RealmObserverModel: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
#Published var dogsList: [dogs] = [dogs(name: "Dog 1"), dogs(name: "Dog 2")]
// get your realm objects here and set it to
// the #Publsished var
func getDogs() {
let count = dogsList.count + 1
dogsList.append(dogs(name: "Dog \(count)"))
}
// get your realm objects here and set it to
// the #Publsished var
func deletetDogs() {
_ = dogsList.popLast()
}
}
/// Master View
struct DogView: View {
#EnvironmentObject var observer: RealmObserverModel
var body: some View {
VStack{
DogsListView(dogsList: $observer.dogsList)
HStack{
Button(action: {
self.observer.getDogs()
}) {
Text("Get more dogs")
}
Button(action: {
self.observer.deletetDogs()
}) {
Text("Delete dogs")
}
}
}
}
}
// List Subview wiht Binding
struct DogsListView: View {
#Binding var dogsList: [dogs]
var body: some View {
VStack{
List{
ForEach(dogsList, id:\.self) { dog in
Text("\(dog.name)")
}
}
}
}
}
struct DogView_Previews: PreviewProvider {
static var previews: some View {
DogView().environmentObject(RealmObserverModel())
}
}
Not a great solution but my work around was copying each realm result to a local object/array. I updated my Lists/Views to use the realmLocalData instead of the data returned from the realm object itself.
class ContentViewController: ObservableObject {
private var realmLocalData: [ScheduleModel] = [ScheduleModel]()
private let realm = try! Realm()
func updateData() {
realmLocalData.removeAll()
let predicate = NSPredicate(format: "dateIndex >= %# && dateIndex <= %#", argumentArray: [startDate, endDate])
let data = self.realm.objects(MonthScheduleModel.self).filter(predicate)
for obj in data {
realmLocalData.append(ScheduleModel(realmObj: obj))
}
}
}

Resources