Realm write transaction failing, despite being in transaction - swift

I have this code, which should append messages with a new message:
func addMessage(_ message: Message) {
do {
try Realm().write {
self.messages.append(message)
}
} catch let error {
print("could not add message due to error:\n\(error)")
}
}
However, I get an exception Cannot modify managed RLMArray outside of a write transaction It doesn't make any sense to me, because I'm already in a write transaction...

You need to create a Realm object before applying the write module.
According to the GitHub documentation, you can try code like this:
func addMessage(_ message: Message) {
do {
let realm = try! Realm()
try! realm.write {
self.messages.append(message)
}
} catch let error {
print("Could not add message due to error:\n\(error)")
}
}
Hope it helps!

You can use let realm = try! Realm() with a custom default configuration by setting a default configuration see here:
var config = Realm.Configuration()
// Set this as the configuration used for the default Realm
Realm.Configuration.defaultConfiguration = config

The trouble was I was using a plain Realm object with no special configuration. Since I am using Realm Mobile Platform, I needed to create a Realm object with the same config each time I want to write to that DB:
let configuration = Realm.Configuration(
syncConfiguration: SyncConfiguration(user: user, realmURL: URL(string: "realm://127.0.0.1:9080/~/speciail")!)
)
self.realm = try! Realm(configuration: configuration)
//now do the write transaction!
It took a bit of refactoring, but I have it now. My thanks to those of you who took the time to help me.

Related

Reading Data from Realm Database (Swift)

