Realm, Cannot register notification blocks from within write transactions - ios

I'm using a function to save a realm object to my server. It's called using DataManager().updatedCategory(~InsertCategory~). The implementation goes like this;
func updatedCategory(_ c: Category) {
guard let realm = try? Realm() else { return }
guard let category = realm.object(ofType: Category.self, forPrimaryKey: c.id) else { return }
do {
try realm.write {
c.updatedAt = Date()
}
} catch {
print(error)
}
dbCategories.document(category.id).updateData(category.dictionaryValue())
}
This function is being called after using a realm.write block. It's not in another write block and even the UI is being updated before this is called. I can't figure out what's causing this error, since there's no notification being registered.

I think this
try realm.write {
c.updatedAt = Date()
}
should be
try realm.write {
category.updatedAt = Date()
}
also, this
dbCategories.document(category.id).updateData(category.dictionaryValue())
looks like you're also attempting to write data to realm (?). If so, it needs to be within a write closure. If not, clarify what that does so I can update the answer.
Lastly, this is a bit unclear
This function is being called after using a realm.write block.
You can't embed a write closure within another, and adding observers should occur outside of a write closure as well - the error indicates the observer is being attached within a closure

Related

How to properly loop through a Firebase Query with completion handlers

Problem:
I need to loop through a Firebase query call.
My loop is initiated before the Firebase call as it holds the uids needed.
for uid in following_uids {
}
What is the proper way to loop variable uid into the Firebase reference & query?
for uid in following_uids {
let fbRef = ref.child("users").child(uid).child("posts")
//query firebase call
}
Replication:
func getRecentPostsFromFollowers(following_uids: [String]) {
for uid in following_uids {// first loop
let fbRef = ref.child("users").child(uid).child("posts")
fbRef.queryOrderedByKey().queryLimited(toLast: 5).observeSingleEvent(of: .value, with: {snapshot in
if(snapshot.exists()){
let values = snapshot.children.compactMap { ($0 as? DataSnapshot)?.value }
for postData in values {
guard let restDict = (postData as AnyObject) as? [String: Any] else { continue }
//do with data
}
}
})
}//end first loop
print("Completion handler - Loop Done")
}
PSEUDO code:
func getFollowerUIDS(completion: #escaping (_ followers: [String]) -> Void) {
for uid in following_uids {
let fbRef = ref.child("users").child(uid).child("posts")
//query firebase call
}
completion(value)
}
Specs:
Xcode Version 14.2 (14C18)
iOS
I'm assuming you'd like to combine the results from all user posts and return them as a single array of posts.
When you call observeSingleEvent on your document reference, the second parameter is a type of closure. This is essentially just another function that the Firebase SDK will call when it has the data ready for you.
The idea is that: because it may take some time to fetch this data, we don't want to block the rest of your function/code from running while the network call is taking place.
This means you will likely see the end of the loop "Completion handler - Loop Done" called before your data is made available.
This means your getRecentPostsFromFollowers function will return before any of your closures are called with data.
In order to allow callers of your getRecentPostsFromFollowers function to know when the data is ready, you can add your own completion handler to provide this data back to them.
This is a closure that you will call when you know all the data is ready.
However, because there's multiple closures with data that we need to combine, we can use something like a DispatchGroup for this purpose.
We'll combine the posts in an allPosts variable and return the data when it's combined from all the requests.
We need to lock around the access to allPosts as observeSingleEvent's completion handler can run on any thread, and multiple threads could try to access this variable at once.
typealias PostData = [String: Any]
func getRecentPostsFromFollowers(following_uids: [String], completion: #escaping ([PostData]) -> Void) {
let group = DispatchGroup()
let lock = NSLock()
var allPosts = [PostData]()
for uid in following_uids {
let fbRef = ref.child("users").child(uid).child("posts")
group.enter()
fbRef.queryOrderedByKey().queryLimited(toLast: 5).observeSingleEvent(of: .value, with: {snapshot in
defer { group.leave() }
if(snapshot.exists()){
let values = snapshot.children.compactMap { ($0 as? DataSnapshot)?.value }
lock.lock()
defer { lock.unlock() }
for postData in values {
guard let restDict = (postData as AnyObject) as? PostData else { continue }
allPosts.append(restData)
}
}
})
}
group.notify(queue: .main) {
completion(allPosts)
}
}
Side Notes
Swift async/await is a more modern way to handle asynchronous-based tasks like this without some of the pitfalls in this solution, it's worth looking into.
Performing a separate Firestore query for each user to retrieve all their data is not very efficient.
This is because each query requires a separate network request and will cost you at least one document read per query made, regardless if there are results or not per-user.
You should consider structuring your data within your Firestore database so you can return all the data you require in a single query.
This may involve denormalizing some of your data to allow for more efficient queries.

