Query Based Synced Realm LinkingObjects returning nil - swift

All the solutions I have found online are for local Realms, not synced Realms (I am using Query based sync). How to do it right for a synced Realm?
I have a Shop object and an Item object. Shop has many items. User can browse the items and should see which shop that item belongs to. Pretty simple and straight forward scenario.
In the Shop class I have:
let items = List<Item>()
and in the Item class I have
let shops = LinkingObjects(fromType: Shop.self, property: "items")
var shop: Shop? { return shops.first }
The Realm query is like this:
private var realm : Realm!
private var subscriptionToken : NotificationToken?
private var syncSubscription : SyncSubscription!
...
...
...
let items = realm.objects(Item.self)
syncSubscriptionItem = items.subscribe()
subscriptionTokenItem = syncSubscriptionItem.observe(\.state, options: .initial) { state in
if state == .complete {
let shopName = items[0].shop?.name // Shop is always nil
}
}
I can see shop only if the shop's owner has logged into the app, which means Realm has synced shop information to local Realm. But for users on other devices, shops never get synced. But how to sync shops for all other users via this type of back linking?
Screenshot is attached to clarify what I mean:

I took a minute and wrote some code based on yours. The code in the question was not complete so I added all of the other elements to the code to paint a more complete picture. Theres really no error checking here so make sure you add that. This is working for me and outputs all of the items associated shops
var items: Results<Item>? = nil
var notificationToken: NotificationToken? = nil
var subscriptionToken: NotificationToken? = nil
var subscription: SyncSubscription<Item>!
var realm: Realm!
func setupItems() {
let config = SyncUser.current?.configuration()
self.realm = try! Realm(configuration: config!)
self.items = self.realm.objects(Item.self)
self.subscription = self.items!.subscribe(named: "my-items")
self.subscriptionToken = subscription.observe(\.state, options: .initial) { state in
if state == .complete {
print("subscription: queried data has been synced")
} else {
print("Subscription State: \(state)")
}
}
self.notificationToken = self.items!.observe { changes in
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
print("notification: initial results are populated")
for item in self.items! {
if let shop = item.shop {
print(shop.name)
} else {
print("\(item.itemName) has no shop")
}
}
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed
print("notification: results, inserted, deleteed or modified")
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
}

Related

Firestore Database structure question & Swift

I have recently started learning Swift and Firestore and i have a question around how to structure a database for what im doing.
At the moment i have the following structure.
enter image description here
What i would like to do is to fetch the newest collection with a specific ownerID in the above structure am i right in suggesting i would have to fetch all documents and add it to an array and go from there, or would it work better with a structure where the document's id is the ownerID. I feel like fetching all the documents is a real waste and would become really problematic if there were lots of documents.
It maybe obvious that im a little lost here.
Thanks in advance
Luke
At the moment i have fetched the documents and sorted them from newest to oldest but now that i need the newest one with a specific ownerID i feel thats its a really clumsy way to do it.
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
class ThingManager: ObservableObject {
#Published private(set) var things: [Things] = []
#Published private(set) var lastThingId = ""
#Published var firstElement = ""
let db = Firestore.firestore()
init(){
getThings()
}
func getThings() {
db.collection("Things").addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("Error: fetching documents \(String(describing: error))")
return
}
self.things = documents.compactMap{ document -> Things? in
do {
return try document.data(as: Things.self)
} catch {
print("Error: decoding document into Thing failed: \(error)")
return nil
}
}
self.things.sort { $0.timestamp > $1.timestamp }
if let Element = self.things.first?.thing {
self.firstElement = Element
print("First Element : \(self.firstElement)")
}
if let id = self.things.last?.id {
self.lastThingId = id
}
}
}

Reading Firestore Document containing an array of references