I am new to Realm DataBase and I need a way to read data from realmCloud, but from two different app projects. The way I have tried to implement this is by using query-synced realm. At the moment I'm using a singe realm user to write the data in one app, and the same realm user to read data from another app. The problem is that making a query from the second app(the one used for reading) doesn't return any realm objects ( I have also noticed that user identifier is different from the first one, and also the user permissions are nil.
I have tried setting permissions directly from RealmStudio since documentation is not precise on how to set them from code
func openRealm() {
do {
realm = try Realm(configuration: SyncUser.current!.configuration())
let queryResults = realm.objects(*className*.self)
let syncSubscription = queryResults.subscribe()
let notificationToken = queryResults.observe() { [weak self] (changes) in
switch (changes) {
case .initial: print(queryResults)
case .error(let error): print(error)
default: print("default")
}
}
for token in queryResults {
print(token.tokenString)
}
syncSubscription.unsubscribe()
notificationToken.invalidate()
} catch {
print(error)
}
}
This function prints the data in one app project, but used in another app project with the same user logged in, and the same classFile referenced in the project, it does not. (note that SyncUser.current.identifier is different also
There are a couple of issues.
Some of these calls are asynchronous and the code in your question is going out of scope before the data is sync'd (retreived). The bottom line is code is faster than the internet and you need to design the flow of the app around async calls; don't try to work with the data until it's available.
For example
let notificationToken = queryResults.observe() { [weak self] (changes) in
//here is where results are fully populated
}
// this code may run before results are populated //
for token in queryResults {
print(token.tokenString)
}
Also, let notificationToken is a local var and goes out of scope before the results are populated as well.
These issues are super easy to fix. First is to keep the notification token alive while waiting for results to be populated and the second is to work with the results inside the closure, as that's when they are valid.
var notificationToken: NotificationToken? = nil //a class var
func openRealm() {
do {
let config = SyncUser.current?.configuration()
let realm = try Realm(configuration: config!)
let queryResults = realm.objects(Project.self)
let syncSubscription = queryResults.subscribe(named: "my-projects")
self.notificationToken = queryResults.observe() { changes in
switch changes {
case .initial:
print("notification: initial results are populated")
queryResults.forEach { print($0) }
case .update(_, let deletions, let insertions, let modifications):
print("notification: results, inserted, deleteed or modified")
insertions.forEach { print($0) } //or mods or dels
case .error(let error):
fatalError("\(error)")
}
}
} catch {
print(error)
}
}
deinit {
self.notificationToken?.invalidate()
}
The other advantage of keeping that token (and its corresponding code) alive is when there are further changes, your app will be notified. So if another project is added for example, the code in the 'changes' section will run and display that change.

Catching RealmSwift write errors i.e. delete

How exactly do I catch RealmSwift add/delete errors?
i.e.
do {
try realm.write {
realm.delete(MyRealmObject())
}
completion(true)
} catch {
completion(false)
}
In this example I've deliberately tried deleting a RealmObject that I've just made up (to make it fail), but it doesn't catch, instead I get this error:
Terminating app due to uncaught exception 'RLMException', reason: 'Can only delete an object from the Realm it belongs to.'
I haven't seen any examples of people handling specific delete/add errors - is there a reason for this?
Not sure if this is what you're looking for but the following code catches and identifies specific add errors. However, errors accessing different instances of realm are handled differently. So starting with this...
class MyObject: Object {
#objc dynamic msg = ""
}
do {
let realm = try Realm()
let a0 = MyObject()
a0.msg = "Hello, World"
try! realm.write {
realm.add(a0)
}
} catch let error as NSError {
print(error.localizedDescription)
}
to then expand.... do the following to ensure the object being deleted matches the realm you are deleting it from.
do {
let realm = try Realm()
if a0.realm == realm {
try! realm.write {
realm.delete(a0)
}
}
} catch let error as NSError {
print(error.localizedDescription)
}
Realm developers extend NSError class with their custom description Can only delete an object from the Realm it belongs to and do FatalError in the predefined list of errors they expect ( may be for a good use of the their library as a help to the developer to correctly use it ) , so they intentionally don't throw an exception that the try can catch

SyncCredentials Don't exist in Realm Swift

Im trying to connect my app to a Realm Object Server. The documentation says, to do this you use the below:
// create a configuration object
let realmUrl = URL(string: "realms://example.com:9000/~/settings")!
let realmUser = SyncCredentials.usernamePassword(username: username, password: password)
let config = Realm.Configuration(user: realmUser, realmURL: realmUrl)
// open the Realm with the configuration object
let settingsRealm = try! Realm(configuration: config)
However for SyncCredentials.usernamePassword, XCode says SyncCredentials doesn't exist. From the looks you need to set SyncConfiguration on in Realm.configuration (or the only file I can find RealmConfiguration.swift)
Now I'm in that file theres no option to use SyncCredentials
My question is, how do I simply connect my app with a Realm Object Database using SyncCredentials (or however you're supposed to do it). Been pulling my hair out all day over this, surely it can't be that hard :-(
There seem some mistakes.
SyncCredentials is auth info that is used to log in. It is not user object. Realm.Configuration doesn't receive user and realmURL parameters in the initializer. You need to use SyncConfiguration instead.
The example code for logging in or instantiating Realm with existing user is the following.
let syncServerURL = URL(string: "realm://example.com:9080/~/settings")!
let syncAuthURL = URL(string: "http://example.com:9080")!
if let user = SyncUser.current {
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: syncServerURL))
let realm = try! Realm(configuration: config)
...
} else {
let username = ...
let password = ...
let credentials = SyncCredentials.usernamePassword(username: username, password: password)
SyncUser.logIn(with: credentials, server: syncAuthURL) { user, error in
DispatchQueue.main.async {
if let user = user {
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: syncServerURL))
let realm = try! Realm(configuration: config)
...
} else {
// Handle error
}
}
}
}
Please read our documentation again https://realm.io/docs/swift/latest/#sync
and also our RealmTasks sample project helps you to understand interacting Realm Object Server. https://github.com/realm-demos/realm-tasks
If you still see SyncCredentials not found error in above code, probably you didn't setup RealmSwift framework correctly. Please add more info that how did you set up Realm.
Swift Package Manager doesn't load SyncCredentials or SyncUser properly.
I was using the SPM to install RealmSwift as a dependency from IceCream. I removed Realm and IceCream from SPM. Then installed using Carthage to solve the issue.