Realm Swift how to add observer to object type (nil)

Assume i have two flows:
1) I have data in database, then i use fetch entity with params from database and set and observer. After that i load data from server and observer's block fires successfully. That's fine.
2) I don't have data in database. Then i try to do the same, it looks like:
myObject = MyRealmService()
.fetchAll(MyRealmObject.self,
filter: "userID == \(someID)")?
.first
realmToken = myObject?.observe { [weak self] change in
guard let _self = self else { return }
switch change {
case .deleted:
_self.popCurrentViewController()
case .error(let error):
_self.show(error: error)
case .change:
_self.updateUI()
}
}
loadDataFromServer() { object in
object.saveToRealm()
}
Then myObject is nil, so the notification block don't setting.
What is the way to handle notifications in this way? I mean, maybe somehow we can set the block to the filter type (MyRealmObject.self, filter: "userID == \(someID)"), so if in realm has write the object that fits it, then the observe block fires?
Instead of observing individual object which is still not stored in Realm database, you could observe Realm Results
let realm = try! Realm()
var results = realm.objects(MyRealmObject.self).filter: "userID == \(someID)")
var notificationToken = results.observe { change in
switch change {
case .update:
DispatchQueue.main.async {
block()
}
default: ()
}
}
You can observe the result object instead of individual and do some action based on changes.

Re-initializing a Realm object in a different thread

Is running a query the only way of re-initializing a Realm Object for use in a different thread? Is there no way to somehow grab some kind of reference for use with Realm, so that the query doesn't have to be completely from scratch? It would be nice to have some kind of guarantee that the object is for the same record (if it exists and the query for that "ref" is successful), without having to add a primary key just for this one purpose.
This outlines my situation:
func construct(name: String, tokens: [String]) -> Document {
let doc = Document()
let realm = try! Realm()
try! realm.write {
realm.add(doc)
}
DispatchQueue.global(qos: .userInitiated).async {
let realm = try! Realm()
// Some long running task to convert `tokens` into `[Blurb]`
// var blurbs: [Blurb]
// What's the recommended way of re-initing that new `Document`?
// let sameDoc = .....
try! realm.write {
sameDoc.blurbs.append(blurbs)
}
}
return doc
}

RealmSwift: Detach an object from Realm, including its properties of List type