Thanks in advance for the help. I'm teaching myself Swift and trying to figure out how to retrieve the following data from Firebase. Here's my Firebase Data Model...
Groups (Collection)
-> GroupName (String)
-> Owner (References to someone in the Players collection)
Players (Collection)
-> PlayerFirstName
-> PlayerLastName
The Swift I've written to retrieve this data is in a ViewModel. getAllGroups is called from onAppear in the View and looks like this...
class Group: Identifiable, ObservableObject {
var id: String = UUID().uuidString
var name: String?
var owner: Player?
}
class GroupViewModel: ObservableObject {
#Published var groups = [Group]()
private var db = Firestore.firestore()
func getAllGroups() {
db.collection("groups").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No groups")
return
}
self.groups = documents.map { (queryDocumentSnapshot) -> Group in
var group = Group()
let data = queryDocumentSnapshot.data()
group.name = data["name"] as? String ?? ""
//
// LIKE --- SHOULD THIS CALL TO GETPLAYER use AWAIT, FOR EXAMPLE?
// WE'RE EXECUTING THE CLOSURE FOR THE FIRST CALL AND ABOUT TO MAKE A SECOND
//
group.owner = self.getPlayer(playerRef: data["owner"] as! DocumentReference)
return group
}
}
}
func getPlayer(playerRef: DocumentReference) -> Player {
var player = Player()
playerRef.getDocument { (document, error) in
guard error == nil else {
print ("error", error ?? "")
return
}
if let document = document, document.exists {
let data = document.data()
if let data = data {
player.firstName = data["firstname"] as? String
player.lastName = data["lastname"] as? String
}
}
}
return player
}
}
The sorta obvious problem here is the closure for retrieving the parent Group executes and then goes and tries to retrieve the Owner. But by the time the closure inside getPlayer completes... the Group has already been established.
Groups will have...
group[0]
-> GroupName = "Cool Name Here"
-> Owner = nil
group[0]
-> GroupName = "Different Cool Name"
-> Owner = nil
even though each Group definitely has an Owner.
I get there's some stuff here about asynchronous calls in Swift and how best to handle that... I'm just not sure what the proper pattern is. Thanks again for the help and advice!
-j
To restate the question:
How do you nest Firestore functions
There are 100 ways to do it and, a lot of it depends on the use case. Some people like DispatchGroups, others like escaping completion handlers but in a nutshell, they pretty much do the "same thing" as the following code, written out "long hand" for readability
func populateGroupArray() {
db.collection("groups").addSnapshotListener { (querySnapshot, error) in
guard let docs = querySnapshot?.documents else { return }
for doc in docs {
let groupName = doc.get("name") as! String
let ownerId = doc.get("owner_id") as! String
self.addToArray(groupName: groupName, andOwnerId: ownerId)
}
}
}
func addToArray(groupName: String, andOwnerId: String) {
db.collection("owners").document(andOwnerId).getDocument(completion: { snapshot, error in
let name = snapshot?.get("owner_name") as! String
let group = Group(groupName: groupName, ownerName: name)
self.groups.append(group)
})
}
In summary; calling populateGroupArray reads in all of the documents from the groups collection from Firestore (adding a listener too). We then iterate over the returned documents to get each group name and the owner id of the group.
Within that iteration, the group name and ownerId are passed to another function that reads in that specific owner via it's ownerId and retrieves the name
Finally, a Group object is instantiated with groupName and owner name being populated. That group is then added to a class var groups array.
Now, if you ask a Firebaser about this method, they will generally recommend not reading large amounts of Firebase data 'in a tight loop'. That being said, this will work very well for many use cases.
In the case you've got a HUGE dataset, you may want to consider denormalizing your data by including the owner name in the group. But again, that would be a rare situation.

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.

How to Do Interface-driven Write with Realm when Datasource is Results Object

