How to completely delete items when using #ObservedResults in Realm and SwiftUI - swiftui

I'm trying the new Realm 10 wrappers by using the example given in the Realm documentation Integration Guides -> SwiftUI & Combine and I like how simple it is to add and delete records when using the #ObservedResults and the #ObservedRealmObject. The one thing I don't quite understand is why when deleting items from the Group object it only removes the items from the Group but leaves the actual items in the Item Realm object undeleted. See the Realm Browser image below.
Here is what the Realm Browser shows after adding and deleting four (4) items through the app UI, as you can see the four (4) items were deleted from the Group but left all four (4) items in the Item object.
Can someone please explain why the items don't get deleted from the Item object only from the Group object when calling .onDelete(perform: $group.items.remove)? How can I delete them?
I tried deleting them like this...
ItemsView.swift
.onDelete(perform: deleteItems)
func deleteItems(at offsets: IndexSet){
let realm = try? Realm()
try! realm?.write {
// 1. delete items
for item in list.items{
realm?.delete(item)
}
// 2. delete the list
realm?.delete(list)
}
}
but I got the following error:
Thread 1: "Can only delete an object from the Realm it belongs to."
Again, the whole code can be found in the Integration Guides - Without Sync.
EDIT: Added code for, LocalOnlyContentView, ItemsView, Group and Item models.
Item.swift
import Foundation
import RealmSwift
/// Random adjectives for more interesting demo item names
let randomAdjectives = [
"fluffy", "classy", "bumpy", "bizarre", "wiggly", "quick", "sudden",
"acoustic", "smiling", "dispensable", "foreign", "shaky", "purple", "keen",
"aberrant", "disastrous", "vague", "squealing", "ad hoc", "sweet"
]
/// Random noun for more interesting demo item names
let randomNouns = [
"floor", "monitor", "hair tie", "puddle", "hair brush", "bread",
"cinder block", "glass", "ring", "twister", "coasters", "fridge",
"toe ring", "bracelet", "cabinet", "nail file", "plate", "lace",
"cork", "mouse pad"
]
final class Item: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var name = "\(randomAdjectives.randomElement()!) \(randomNouns.randomElement()!)"
#Persisted var isFavorite = false
#Persisted(originProperty: "items") var group: LinkingObjects<Group>
}
Group.swift
import Foundation
import RealmSwift
final class Group: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var items = RealmSwift.List<Item>()
}
ItemsView.swift
struct ItemsView: View {
#ObservedRealmObject var group: Group
var leadingBarButton: AnyView?
var body: some View {
NavigationView {
VStack {
// The list shows the items in the realm.
List {
ForEach(group.items) { item in
ItemRow(item: item)
}
.onDelete(perform: $group.items.remove)
.onMove(perform: $group.items.move)
}.listStyle(GroupedListStyle())
.navigationBarTitle("Items", displayMode: .large)
.navigationBarBackButtonHidden(true)
.navigationBarItems(
leading: self.leadingBarButton,
trailing: EditButton())
HStack {
Spacer()
Button(action: {
$group.items.append(Item())
}) { Image(systemName: "plus") }
}.padding()
}
}
}
}
LocalOnlyContentView.swift
struct LocalOnlyContentView: View {
#ObservedResults(Group.self) var groups
var body: some View {
if let group = groups.first {
AnyView(ItemsView(group: group))
} else {
AnyView(ProgressView().onAppear {
$groups.append(Group())
})
}
}
}
SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = LocalOnlyContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}

Keep in mind that what's being shown in that tutorial is how to remove an item from a group's List - not how to totally delete the item.
Going very high level with this answer - objects in a List are not the actual objects - they are a "pointer" to the actual item stored on disk.
Suppose you have three items
Item 0
Item 1
Item 2
and a Group with List of items
MyGroup
List of items
Item 0
Item 1
Item 2
What's actually going on is the List "points" to the items on disk
My Group
List of items
pointer to Item 0
pointer to Item 1
pointer to Item 2
So when this is called $group.items.remove it's removing the pointer to the item from the list, not the item itself.
The solution (well, one of the solutions) is to remove the actual item
let myItemToRemove = List of items[0] //get the item at index 0
realm.remove(myItemToRemove) // removes the item itself from realm, along
// with the pointer stored in the list
The code to actually delete an item is this
try? realm.write {
realm.delete(objectToDelete)
}

Related

in SwiftUI, I have 2 Entities (A & B) in my CoreData with a relationship (one to many) between them, how can I fetch all attributes of B in TextFields

