I have this class inherit from Object:
class Location: Object {
dynamic var id: String = ""
dynamic var name: String = ""
override class func primaryKey() -> String {
return "id"
}
}
This class is used as an instance inside my manager like this:
class LocationServiceAPI {
fileprivate var _location: Location?
var location: Location? {
get {
if _location == nil {
let realm = try! Realm()
_location = realm.objects(Location.self).first
}
return _location
}
set {
let realm = try! Realm()
if let newValue = newValue {
// delete previous locations
let locations = realm.objects(Location.self)
try! realm.write {
realm.delete(locations)
}
// store new location
try! realm.write {
realm.add(newValue, update: true)
_location = newValue
}
} else {
let locations = realm.objects(Location.self)
try! realm.write {
realm.delete(locations)
}
}
}
}
}
So whenever I get a location I delete the old one (new and old locations could be identical) and replace it with the new one, then I used the newValue as new value for the property _location but whenever I try to access the location it gives me 'Object has been deleted or invalidated'.
I am really confused since location will hold the value passed from the setter but not the realm!!
Note: If I stop the deleting then It will work fine.
The Object has been deleted or invalidated error will occur if an object has been deleted from a Realm, but you subsequently try and access a stored property of an instance of that object that your code was hanging onto since before the deletion.
You'll need to examine your logic paths and make sure there's no way you're deleting the location object, and not subsequently updating the _location property. There's no mention of deleting the object in the sample code you've provided, but your if let newValue = newValue line of code would mean that _location wouldn't actually get cleared if you passed in nil.
Finally, it's possible to manually check if an object has been deleted from a Realm by calling _location.invalidated, so if this happens a lot, it might be a good idea to include some extra checks in your code as well.
Without knowing really anything about your app and your design choices, it looks like you're trying to avoid reading/writing to the DB too often by caching the location property. Unless you're working with tons of LocationServiceAPI objects it shouldn't be a real performance penalty to actually read/write directly in the DB, like this :
class LocationServiceAPI {
var location: Location? {
get {
let realm = try! Realm()
return realm.objects(Location.self).first
}
set {
let realm = try! Realm()
if let newValue = newValue {
// store new location
try! realm.write {
realm.add(newValue, update: true)
}
} else {
// delete the record from Realm
...
}
}
}
}
Also, I would in general avoid keeping Realm objects along for longer periods, I don't say it's not possible but in general it leads to issues like you've experienced (especially if do multi-threading). In most cases I'd rather fetch the object from DB, use it, change it and save it back in the DB asap. If keeping references to specific records in the DB is necessary I'd rather keep the id and re-fetch it when I need it.
Related
I'm trying to port a little RSS feed reader app from UIKit over to SwiftUI, this app uses Realm for persistence.
In order to make Realm bindable in SwiftUI, I added the following code to my project:
import Foundation
import SwiftUI
import RealmSwift
final class FeedData: ObservableObject {
#Published var feeds: [Feed] {
didSet {
cleanRealm()
}
}
private var feedsToken: NotificationToken?
private func activateFeedsToken() {
let realm = try! Realm()
let feeds = realm.objects(Feed.self)
feedsToken = feeds.observe { _ in
self.feeds = Array(feeds)
}
}
func cleanRealm() {
let realm = try! Realm()
let tempFeeds = realm.objects(Feed.self)
let diff = feeds.difference(from: tempFeeds)
for change in diff {
switch change {
case .remove(_, let element, _):
do {
try realm.write {
print("Removing \(element.name)")
if element.isInvalidated {
print("Error: element invalidated.")
} else {
realm.delete(element)
print("Removed \(element.name)")
}
}
} catch {
fatalError(error.localizedDescription)
}
default:
break
}
}
}
init() {
let realm = try! Realm()
feeds = Array(realm.objects(Feed.self))
activateFeedsToken()
}
deinit {
feedsToken?.invalidate()
}
}
So, we have a feeds array with a DidSet observer that invokes the cleanRealm() function when triggered, which then uses collection diffing to remove Feed objects which are no longer in the array but still stored in Realm - I'm aware this is super clunky, but I figured it would at least keep the array in sync with the Realm database.
In my SwiftUI view, I then use FeedData as EnvironmentObject and use a List showing all Feed objects using ForEach.
When a user deletes a feed from the List, it is then removed from the array like so:
.onDelete(perform: deleteItems)
Which then calls this function:
func deleteItems(at offsets: IndexSet) {
print("Removing feeds at requested offsets.")
feedData.feeds.remove(atOffsets: offsets)
print("Removed feeds at requested offsets.")
}
Problem: when I run my app and then delete an entry from the list, the following exception is thrown:
* Terminating app due to uncaught exception 'RLMException', reason: 'Object has been deleted or invalidated.'
* First throw call stack:
(0x1a4a97ab0 0x1a47b1028 0x10098f6a8 0x100996324 0x1009962e8 0x10000dfa4 0x1b215d830 0x1b215d3f0 0x1b215d170 0x1b215f470 0x1db363c4c 0x1db36a0c8 0x1db36a240 0x1db36a6c0 0x1b22d647c 0x1db3ca2c4 0x1db3c9f04 0x1b21a924c 0x1b21a943c 0x1b21a9b50 0x1b22d647c 0x1daefcd50 0x1db3f0ac4 0x1db3eb7b8 0x1db3ead20 0x1db14dca4 0x1db14c5c0 0x1db4d5d0c 0x1db1bdc1c 0x1db1b836c 0x1db1bef70 0x1cf72c9c0 0x1cf713e9c 0x1cf714164 0x1cf719130 0x1db07f9f0 0x1db084e10 0x1db3ac770 0x1db07f96c 0x1cf719284 0x1db081ca0 0x1db081a48 0x1db0816c8 0x1db081834 0x1db3ac770 0x1db0817fc 0x1db09f848 0x1daf00a10 0x1daf00970 0x1daf00a8c 0x1a4a12668 0x1a4a0d308 0x1a4a0d8b8 0x1a4a0d084 0x1aec56534 0x1a8b7b8b8 0x10004e520 0x1a488ce18)
libc++abi.dylib: terminating with uncaught exception of type NSException
My research so far suggests that when a Realm object is deleted, it is mutated before being removed entirely.
So, I think what may be going on here is that when the object is mutated prior to being removed from Realm, SwiftUI detects this change, redraws the view and then tries accessing the now invalidated Realm object resulting in the exception being raised.
Realm objects do have a isInvalidated property that I should probably check before adding it to the List, but AFAIK (and please do feel free to correct me on this) there is no way in a ForEach block to check for such a condition and "continue" to the next array item if desired.
Any help with this is greatly appreciated, I've been messing with this issue all day and just cannot find a good solution, it's probably obvious but I'm also still learning about all the wonderful stuff SwiftUI can do while working on this sample project.
Thanks!
Update:
Following Jay's advice, I managed to modify my FeedData class so that it exposes a Realm Results property for my app to work with, instead of copying to a separate array.
That also means I no longer need to do any collection diffing when deleting feeds, but since SwiftUI's .onDelete(perform:) modifier expects a function that accepts an IndexSet, my delete function is still a bit of a hack.
The updated FeedData class now looks as follows:
import Foundation
import SwiftUI
import RealmSwift
final class FeedData: ObservableObject {
#Published var feeds: Results<Feed>
private var feedsToken: NotificationToken?
private func activateFeedsToken() {
let realm = try! Realm()
let feeds = realm.objects(Feed.self)
feedsToken = feeds.observe { _ in
self.feeds = feeds
}
}
func deleteItems(at offsets: IndexSet) {
print("deleteItems called.")
let realm = try! Realm()
do {
try realm.write {
offsets.forEach { index in
print("Attempting to access index \(index).")
if index < feeds.count {
print("Index is valid.")
let item = feeds[index]
print("Removing \(item.name)")
realm.delete(item)
print("Removed item.")
}
}
}
} catch {
fatalError(error.localizedDescription)
}
}
init() {
let realm = try! Realm()
feeds = realm.objects(Feed.self)
activateFeedsToken()
}
deinit {
feedsToken?.invalidate()
}
}
While that technically works, my issue now is that my view's ForEach() method will be called as soon as an object is deleted from the Realm (tested by using breakpoints), this happens even before the Realm notification is dispatched.
This then results in an attempt to access the index of the deleted object, in which case the app will crash with an index out of bounds error from Realm.
That should probably be a separate question though, as my original issue is resolved.
#Jay, can you please mark your first comment as an answer to my question so that I can approve it?
Thanks for the great help!
I ran into this problem, too. Not sure what's going on, some complicated multiple call behavior caused by SwiftUI, I think. When I expanded my notificationToken observation block and switch on the changes to return if a deletion is called, it works fine. In your case:
feedsToken = feeds.observe { [weak self] (changes: RealmCollectionChange) in
switch changes {
case .initial:
print("Initial call")
case .update(_, let deletions, _, _):
print("Updates made!")
if !deletions.isEmpty { return }
self.feeds = feeds
case .error(let error):
print("Error observing changes")
}
}
I am using RealmSwift to create a PIN code screen for an app. I have a manager class that has a few functions, including checkForExistingPin() which is intended to be used to check whether a pin exists (as the name suggests).
When I create an instance of the manager class and call the checkForExistingPin() function, it always tells me that there are 4 (It prints: "Optional(4)"), even though I have not created a pin yet.
Can anyone explain why this might be doing this and how I might get the correct output from the code?
Here is the class:
import Foundation
import RealmSwift
class pinCode: Object {
#objc dynamic var pin = ""
}
protocol pinCodeManager {
func checkForExistingPin() -> Bool
func enterNewPin(newPin:String)
func checkPin(pin:String) -> Bool
}
class manager:pinCodeManager {
let realm = try! Realm()
func checkForExistingPin() -> Bool {
let existingCode = realm.objects(pinCode.self).first?.pin
print("\n\nNumber of existing PINs: ", existingCode?.count as Any, "\n\n") // Number of existing PINs: Optional(4)
if existingCode?.count == 0 {
return false
}
else {
return true
}
}
func enterNewPin(newPin:String) {
if checkForExistingPin() {
let oldCode = realm.objects(pinCode.self).first
try! realm.write {
oldCode!.pin = newPin
}
}
let newPinObject = pinCode()
newPinObject.pin = newPin
realm.add(newPinObject)
}
func checkPin(pin:String) -> Bool {
if checkForExistingPin() {
print ("Realm object first: ", realm.objects(pinCode.self).first?.pin as Any)
if pin == realm.objects(pinCode.self).first?.pin {
print ("Pin Correct")
return true
}
else {
print ("Pin Incorrect")
return false
}
}
print ("No existing pin")
return false
}
}
And here is the relevant code snippet of the ViewController:
class InitialViewController: UIViewController {
let myPin = pinCode()
let myManager = manager()
let realm = try! Realm()
#IBAction func NewUserButton(_ sender: Any) {
print("No existing PINs: ", self.myManager.checkForExistingPin())
}
The output is : Number of existing PINs: Optional(4)
You must have created a pinCode object (or multiple of them). "Optional(4) doesn't mean you have created 4 pins. You are counting String. It means that the object you retrieved has a 4 digit pin. If you haven't created any pinCode object, you should get nil. Or if you have created one without assigning a pin, you should get 0.
I recommend your looking at your realm file. You should be able to print out its location this way:
print(Realm.Configuration.defaultConfiguration.fileURL!)
You can then open the file with Realm Studio and verify what is in there.
You have a few things going on here:
Although this is not really in the scope of the question, here's a tip for the future. Your types' names should be capitalized (following CamelCase standard), as per Swift API Design Guidelines. Thus, your pinCodes and manager classes and pinCodeManager protocol should be called PinCode, Manager and PinCodeManager respectively.
Assuming you renamed your types and as other users pointed out, you're not counting instances of PinCode. You're counting the length of the pin member of PinCode class. Refactoring your checkForExistingPin() function:
func checkForExistingPin() -> Bool {
return realm.objects(pinCode.self).count > 0
}
In your enterNewPin(newPin:) function, in the case you already have a PinCode object stored, note that you are actually updating the old PinCode and adding a new one with the same pin. For instance, if you previously have a PinCode object stored with pin=1234. After calling enterNewPin(newPin: "5678") you will have two such objects stored with the pin=5678. You might want to refactor that as well:
func enterNewPin(newPin:String) {
if checkForExistingPin() {
let oldCode = realm.objects(pinCode.self).first
try! realm.write {
oldCode!.pin = newPin
}
} else {
let newPinObject = pinCode()
newPinObject.pin = newPin
try! realm.write {
realm.add(newPinObject)
}
}
}
Before trying to do any debugging in your app. I recommend you first uninstalling and then reinstalling the app wherever you running (simulator or actual device). If things keep behaving weird, that's probably something related with your configuration if you're overriding the default one (i.e. I noticed that you just used try! Realm() for retrieving a Realm, but you might have overridden Realm.Configuration.defaultConfiguration somewhere else).
Hope that helps!
As I try to update an existing entry in my Core Data DB, I fetch the desired item by id, change it to a new item and save in context.
However, when I fetch the object and replace it, I get the warning "Core Data Object was written to, but never read." It does make sense since I'm not really using that object, but as I understand it, just giving it a value saves it in Core Data.
static var current: User? {
didSet {
if var userInCoreData = User.get(with: current?.id), let current = current { //userInCoreData is the value with the warning
userInCoreData = current
}
CoreDataManager.saveInContext()
}
}
static func get(with id: String?) -> User? {
guard let id = id else { return nil }
let request: NSFetchRequest = User.fetchRequest()
let predicate = NSPredicate(format: "id = %#", id)
request.predicate = predicate
do {
let users = try CoreDataManager.managedContext.fetch(request)
return users.first
} catch let error {
print(error.localizedDescription)
return nil
}
}
I want to make sure, is this the recommended process to overwrite a value in Core Data, or am I doing something wrong?
This section
if var userInCoreData = User.get(with: current?.id), let current = current { //userInCoreData is the value with the warning
userInCoreData = current
}
seems just updating local variable userInCoreData, not User object in Core Data.
So the warning says "you fetched data from core data and set to a variable, but you set another value to the variable soon, never use the first value from core data. Is it OK?"
What you really want to do is something like this?
if var userInCoreData = User.get(with: current?.id), let current = current {
userInCoreData.someValue = current.someValue
userInCoreData.anotherValue = current.anotherValue
}
Imagine this code:
class StoredVersions: Object{
#objc dynamic var minimumAppVersion = 0.0
#objc dynamic var sets = 0.0
}
class LoadViewController: UIViewController {
let realm = try! Realm()
override func viewDidLoad() {
super.viewDidLoad()
let db = Firestore.firestore()
var newestVersions = StoredVersions()
if let resultsStoredVersion = self.realm.objects(StoredVersions.self).first{
print("found stored versions: \(resultsStoredVersion)")
self.storedVersions = resultsStoredVersion
}else{
try! self.realm.write {
print("no stored versions")
self.realm.add(self.storedVersions)
}
}
db.collection("data").document("version").getDocument(completion: { (data, someError) in
if let versions = data.flatMap({StoredVersions(value: $0.data()) }) {
try! self.realm.write {
self.storedVersions = versions
}
}
})
}
storedVersions is updated but when I restart the application, storedVersions is back to its initial state. I do see the print "found stored versions".
If I write just 1 variable at a time, it works. That looks like this:
try! self.realm.write {
self.storedVersions.sets = versions.sets
}
How can I update a whole class without having to put in variables one at a time?
When you do this:
if let versions = data.flatMap({StoredVersions(value: $0.data()) }) {
try! self.realm.write {
self.storedVersions = versions
}
}
You're creating a new, unmanaged StoredVersions object. You need to call realm.add(_:) to add it to the Realm, otherwise the object only exists in memory.
If you want to update the existing StoredVersions object rather than creating a new one, you should instead use Realm.add(_:update:), specifying true for the update argument. Note that this requires your type have a primary key property declared so that Realm knows which existing object to update.
I am new to Realm and its the first time I am using it. I followed every step from the guide and its inserted in my project just fine. I created a model and a function to insert the object into the realm database.
Somehow I keep getting errors. Here is what I do.
my function
do {
let realm = try Realm()
let proposition = Proposition()
proposition.name = (currentProposition.name)
proposition.energyType = (currentProposition.energyType)
proposition.lifetime = (currentProposition.lifetime)
proposition.saving = (currentProposition.saving)
proposition.investing = (currentProposition.investing)
if let _ = propositionsArray.indexOf(proposition) {
try! realm.write {
realm.delete(proposition)
loadPropositions()
}
} else {
try! realm.write {
realm.add(proposition)
loadPropositions()
}
}
} catch let error as NSError {
print("Add proposition error \(error)")
}
Here is my model
import RealmSwift
import Foundation
class Proposition : Object {
dynamic var name: String = ""
dynamic var energyType: String = ""
dynamic var lifetime = 0
dynamic var saving = 0
dynamic var investing = 0
}
Somehow I keep getting the following error
Can someone tell me what I am doing wrong?
The errors you're seeing indicate that the data model defined by your application does not match the data model of the Realm you're opening. This is usually due to changing your data model. In this case, the errors mention that you've added the lifetime, saving, and investing properties, and changed name and energyType to be non-nullable.
There are two ways to accommodate changes to your data model:
If you're in early development and don't need to support your old data model, you can simply remove the Realm files and start over with empty data.
You can perform a migration to have Realm update the data model of the Realm file. See the Migrations section of the Realm documentation for information about how to perform a migration.