I want to create a duplicate of a persisted object such that the new instance has all the same values but is not attached to Realm. Using Object(value: persistedInstance) works great for classes whose properties are all strings, dates, numbers, etc. However, when duplicating an instance of a class with List type properties, the duplicate's List and the List's elements continue to reference the persisted records. How can I create a duplicate that's fully detached from Realm, including any Lists and elements in those Lists?
You can make a deep copy of your object via the following extension functions:
import UIKit
import Realm
import RealmSwift
protocol RealmListDetachable {
func detached() -> Self
}
extension List: RealmListDetachable where Element: Object {
func detached() -> List<Element> {
let detached = self.detached
let result = List<Element>()
result.append(objectsIn: detached)
return result
}
}
#objc extension Object {
public func detached() -> Self {
let detached = type(of: self).init()
for property in objectSchema.properties {
guard
property != objectSchema.primaryKeyProperty,
let value = value(forKey: property.name)
else { continue }
if let detachable = value as? Object {
detached.setValue(detachable.detached(), forKey: property.name)
} else if let list = value as? RealmListDetachable {
detached.setValue(list.detached(), forKey: property.name)
} else {
detached.setValue(value, forKey: property.name)
}
}
return detached
}
}
extension Sequence where Iterator.Element: Object {
public var detached: [Element] {
return self.map({ $0.detached() })
}
}
Use
/// in collections
let data = realm.objects(AbcDfg.self).detached
/// single object
data.first?.detached()
This is not yet supported natively by Realm, but a requested feature tracked by issue #3381.
For now, you would need to implement your own deep copy constructor. A common strategy is to do that on every model and call the deep copy constructors of related objects. You need to pay attention though that you don't run into cycles.
We use ObjectMapper to create a deep copy of the object by turning into JSON and then turn that JSON back into same object except it's not associated with Realm.
Mike.
As mentioned in the issue tracked at #3381, the solution for now is an implementation to create detached copies from Realm objects.
There is a better version of the detachable object implementation at
https://github.com/realm/realm-cocoa/issues/5433#issuecomment-415066361.
Incase the link doesnot work, the code by Alarson93 is:
protocol DetachableObject: AnyObject {
func detached() -> Self
}
extension Object: DetachableObject {
func detached() -> Self {
let detached = type(of: self).init()
for property in objectSchema.properties {
guard let value = value(forKey: property.name) else { continue }
if property.isArray == true {
//Realm List property support
let detachable = value as? DetachableObject
detached.setValue(detachable?.detached(), forKey: property.name)
} else if property.type == .object {
//Realm Object property support
let detachable = value as? DetachableObject
detached.setValue(detachable?.detached(), forKey: property.name)
} else {
detached.setValue(value, forKey: property.name)
}
}
return detached
}
}
extension List: DetachableObject {
func detached() -> List<Element> {
let result = List<Element>()
forEach {
if let detachable = $0 as? DetachableObject {
let detached = detachable.detached() as! Element
result.append(detached)
} else {
result.append($0) //Primtives are pass by value; don't need to recreate
}
}
return result
}
func toArray() -> [Element] {
return Array(self.detached())
}
}
extension Results {
func toArray() -> [Element] {
let result = List<Element>()
forEach {
result.append($0)
}
return Array(result.detached())
}
}
In Realm 5.0.0 freeze() method has been added.
according to release notes:
Add support for frozen objects. Realm, Results, List and Object now have freeze() methods which return a frozen copy of the object. These objects behave similarly to creating unmanaged deep copies of the source objects. They can be read from any thread and do not update when writes are made to the Realm, but creating frozen objects does not actually copy data out of the Realm and so can be much faster and use less memory. Frozen objects cannot be mutated or observed for changes (as they never change). (PR #6427).
Previously answered here
As of now, Dec 2020, there is not proper solution of this issue. We have many workarounds though.
Here is the one I have been using, and one with less limitations in my opinion.
Make your Realm Model Object classes conform to codable
class Dog: Object, Codable{
#objc dynamic var breed:String = "JustAnyDog"
}
Create this helper class
class RealmHelper {
//Used to expose generic
static func DetachedCopy<T:Codable>(of object:T) -> T?{
do{
let json = try JSONEncoder().encode(object)
return try JSONDecoder().decode(T.self, from: json)
}
catch let error{
print(error)
return nil
}
}
}
Call this method whenever you need detached / true deep copy of your Realm Object, like this:
//Suppose your Realm managed object: let dog:Dog = RealmDBService.shared.getFirstDog()
guard let detachedDog = RealmHelper.DetachedCopy(of: dog) else{
print("Could not detach Dog")
return
}
//Change/mutate object properties as you want
detachedDog.breed = "rottweiler"
As you can see we are piggy backing on Swift's JSONEncoder and JSONDecoder, using power of Codable, making true deep copy no matter how many nested objects are there under our realm object. Just make sure all your Realm Model Classes conform to Codable.
Though its NOT an ideal solution, but its one of the most effective workaround.

Why does Realm use try! in Swift?

Why Does Realm use try! so frequently? It seems like if you're certain your call won't fail then you should not design it to throw - no?
Here is an example, from the Swift page on realm.io:
// Get the default Realm
let realm = try! Realm()
or
// Persist your data easily
try! realm.write {
realm.add(myDog)
}
To me this implies they will never fail so why have the constructor or write() throw?
If you're referring to the examples in the Realm Swift Docs, I suspect try! is used liberally for the sake of brevity. The user is given a quick and dirty overview of core concepts without too much mental overhead.
You probably will encounter errors at some point in your journey using Realm. You'll notice later on in the docs, in the Realms > Error Handling section that a do-catch example is given.
do {
let realm = try Realm()
} catch let error as NSError {
// handle error
}
To me, it's implied that the code examples from the docs are not necessarily production-quality, and the user is encouraged to use the relevant error-handling features of Swift.
From the Realm Swift 2.1.0 guide in the Writes section:
Because write transactions could potentially fail like any other disk
IO operations, both Realm.write() & Realm.commitWrite() are marked as
throws so you can handle and recover from failures like running out of
disk space. There are no other recoverable errors. For brevity, our
code samples don’t handle these errors but you certainly should in
your production applications.
Source: https://realm.io/docs/swift/latest/#writes
The way I deal with this issue is by creating a DatabaseManager class, that handles the unlikely event of realm throwing an error:
public class DatabaseManager {
static var realm: Realm {
get {
do {
let realm = try Realm()
return realm
}
catch {
NSLog("Could not access database: ", error)
}
return self.realm
}
}
public static func write(realm: Realm, writeClosure: () -> ()) {
do {
try realm.write {
writeClosure()
}
} catch {
NSLog("Could not write to database: ", error)
}
}
}
Thanks to that solution the code looks much cleaner whenever I want to read from realm or write to db :)
DatabaseManager.write(realm: realm) {
let queryResult = self.realm.objects(Cookies.self).filter("cookieId == %#", cookieId)
let cookie = queryResult.first
cookie?.expirationDate = expirationDate as NSDate?
}
Why creating a class with static func when we can create an extension of Realm?
extension Realm {
static func safeInit() -> Realm? {
do {
let realm = try Realm()
return realm
}
catch {
// LOG ERROR
}
return nil
}
func safeWrite(_ block: () -> ()) {
do {
// Async safety, to prevent "Realm already in a write transaction" Exceptions
if !isInWriteTransaction {
try write(block)
}
} catch {
// LOG ERROR
}
}
}
Usage Example
Old unsafe code:
let realm = try! Realm()
try! realm.write {
// Your write transaction body
}
Safety refactor with this extension:
guard let realm = Realm.safeInit() else {
// Track Error
return
}
realm.safeWrite {
// Your write transaction body as before
}
From the Realm documentation:
You may have noticed so far that we have initialized access to our realm variable by calling Realm(). That method returns a Realm object that maps to a file called “default.realm” under the Documents folder (iOS) or Application Support folder (OS X) of your app.
Any time you interact with the file system you risk encountering errors such as permissions problems or insufficient disk space. Success is not certain.
So if for any reason Realm is unable to create or write to the realm file, these methods you cite would indeed throw an exception.
I create this for simple init call
import RealmSwift
// MARK: - RealmDB
/// RealmDB import realm in foundation, and add is format for refactoring catch
public class RealmDB {
/// Realm
public static var realm: Realm? {
do {
return try Realm()
} catch let error {
NotificationCenter.default.post(name: .logError, object: "Could not access database: \(error)")
return nil
}
}
/// Write in Realm
///
/// - Parameter writeClosure: Write Closure
public static func write(writeClosure: #escaping (_ realm: Realm) -> ()) {
do {
try self.realm?.write {
// self.realm has so can `!`
writeClosure(self.realm!)
}
} catch let error {
NotificationCenter.default.post(name: .logError, object: "Could not write database: \(error)")
}
}
}

Resources