Object has been deleted or invalidated realm

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.

Realm writes are blocking main thread

I have a json file on a remote server that I'm grabbing data from (36k records) using dataTaskWithURL. I'm the resulting JSON (SwiftyJSON) is returned. Below is my completion handler.
The problem is that as soon as I start rolling through the realm create's, all my other callbacks stop executing until the realm commit fires. My desired outcome is that this task simply runs and inserts the data in the background allowing the user to continue on their merry way while this rolls.
The blocking seems to happen once the realm.beginWrite() fires.
RemoteAPI().getMetafile({JSONData, error -> Void in
if (JSONData != nil) {
do {
print("***** Loading realm")
let realm = try Realm()
realm.beginWrite()
for (_, subJSON) in JSONData {
realm.create(Meta.self, value: ["xxxxxx": subJSON["xxxxx"].int!, "xxxxx": subJSON["xxxxx"].stringValue, "xxxxx": subJSON["xxxxx"].stringValue, "xxxxxx": subJSON["xxxxxx"].int!, "xxxxxx": subJSON["xxxxx"].stringValue, "xxxxx": subJSON["xxxxx"].stringValue, "xxxxxx": subJSON["xxxxx"].stringValue], update: true)
}
try realm.commitWrite()
print("***** Finished loading realm")
} catch _ {}
} else {
print("api data fetch failed")
print(error)
}
})
While the above call is happening, I have another call:
RemoteAPI().getLatestActivityData({JSONData, error -> Void in
if (JSONData != nil) {
// do stuff
dispatch_async(dispatch_get_main_queue(), {
NSNotificationCenter.defaultCenter().postNotificationName("refreshTableView", object: nil)
})
}
})
Both of these calls fire from the app delegate. The problem however, is that the observer in this second call does not fire until after the first call above completes.
You should perform the transaction in the background if you don't want to block the main thread.
...
do {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
let realm = try! Realm()
realm.beginWrite()
for (_, subJSON) in JSONData {
realm.create(Meta.self, value: [...], update: true)
}
try realm.commitWrite()
}
}
...
See Realm docs about threading.
The issue had nothing to do with thread blocking. Even though I was running the requests asynchronously something I figured out is if I have Request A and Request B running asynchronously and they both start trying to do Realm writes. They basically get treated as serial requests and the writes will be queued up until the initial writes are completed.
As noted in the documentation (not in the threads section) -- Realm writes will block each other. They note that you can use threading to resolve this, but even using the dispatch_async methods, the writes continued to block each other.
However what I realized was that the initial request is simply loading data to be used for an in-app search and not for anything else. The rest of the system uses Realm for storing user specific data.
My Solution
Request A represents my search meta data request populating, for that I created a new realm file:
let config = Realm.Configuration(
path: utility.getDocumentsDirectory().stringByAppendingPathComponent("Meta.realm"),
readOnly: false)
let realm = try! Realm(configuration: config)
Request B Which was writing all my user data, and then triggering a tableview refresh I had read/write from the default realm.
As soon as I implemented this change in usage, no more blocking and everything worked correctly and as expected.
Honestly I don't know if this is best practice for Swift/iOS or not (I'm not a Swift developer) but it works, and the performance is quite acceptable.
You can try this:
for (_, subJSON) in JSONData {
realm.beginWrite()
realm.create(Meta.self, value: ["xxxxxx": subJSON["xxxxx"].int!, "xxxxx": subJSON["xxxxx"].stringValue, "xxxxx": subJSON["xxxxx"].stringValue, "xxxxxx": subJSON["xxxxxx"].int!, "xxxxxx": subJSON["xxxxx"].stringValue, "xxxxx": subJSON["xxxxx"].stringValue, "xxxxxx": subJSON["xxxxx"].stringValue], update: true)
try realm.commitWrite()
}
I don't know why, but it works for me.