SwiftUI App Shows Realm Changes but Not New Objects - swiftui

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.

Related

How to delete on a line in a list with Realm

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.

Using a button to add data that a user inputted

Okay so I've been working on this for several days now and have not had any luck with an answer that makes any sense. I have a form in SwiftUI, using #ObservedObject to pull variables from a struct. In that form, I have a variety of text fields and pickers that the user can interact with. HOWEVER, I cannot figure out how to get my "Add" button to actually add that data to any of the other views in the app. I followed the sandwiches tutorial from WWDC20, with significant changes, so there is a swift file with "testData" and essentially I'm trying to get it so that the button uses the user input to append the testData and show that instead of nothing.
struct Horse: Identifiable {
var id = UUID()
var name: String
var gender: String
var breed: String
var type: String
var scale: String
var brand: String
var finish: String
var specialty: String
var imageName: String { return name }
var thumbnailName: String { return name + "Thumb" }
}
let testData = [
Horse(name: "Van Gogh", gender: "Stallion", breed: "Unknown", type: "Customized", scale: "Stablemate", brand: "Peter Stone", finish: "Gloss", specialty: "OOAK")
]
So this is what I'm using to establish testData and the parameters for what should be included in it.
func addANewHorse() {
withAnimation {
testStore.horses.append(Horse(name: "\(horseDetails.horseName)", gender: "\(horseDetails.selectedGender.rawValue)", breed: "\(horseDetails.horseBreed)", type: "\(horseDetails.type.rawValue)", scale: "\(horseDetails.scale.rawValue)", brand: "\(horseDetails.brand.rawValue)", finish: "\(horseDetails.finish.rawValue)", specialty: "\(horseDetails.specialty.rawValue)"))
}
}
Button("Add", action: {
addANewHorse();
self.presentationMode.wrappedValue.dismiss()
})
And that is what I'm using to try and append the testData to update with the users input. I know this is kind of choppy but does anyone have any advice whatsoever?
---EDIT---
My main app file looks like this...
#main
struct Pferd_HerdApp: App {
#StateObject private var store = HorseStore()
#StateObject private var horseDetails = HorseDetails()
var body: some Scene {
WindowGroup {
ContentView(store: store, horseDetails: HorseDetails())
}
}
}
my horse store class looks like this...
class HorseStore: ObservableObject {
#Published var horses: [Horse]
init(horses: [Horse] = []) {
self.horses = horses
}
}
let testStore = HorseStore(horses: testData)
Also, "HorseDetails" is the observableobject I'm trying to pull data from to append the testData, so here is the code for that
class HorseDetails: ObservableObject {
#Published var horseName = ""
#Published var selectedGender = Gender.allCases[0]
#Published var horseBreed = ""
#Published var purchaseDate = Date()
#Published var winCount = ""
#Published var notes = ""
#Published var brand = Brands.allCases[0]
#Published var type = Type.allCases[0]
#Published var scale = Scale.allCases[0]
#Published var finish = Finish.allCases[0]
#Published var specialRun = false
#Published var specialty = Specialty.allCases[0]
}
var horseDetails = HorseDetails()
and I changed the let for testData to a variable
Since your Question leaves a lot of code out, I will be making a few assumptions. I'm assuming that your form (where you have the button to add data) and your view for displaying the data are in two different views. You have not included your view model in the code, although there was an instance of your view model (testStore) used in the code above. You need to make sure that somewhere at the root of your view hierarchy, you made an instance of your view model (I'm assuming its called TestStoreViewModel) and passed that as an environment object to your subviews. For example, you should something like this
#main
struct YourApp: App {
let testStoreViewModel = TestStoreViewModel()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(testStoreViewModel)
}
}
}
in all of your views where you need to use the data from your TestStoreViewModel, you should declare it like so
#EnvironmentObject var testStore:TestStoreViewModel
Using environment objects means that your observable object is automatically synced across all of your views that use the environment object. Everything else in the code above should work fine with the use of EnvironmentObjects and a single source of truth. For more on environment objects, you can check out this article which in my opinion is great at explaining Environment Objects in swiftui. It is also important to note that in that article, it mentioned the use of a SceneDelegte and the ContentView being wrapped around a UIHostingController. That was replaced by the first block of code I showed you above.

SwiftUI IOS 14 New AppDeligate with .environmentObject

For the new delegate file in iOS 14 I need to include both the .environmentObject settings and the UserSettings: ObservableObject (which is a Realm Class).
But I first need to create the User data if there is none (first time user) otherwise it give me a null error and crashes.
Where would I put the code to initiate the user before calling it in the body loads?
#main
struct myapp_App: App {
let userSettings = UserSettings() // calling the data which will not exist if initial user
var body: some Scene {
WindowGroup {
ContentView().environmentObject(userSettings)
}
}
}
Thank you.
I put the code to create the user if first time inside the int() of the ObservableObject.
I hope can only hope this code is proper? But it works.
Good luck.
class UserSettings: ObservableObject {
#Published var name: String? = nil
#Published var email: String? = nil
init(){
if User.userExist() == false {
User.initiateUser()
}
let u = User.getUser()
name = u!.name
email = u!.email
}
}

SwiftUI - Navigate to view after retrieving data

