How to delete on a line in a list with Realm - swiftui

I have a List structure that removes a line with ondelete, but while returning the result, the line is deleted but shows an error.
I think the problem is that the result is not updated correctly.
What code should I add to update the list?
List{
ForEach(datosRealm.aquaris, id: \.self) { item in
Text(item.nombre)
}.onDelete { (indexSet) in
let aqua = datosRealm.aquaris[indexSet.first!]
let realm = try! Realm()
try! realm.write{
realm.delete(aqua)
}
}
}
import SwiftUI
import RealmSwift
final class DadesRealm: ObservableObject {
#Published var aquaris: [Aquaris]
private var cargaToken: NotificationToken?
init() {
let realm = try! Realm()
aquaris = Array(realm.objects(Aquaris.self))
recargarDatos()
}
private func recargarDatos() {
let realm = try! Realm()
let personas = realm.objects(Aquaris.self)
cargaToken = personas.observe { _ in
self.aquaris = Array(personas)
}
}
deinit {
cargaToken?.invalidate()
}
}
class Aquaris: Object {
#objc dynamic var nombre = ""
#objc dynamic var litros = ""
#objc dynamic var tipoAcuario = ""
#objc dynamic var data : Date = Date()
#objc dynamic var id = UUID().uuidString
override static func primaryKey() -> String? {
return "id"
}
let mascota = List<Controls>()
}
Error:
libc++abi.dylib: terminating with uncaught exception of type
NSException
*** Terminating app due to uncaught exception 'RLMException', reason: 'Object has been deleted or invalidated.' terminating with uncaught
exception of type NSException