The Realm documentation for an interface-driven write indicates that you can add a record to a collection like this:
func insertItem() throws {
// Perform an interface-driven write on the main thread:
collection.realm!.beginWrite()
collection.insert(Item(), at: 0)
// And mirror it instantly in the UI
tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
// Making sure the change notification doesn't apply the change a second time
try collection.realm!.commitWrite(withoutNotifying: [token])
}
This seems to imply that the datasource for the table is an Array because there is not an insert method on a Results<Item> collection.
What data type is collection in this situation? It seems like it's an array, but my understanding is that you can't create a Realm notification on an array.
I've also read that it's not a good idea to copy all your Realm objects into an array (for performance reasons) since the results are accessed lazily. But it would seem that interface-driven writes are impossible unless you do. 🤔
Any suggestions on how to properly set this up?
The documentation is a bit vague but the specific answer to your question is that in this case, collection is a List type. See the documentation for Collections.
To drill down a bit, here's an example implementation.
Suppose we have a Person Object and each person has a List property of Dog Objects.
class PersonClass: Object {
#objc dynamic var person_name = ""
let dogs = List<DogClass>()
}
class DogClass: Object {
#objc dynamic var dog_name = ""
#objc dynamic var dog_age = 0
let owners = LinkingObjects(fromType: PersonClass.self, property: "dogs")
}
We want to know when a specific person gets a new dog, update our tableView with that dog info immediately and not receive a Realm notification for the event.
Here's the code to set up an observer for Jean-Luc's dogs.
//a class var for the notification and the List.
var dogListNotificationToken: NotificationToken? = nil
var dogList: List<DogClass>? = nil
func doObserveDogList() {
if let realm = gGetRealm() { //using a singleton Realm for this example
let personResults = realm.objects(PersonClass.self).filter("name == 'Jean-Luc'")
let firstPerson = personResults.first
self.dogList = firstPerson?.dogs
}
if self.dogList == nil {
print("there were no dogs for this person")
return
}
self.dogListNotificationToken = self.dogList!.observe { (changes: RealmCollectionChange) in
switch changes {
case .initial:
print("initial object load complete")
if let results = self.dogList {
for d in results {
print(d.dog_name, d.dog_age)
}
}
break
case .update(_, let deletions, let insertions, let modifications):
print(" handle item delete, insert or mod")
for index in deletions {
print(" deleted object at index: \(index)")
}
for index in insertions {
print(" inserted object at index: \(index)")
}
for index in modifications {
print(" modified object at index: \(index)")
}
break
case .error(let error):
// An error occurred
fatalError("\(error)")
break
}
}
}
and suppose Jean-Luc get gets a new dog so we need to insert that into realm but don't want a notification because we want to handle it 'immediately'.
func insertDog() {
let dogToAdd = DogClass()
dogToAdd.dog_name = "Number 1"
dogToAdd.dog_age = 5
self.dogList?.realm?.beginWrite()
self.dogList?.insert(dogToAdd, at: 0)
//insert into tableView/datasource/whatever
try! self.dogList?.realm!.commitWrite(withoutNotifying: [self.dogListNotificationToken!])
}
The above will result in the Dog 'Number 1' being added to Jean-Luc's dog list with no observe event being triggered upon the insert.

How to call combine multiple completion handlers to combine data to one new array