So I'm retrieving data from FireStore. I'm retrieving the data successfully. When I tap my search button the first time the data is being downloaded and the new view is pushed. As a result, I get a blank view. But when I go back, hit search again, sure enough I can see my data being presented.
How can I make sure I first have the data I'm searching for THEN navigate to the new view? I've used #State variables etc. But nothing seems to be working. I am using the MVVM approach.
My ViewModel:
class SearchPostsViewModel: ObservableObject {
var post: [PostModel] = []
#State var searchCompleted: Bool = false
func searchPosts(completed: #escaping() -> Void, onError: #escaping(_ errorMessage: String) -> Void) {
isLoading = true
API.Post.searchHousesForSale(propertyStatus: propertyStatus, propertyType: propertyType, location: location, noOfBathrooms: noOfBathroomsValue, noOfBedrooms: noOfBedroomsValue, price: Int(price!)) { (post) in
self.post = post
print(self.post.count)
self.isLoading = false
self.searchCompleted.toggle()
}
}
}
The code that does work, but with the bug:
NavigationLink(destination: FilterSearchResults(searchViewModel: self.searchPostsViewModel)
.onAppear(perform: {
DispatchQueue.main.async {
self.createUserRequest()
}
})
)
{
Text("Search").modifier(UploadButtonModifier())
}
Try with the following modified view model
class SearchPostsViewModel: ObservableObject {
#Published var post: [PostModel] = [] // << make both published
#Published var searchCompleted: Bool = false
func searchPosts(completed: #escaping() -> Void, onError: #escaping(_ errorMessage: String) -> Void) {
isLoading = true
API.Post.searchHousesForSale(propertyStatus: propertyStatus, propertyType: propertyType, location: location, noOfBathrooms: noOfBathroomsValue, noOfBedrooms: noOfBedroomsValue, price: Int(price!)) { (post) in
DispatchQueue.main.async {
self.post = post // << update on main queue
print(self.post.count)
self.isLoading = false
self.searchCompleted.toggle()
}
}
}
}
You should look at the Apple documentation for #State and ObservableObject
https://developer.apple.com/documentation/combine/observableobject
https://developer.apple.com/documentation/swiftui/state
Your issue is with using an #State in a non-UI class/View.
It might help if you start with the Apple SwiftUI tutorials. So you understand the differences in with the wrappers and learn how it all connects.
https://developer.apple.com/tutorials/swiftui
Also, when you post questions make sure your code can be copied and pasted onto Xcode as-is so people can test it. You will get better feedback if other developers can see what is actually happening. As you progress it won't be as easy to see issues.

How to prod a SwiftUI view to update when a model class sub-property changes?

I've created a trivial project to try to understand this better. Code below.
I have a source of data (DataSource) which contains a #Published array of MyObject items. MyObject contains a single string. Pushing a button on the UI causes one of the MyObject instances to update immediately, plus sets off a timer to update a second one a few seconds later.
If MyObject is a struct, everything works as I imagine it should. But if MyObject is a class, then the refresh doesn't fire.
My expectation is that changing a struct's value causes an altered instance to be placed in the array, setting off the chain of updates. However, if MyObject is a class then changing the string within a reference type leaves the same instance in the array. Array doesn't realise there has been a change so doesn't mention this to my DataSource. No UI update happens.
So the question is – what needs to be done to cause the UI to update when the MyObject class's property changes? I've attempted to make MyObject an ObservableObject and throw in some didchange.send() instructions but all without success (I believe these are redundant now in any case).
Could anyone tell me if this is possible, and how the code below should be altered to enable this? And if anyone is tempted to ask why I don't just use a struct, the reason is because in my actual project I have already tried doing this. However I am using collections of data types which modify themselves in closures (parallel processing of each item in the collection) and other hoops to jump through. I tried re-writing them as structs but ran in to so many challenges.
import Foundation
import SwiftUI
struct ContentView: View
{
#ObservedObject var source = DataSource()
var body: some View
{
VStack
{
ForEach(0..<5)
{i in
HelloView(displayedString: self.source.results[i].label)
}
Button(action: {self.source.change()})
{
Text("Change me")
}
}
}
}
struct HelloView: View
{
var displayedString: String
var body: some View
{
Text("\(displayedString)")
}
}
class MyObject // Works if declared as a Struct
{
init(label: String)
{
self.label = label
}
var label: String
}
class DataSource: ObservableObject
{
#Published var results = [MyObject](repeating: MyObject(label: "test"), count: 5)
func change()
{
print("I've changed")
results[3].label = "sooner"
_ = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: {_ in self.results[1].label = "Or later"})
}
}
struct ContentView_Previews: PreviewProvider
{
static var previews: some View
{
ContentView()
}
}
When MyObject is a class type the results contains references, so when you change property of any instance inside results the reference of that instance is not changed, so results is not changed, so nothing published and UI is not updated.
In such case the solution is to force publish explicitly when you perform any change of internal model
class DataSource: ObservableObject
{
#Published var results = [MyObject](repeating: MyObject(label: "test"), count: 5)
func change()
{
print("I've changed")
results[3].label = "sooner"
self.objectWillChange.send() // << here !!
_ = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) {[weak self] _ in
self?.results[1].label = "Or later"
self?.objectWillChange.send() // << here !!
}
}
}