The problem is taking a live updating Realm results object and casting it to an array, making those objects non-live updating (static)
In other words realm Results objects always reflect the current state of that data; if an object is deleted from Realm, the realm results object will also have that object removed. Likewise if an object is added then the results object will reflect that addition. (noting this is for objects that fit however the results objects was crafted; filtered, sorted etc)
So here's the issue
final class DadesRealm: ObservableObject {
#Published var aquaris: [Aquaris]
private var cargaToken: NotificationToken?
init() {
let realm = try! Realm()
aquaris = Array(realm.objects(Aquaris.self)) <- PROBLEM IS HERE
So you need to do one of two things
1 - Make aquaris a realm results object
#Published var aquaris: Results<Aquaris>? = nil
then
aquaris = realm.objects(Aquaris.self)
or
2 - If you must use that as an array, when the object is deleted from Realm, you also need to delete it from the array to keep them 'in sync'
I advise using suggestion 1 as it will make things easier in the long run.

Related

Trying to use an Observable Object in another class

I want to use an observable object in another class.
I want to use pitchers
import Foundation
import SwiftUI
class PositionViewModel: ObservableObject, Identifiable {
#Published var Pitchers: [String] = ["--","--","--","--","--","--"]
}
in this class then be able to pass it to different functions to validate rules.
class ValidationLogic: ObservableObject {
#Published var Positions: PositionViewModel = PositionViewModel()
var TempArray: [String];.self//error here
init(){
TempArray = Positions.Pitchers
}
static func Validation(Position: String, FrameStatus: Bool){
confirmNoBackToBack(Position: Position, FrameStatus: FrameStatus)
}
static func confirmNoBackToBack(Position: String, FrameStatus: Bool){
}
}
I have gotten to this point and am now getting an Expected Declaration error on the bold line. Not sure if I am just this last error from getting this to work or doing this the completely wrong way. I can use pitchers in a view but cant see to get it passed to my validationlogic class.
This is not valid Swift syntax, replace that line with:
var TempArray: [String]
Then modify you init to initialize Positions there:
var Positions: PositionViewModel
var TempArray: [String]
init() {
self.Positions = PositionViewModel()
TempArray = Positions.Pitchers
}

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

SwiftUI App Shows Realm Changes but Not New Objects

Realm 10.7.3, Xcode 12.4, macOS 11.2.3
I am experimenting with Realm and Combine+SwiftUI. When I make changes to my data in Realm Studio, they immediately reflect in my app's UI as expected. But when I add or delete an object, my app UI does not change.
Here's my model definition:
//--- Model ---
class Item: Object, ObjectKeyIdentifiable {
#objc dynamic var _id = ObjectId.generate()
#objc dynamic var text = ""
}
Here's my view model:
//--- View Model ---
class ItemModel: ObservableObject {
static let shared = ItemModel()
var token: NotificationToken? = nil
#Published var items = [Item]()
init(){
let realm = try! Realm()
let results = realm.objects(Item.self)
items = Array(results)
token = results.observe { [weak self] _ in
print("-- updated --")
self?.objectWillChange.send()
}
}
deinit{
token?.invalidate()
}
}
And last of all, here's my SwiftUI view:
//--- View ---
struct ItemView: View {
#StateObject private var model = ItemModel.shared
var body: some View {
ScrollView{
VStack(spacing: 7){
ForEach(model.items, id: \._id) { item in
Text(item.text)
}
}
}
}
}
Any ideas why my app won't show new/deleted objects and only edits? If I rebuild my app, the new/deleted objects are shown.
Realm Results objects are live-updating objects and always reflect the current state of those objects in Realm.
However, if you cast your Realm Results object to an array
items = Array(results)
It 'disconnects' those objects ( the results ) from Realm and it (they) are no longer live updating.
Additionally Realm Results objects are lazily-loaded, meaning that they are only loaded into memory when needed so thousands of objects take up almost no space.
Storing them in an array changes that - they are all loaded into memory and could overwhelm the device.
Best practice is to leave Realm Collections (Results, Lists) as that type of object throughout the duration of using them instead of casting to an array.

Store dictionary in UserDefaults

This is a similar approach to Save dictionary to UserDefaults, however, it is intended for SwiftUI, not using a single line like set, so I want to store the value somewhere with a variable so I can call it easily. Also it's different because I'm asking for an initialization.
I have the following:
#Published var mealAndStatus: Dictionary
init() {
mealAndStatus = ["Breakfast": "initial", "Snack": "notSet", "Lunch": "notSet", "Snack2": "notSet", "Dinner": "notSet"]
if let storedDay = UserDefaults.standard.value(forKey: "mealAndStatus") {
mealAndStatus = storedDay as! Dictionary
}
}
1- How do I correctly store that dictionary in UserDefaults in SwiftUI?
2- That init, do I have to call it at the beginning of ContentView? Or can I leave it on the other swift file like that? Not sure how the init gets called.
I already made one with bool working:
#Published var startDay: Bool
init() {
startDay = true
if let storedDay = UserDefaults.standard.value(forKey: "startDay") {
startDay = storedDay as! Bool
}
}
but the dictionary doesn't seem to work. I need to initialize that dictionary and also store it in UserDefaults so I can access it later. Any help is appreciated.
This is the perfect solution I found for SwiftUI:
Store this somewhere, in my case I created a class just for UserDefaults:
#Published var mealAndStatus: [String: Date] =
UserDefaults.standard.dictionary(forKey: "mealAndStatus") as? [String: Date] ?? [:] {
didSet {
UserDefaults.standard.set(self.mealAndStatus, forKey: "mealAndStatus")
}
}
That above initializes the dictionary and also creates a variable to be easily called and use to update the value. This can be modified at lunch time and add new values, that way is initialized with whatever I want.
Furthermore, now on Apple Dev wwdc20 they announced a new way of handling UserDefaults with SwiftUI which may be even better than the above. The propery wrapper is called: #AppStorage.
Using JSONEncoder and JSONDecoder would help you convert to data any struct or dictionary that conforms to codable.
let arrayKey = "arrayKey"
func store(dictionary: [String: String], key: String) {
var data: Data?
let encoder = JSONEncoder()
do {
data = try encoder.encode(dictionary)
} catch {
print("failed to get data")
}
UserDefaults.standard.set(data, forKey: key)
}
func fetchDictionay(key: String) -> [String: String]? {
let decoder = JSONDecoder()
do {
if let storedData = UserDefaults.standard.data(forKey: key) {
let newArray = try decoder.decode([String: String].self, from: storedData)
print("new array: \(newArray)")
return newArray
}
} catch {
print("couldn't decode array: \(error)")
}
return nil
}
// You would put this where you want to save the dictionary
let mealAndStatus = ["Breakfast": "initial", "Snack": "notSet", "Lunch": "notSet", "Snack2": "notSet", "Dinner": "notSet"]
store(dictionary: mealAndStatus, key: arrayKey)
// You would put this where you want to access the dictionary
let savedDictionary = fetchDictionay(key: arrayKey)
On a side note, you probably shouldn't be using standard defaults for storing stuff like this. Storing it as a database, or saving it in a file especially with encryption on eith the database or the file might be a bit safer.

Filtering realm swift objects with calculation needed

I have the following object model:
public class Geofence: Object, Mappable {
#objc public dynamic var id: String?
#objc public dynamic var name: String?
#objc public dynamic var uuid: String?
#objc public dynamic var type: String?
#objc public dynamic var latitude = 0.0
#objc public dynamic var longitude = 0.0
#objc public dynamic var radius = 0
...
}
and my requirement is to get Geofence objects in range of a specific distance like so:
func getGeofences(location: CLLocation, inDistance: Double = 20000.0) -> [Geofence] {
do {
let realm = try Realm()
return realm.objects(Geofence.self).filter("type == 'AREA'").filter({
location.distance(from: CLLocation(latitude: $0.latitude, longitude: $0.longitude)) + CLLocationDistance($0.radius) <= inDistance
})
} catch let error as NSError {
print(error.localizedDescription)
fatalError(error.localizedDescription)
}
}
However, what this does, is: it only gets the objects with type == 'AREA' and performs the calculation in memory using the swift standard filter(_ isInlcuded: (Geofence) throws -> Bool ) rethrows -> [Geofence] function which can be pretty expensive if I get from realm 1.000.000 objects and do the calculation in memory.
What I need instead, is to perform the calculation directly using realm and get from realm only the relevant objects.
Is this possible using realm ? (realm subqueries?)
Any help would be much appreciated. Thank you.