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!
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")
}
}
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.
Using Xcode-8.2.1, Swift-3.0.2, RealmSwift-2.2.0, iOS-Simulator-10:
Trying to write a View-Model with a Realm-Object, I fail at creating a returnArray in another thread. The issue is that the access to the previously created realm-object fails (most likely due to the background-thread access ??).
Can anybody tell me what is wrong with the following code (see below):
Important: It is given that the "createDataEntries()-method" is called before the "getEntries-completionHandler" (as can be seen with correct SimPholders realmobject-entry)! Therefore the "category" is set as "Love" (see code)
import Foundation
import RealmSwift
class MVVMCBalancesModel: BalancesModel
{
fileprivate var entries = [BalancesDataEntry]()
let realm = try! Realm()
init() {
self.createDataEntries()
}
fileprivate func createDataEntries() {
let myBalance = BalancesDataEntry()
myBalance.index = 0
myBalance.category = "Love" // !!!!!!! Here the category is filled
try! self.realm.write {
self.realm.deleteAll()
self.realm.add(myBalance)
}
}
func getEntries(_ completionHandler: #escaping (_ entries: [BalancesDataEntry]) -> Void)
{
// Simulate Aysnchronous data access
DispatchQueue.global().async {
var returnArray: [BalancesDataEntry] = [BalancesDataEntry]()
let realmy = try! Realm()
let cnt = realmy.objects(BalancesDataEntry.self).count
for idx in 0 ..< cnt {
let obj = realmy.objects(BalancesDataEntry.self).filter("index = \(idx)").first!
returnArray.append(obj)
}
completionHandler(returnArray) // !!!!!!! BREAKPOINT (see screenshot below)
}
}
}
Running the above code and setting a breakpoint at the completionHandler(returnArray) produces the following:
Why is the "category" of the returnArray an empty String ???
Properties of objects retrieved from a Realm are lazily retrieved from the underlying storage. Accessing the properties from Swift will return the appropriate values. Likewise, if you run po returnArray from Xcode's LLDB console you should see the object's complete state. The instance variables, shown in the debugger popover, are only used when the object is unmanaged (prior to being added to the Realm).
I set breakpoints all throughout this query to Firebase, and all that's happening is the breakpoint for the following line gets hit (which is the very first line), and then no others.
_CHAT_REF.observeEventType(.Value, withBlock: { snapshot in
Any idea why this would be happening? Even if there is no data, the breakpoints inside the query block should be still be getting hit, but they aren't. What I've done: I've uninstalled and reinstalled Firebase at least 10 times, using both CocoaPods and not CocoaPods, following directions to the T. I'm not getting any kind of compile error, and I'm running FIRApp.configure() in my app delegate.
Full Code (breakpoints on each line, none called only _CHAT_REF.observe line):
private var _CHAT_REF = FIRDatabase.database().reference().child("chats")
_CHAT_REF.observeEventType(.Value, withBlock: { snapshot in
self.individualMessages = []
if snapshot.hasChildren() {
// Found chats
for snap in snapshot.children {
let theChat = Chat(snapshot: snap as! FIRDataSnapshot)
// The current user belongs to this chat, so add it to individual messages.
if theChat.sender_id == GlobalEnv.currentUser.id || theChat.receiver_id == GlobalEnv.currentUser.id {
self.individualMessages.append(theChat)
}
}
} else {
// No Children
print("No children found.")
}
self.tvContacts.reloadData()
})
DB Structure:
DB Structure on Firebase
I ran into a similar problem. It turned out that I wasn't able to read/write the database from behind my organization's proxy server. I built to a device using open wifi, and it worked.
Try this and let me know if it makes a difference. It's very simplified but the variable assignments are handled differently.
assume you are in a ViewController class...
class ViewController: UIViewController {
var ref: FIRDatabaseReference!
override func viewDidLoad() {
super.viewDidLoad()
ref = FIRDatabase.database().reference()
}
func clickAction() { //just tied to an action in the UI
let chatsRef = ref.child("chats")
chatsRef.observeEventType(.Value, withBlock: { snapshot in
print("Hello, World")
})
}
}
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.