Let's say I have 2 entities:
GameSession :which has Attributes "date", "place", "numberofplayer" + a relationship called "players" with "Player"
Player: which has Attributes "name","score_part1","score_part2","score_part3" + a relationship with "GameSession"
the relationship is "one to many": One session can have many players
Let's say now I have a list of GameSession and when I click on on one (with a NavigationLink)
It sends me to a new view where I can see:
All the names of the players of that session (in text) and also right next to the player name I would like to have 3 TextField in which I can enter (an update) "score_part1","score_part2","score_part3" for every players of that session
Basically I am able to display the name of all the players of a given session, But it seems impossible to have the "score_part1","score_part2","score_part3" in editable TextField...
I have an error saying "Cannot convert value of type 'String' to expected argument type 'Binding<String>'"
Basically in my first view I have something like that:
struct RamiListePartieUIView: View {#Environment(.managedObjectContext) var moc#FetchRequest(entity: GameSession.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \GameSession.date, ascending: false)]) var gamesessions: FetchedResults<GameSession>
var body: some View {
VStack {
List {
ForEach(gamesessions, id: \.date) { session in
NavigationLink (destination: DetailPartieSelecUIView(session: session)){
Text("\(session.wrappedPlace) - le \(session.wrappedDate, formatter: itemFormatter) ")
}
}
.onDelete(perform: deleteSessions)
.padding()
}
}
}
}
And in my second view I have something like that:
struct DetailPartieSelecUIView: View {
#State var session:GameSession
#Environment(\.managedObjectContext) var moc
var body: some View {
Section("Ma session du \(session.wrappedDate, formatter: itemFormatter)"){
ForEach(session.playersArray, id: \.self) { player in
HStack {
Text(player.wrappedName) // OK it works
TextField("score", text : player.wrappedScore_part1) // it generates an error
TextField("score", text : player.wrappedScore_part2) // it generates an error
TextField("score", text : player.wrappedScore_part3) // it generates an error
}
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
// formatter.dateStyle = .short
// formatter.timeStyle = .medium
formatter.dateFormat = "YYYY/MM/dd" //"YY/MM/dd"
return formatter
}()
also,
I have defined the "wrappedScore_part1","wrappedScore_part2","wrappedScore_part3" in the Player+CoreDataProperties.swift file
and "wrappedPlace", "wrappedData" as well as the "PlayersArray" in the GameSession+CoreDataProperties.swift file
it is done like that:
public var wrappedPlace: String {
place ?? "Unknown"
}
// Convert NSSet into an array of "Player" object
public var playersArray: [Player] {
let playersSet = players as? Set<Player> ?? []
return playersSet.sorted {
$0.wrappedName< $1.wrappedName
}
}
I am new at coding with swiftUI so I am probably doing something wrong... If anyone can help me it would be much appreciated.
Thanks a lot
I have tried a lot of things. Like changing the type of my attribute to Int32 instead os String. As I am suppose to enter numbers in those fields, I thought it would be best to have Integer. But it didn't change anything. and ultimately I had the same kind of error message
I tried also to add the $ symbol, like that:
TextField("score", text : player.$wrappedScore_part1)
But then I had other error message popping up at the row of my "ForEach", saying "Cannot convert value of type '[Player]' to expected argument type 'Binding'"
And also on the line just after the HStack, I had an error saying "Initializer 'init(_:)' requires that 'Binding' conform to 'StringProtocol'"
Thank you for your help!
Best regards,
JB
Your first problem of how to fetch the players in a session you need to supply a predicate to the #FetchRequest<Player>, e.g.
#FetchRequest
private var players: FetchedResults<Player>
init(session: Session) {
let predicate = NSPredicate(format: "session = %#", session)
let sortDescriptors = [SortDescriptor(\Player.timestamp)] // need something to sort by.
_players = FetchRequest(sortDescriptors: sortDescriptors, predicate: predicate)
}
That acts like a filter and will only return the players that have the session relation equalling that object. The reason you have to fetch like this is so any changes will be detected.
The second problem about the bindings can be solved like this:
struct PlayerView: View{
#ObservedObject var player: Player {
var body:some View {
if let score = Binding($player.score) {
TextField("Score", score)
}else{
Text("Player score missing")
}
}
}
This View takes the player object as an ObservedObject so body will be called when any of its properties change and allows you to get a binding to property. The Binding init takes an optional binding and returns a non-optional, allowing you to use it with a TextField.

Changing swipeActions dynamically in SwiftUI

I am trying to change the swipeAction from "Paid" to "UnPaid" based on payment status and somehow seems to be failing. Error: "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
Appreciate any help
struct ContentView: View {
var data: [Data] = [data1, data2, data3, data4]
#State var swipeLabel = true
var body: some View {
let grouped = groupByDate(data)
List {
ForEach(Array(grouped.keys).sorted(by: >), id: \.self) { date in
let studentsDateWise = grouped[date]!
Section(header:Text(date, style: .date)) {
ForEach(studentsDateWise, id:\.self) { item in
HStack {
Text(item.name)
padding()
Text(item.date, style: .time)
if(item.paymentStatus == false) {
Image(systemName: "person.fill.questionmark")
.foregroundColor(Color.red)
} else {
Image(systemName: "banknote")
.foregroundColor(Color.green)
}
} // HStack ends here
.swipeActions() {
if(item.paymentStatus) {
Button("Paid"){}
} else {
Button("UnPaid"){}
}
}
} // ForEach ends here...
} // section ends here
} // ForEach ends here
} // List ends here
} // var ends here
}
The body func shouldn't do any grouping or sorting. You need to prepare your data first into properties and read from those in body, e.g. in an onAppear block. Also if your Data is a struct you can't use id: \.self you need to either specify a unique identifier property on the data id:\.myUniqueID or implement the Indentifiable protocol by either having an id property or an id getter that computes a unique identifier from other properties.
I would suggest separating all this code into small Views with a small body that only uses one or a two properties. Work from bottom up. Then eventually with one View works on an array of dates and another on an array of items that contains the small Views made earlier.
You should probably also learn that if and foreach in body are not like normal code, those are converted into special Views. Worth watching Apple's video Demystify SwiftUI to learn about structural identity.

How to add and delete objects from a List from an object who's inside another List in SwiftUI and Realm

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()
}
}
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

Quandry over Text Storage

I have an app with a number of categories--some the can be changed by the user and some that can't. The categories that can't be changed are stored currently in an array while categories that the user can change are stored in a class. The problem comes when creating expense entries where the picker needs to show both types of categories.
The other side of the coin is to place all the categories (text strings) in the class. Here the expense entry picker and class storage will work ok, but then there is the problem of preventing the user from deleting the default categories.
I'm guessing that latter option is the better route since it will place all the categories in the picker list. Here is the code for storing the dynamic categories. I suppose I could add some init() code to store the categories that don't change. Not sure exactly how to do that.
struct CatItem: Codable {
var catName: String
var catPix: String
}
class Categories: ObservableObject {
#Published var catItem: [CatItem] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(catItem) {
UserDefaults.standard.set(encoded, forKey: "workCat")
}
}
}
init() {
if let catItem = UserDefaults.standard.data(forKey: "workCat") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([CatItem].self, from: catItem) {
self.catItem = decoded
return
}
}
self.catItem = []
}
}
How would you prevent the user from deleting some of the fixed categories? Usually you have a list with an onDelete statement.
Can you use the index to determine if deleting is allowed? For example don't delete entry if indexSet[index] < 8?
.onDelete { indexSet in
for index in indexSet {
remove entry
}
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
}
I assume you wanted something like
.onDelete { indexSet in
guard let i = indexSet.first, indexSet[i] < 8 else { return }
// .. other code
I resolved this storage quandary by deciding to store both the permanent and changeable categories in the same class catItem. In the module where the categories may be viewed and where new categories may be added I use a filtered list showing only the categories that may be edited.
This is how the categories are initialized at startup:
let item = CatItem(catName: "name1", catPix: "sf symbol", noShow: true)
self.catItem.append(item)
This is how the categories are displayed in a list:
List {
ForEach(categories.catItem, id: \.catName) { item in
if item.noShow == false {
HStack {
Text(item.catName)
.padding(.horizontal, 10)
Spacer()
Image(systemName: item.catPix).resizable()
.frame(width: 30, height: 30)
}
}
}
.onDelete(perform: removeItems)
}
So if the user can't see the permanent categories in List, then they can't delete them. The user can only delete the categories that they add.
When the user adds new categories the noShow parameter is set to false.
In the picker the categories are not filtered so all categories may be viewed.

SwiftUI - Wrong index with removeAll(where: )

I want to let the user remove an image in an array. When the image is pressed, an action sheet is presented so the user can confirm the removal of the image. The problem is that it removes the wrong image. It always removes the first image of the array.
#State var pickerResult: [SImage] = []
...
ScrollView(.horizontal, showsIndicators: true){
HStack{
ForEach(pickerResult) { simage in
Image(uiImage: simage.image)
.onTapGesture() {
imageActionSheetIsPresented = true
// This will work: self.pickerResult.removeAll(where: {$0.image == simage.image})
}
.actionSheet(isPresented: $imageActionSheetIsPresented) {
ActionSheet(title: Text("Do you want to remove the image?"), buttons: [
.default(Text("Remove image")){
self.pickerResult.removeAll(where: {$0.image == simage.image})
print(simage.id)
// Returns id of the first image in the array
},
.cancel()
])
}
}
}
}
As you see in the code, skipping the confirmation and letting the user remove the image with onTap will work just fine.
Some how 'simage' is always the first item in the array when using ActionSheet.
Here is the SImage:
struct SImage: Identifiable{
var id = UUID()
var image: UIImage
}