I have been stuck for a while now and any advice would be greatly appreciated. I am creating an app that uses Firebase database and I have created 5 classes that hold different data in Firebase. I'm creating a tableview that needs to display information from each of the 5 classes (Profile name, image, then information about a league, and info about scores). So in my new class I created a function calling for data from firebase from each class...
For example: GET all players from X league {
FOR every player in the league {
GET the players information
THEN GET the scores
THEN on and on
once we have all information APPEND to new array
}
and then rank the array
}
After all this runs I want to reload the table view on the VC
SO my solution works on the original load but if I back out and re enter the screen the names and images repeat.
To be exact when the indexes print to the console I get
"Player 1: Zack"
"Player 2: John"
However, the screen shows John's image and name repeatedly. BUT only that class... All other data stays where it is supposed to be. And the original functions are all written the same way.
I'm thinking it's something to do with memory management or I wrote my completion handler poorly?
Here is the code in the new array class:
You'll also notice that my completion() is inside my for in loop which I HATE but it's the only way I could get the function to finish before completing.. Otherwise the function completes before the data is ready.
func getLeaderboard(leagueID: String, completion: #escaping ()->()) {
print("League Count After removeAll \(self.rankedGolfers.count)")
self.leagueMembers.getLeagueMembers(leagueID: leagueID) {
print("HANDLER: Step 1: Get League Members")
for member in self.leagueMembers.leagueMembers {
print("Golfer Member ID: \(member.userID)")
self.golferInfo.getGolferInfo(userKey: member.userID, completion: {
print("HANDLER: Step 2: Get player profile info")
print("Golfer Name3: \(self.golferInfo.golfers[0].firstName) \(self.golferInfo.golfers[0].lastName)")
self.handicapHelper.getRounds(userID: member.userID, completion: {
print("HANDLER: Step 3: Get players Handicap")
print("Golfer Handicap3: \(self.golferInfo.golfers[0].lastName): \(self.handicapHelper.handicap)")
self.leagueInfo.getLeagueInfo(leagueID: leagueID, completion: {
print("HANDLER: Step 4: Get league info")
let golferIndex = self.golferInfo.golfers[0]
let memberInfoIndex = self.leagueInfo.leagueInfo[0]
let golferID = member.userID
let profileImg = golferIndex.profileImage
let golferName = "\(golferIndex.firstName) \(golferIndex.lastName)"
let handicap = self.handicapHelper.handicap
let golferLeaguePardieScore = member.pardieScore
let leagueRoundsPlayed = member.numberOfRounds
let roundsRemaining = memberInfoIndex.leagueMinRounds - leagueRoundsPlayed
let currentWinnings = member.currentWinnings
let newGolfer = Leaderboard(golferID: golferID, profileImg: profileImg ?? "No Img", golferName: golferName, golferHandicap: handicap, golferLeaguePardieScore: golferLeaguePardieScore, roundsPlayedInLeague: leagueRoundsPlayed, roundsRemaining: roundsRemaining, currentWinnings: currentWinnings)
self.rankedGolfers.append(newGolfer)
print("HANDLER: Step 5: Add golfer to array")
//print("Golfer Name 4: \(newGolfer.golferName)")
//print("Rounds Remaining: \(newGolfer.roundsRemaining)")
print("league Member Count: \(self.rankedGolfers.count)")
self.getLeaderboardRanking()
print("HANDLER: Step 6: Rank Array")
//print("COMPLETION: \(self.rankedGolfers.count)")
completion()
})
})
})
}
}
}
Thank you for any help possible!
I think we can solve this with a DispatchGroup, which will ensure all of the data is loaded for each user, then append the user to an array used as a tableView dataSource and then reload the tableView upon completion.
To keep it simple we'll start with a UserInfo class which stores their uid, name, favorite food and handicap.
class UserInfoClass {
var uid = ""
var name = ""
var favFood = ""
var handicap = 0
}
and a class var array used as the dataSource for the tableView
var userInfoArray = [UserInfoClass]()
Then, assuming we have a structure like this...
users
uid_0
name: "Leroy"
handicaps
uid_0
amt: 4
fav_foods
uid_0
fav_food: "Pizza"
...here's a function that reads all users, then iterates over each one populating a UserInfoClass with their name and uid, as well as creating a dispatch group that also populates their favorite food and handicap. When that's complete the user is added to the dataSource array and when all of the users are read the tableView is reloaded to display the information.
func loadUsersInfoAndHandicap() {
let ref = self.ref.child("users")
self.userInfoArray = []
ref.observeSingleEvent(of: .value, with: { snapshot in
let group = DispatchGroup()
let allUsers = snapshot.children.allObjects as! [DataSnapshot]
for user in allUsers {
let uid = user.key
let name = user.childSnapshot(forPath: "name").value as? String ?? "No Name"
let aUser = UserInfoClass()
aUser.uid = uid
aUser.name = name
group.enter()
self.loadFavFood(withUid: uid) {favFood in
aUser.favFood = favFood
group.leave()
}
group.enter()
self.loadHandicap(withUid: uid) { handicap in
aUser.handicap = handicap
group.leave()
}
group.notify(queue: .main) {
self.userInfoArray.append(aUser)
}
}
group.notify(queue: .main) {
print("done, reload the tableview")
for user in self.userInfoArray {
print(user.uid, user.name, user.favFood, user.handicap)
}
}
})
}
the users name and uid is read from the main users node and here are the two functions that read their favorite food and handicap.
func loadFavFood(withUid: String, completion: #escaping(String) -> Void) {
let thisUser = self.ref.child("userInfo").child(withUid)
thisUser.observeSingleEvent(of: .value, with: { snapshot in
let food = snapshot.childSnapshot(forPath: "fav_food").value as? String ?? "No Fav Food"
completion(food)
})
}
func loadHandicap(withUid: String, completion: #escaping(Int) -> Void) {
let thisUser = self.ref.child("handicaps").child(withUid)
thisUser.observeSingleEvent(of: .value, with: { snapshot in
let handicap = snapshot.childSnapshot(forPath: "amt").value as? Int ?? 0
completion(handicap)
})
}
note that self.ref points to my firebase so substitute a reference to your firebase.
Note I typed this up very quickly and there is essentially no error checking so please add that accordingly.