Trying to get data from Firebase realtime-database - swift

I'm trying to get data from a realtine-database I have in Firebase.
I saw a few answers here but couldn't find a solution that I understood and worked for me.
this is my code:
func ReadTeamPlayers(teamName name: String, completion: #escaping ([Player]?) -> Void) {
var playersArray = [Player]()
ref.child(name).observeSingleEvent(of: .value) { (snapshot) in
if let postDict = snapshot.value as? Dictionary<String, Any>{
for dict in postDict {
playersArray.append(Player(name: dict[snapshot.key] as! String, rank: dict[snapshot.value]))
}
}
}
}
In the player initializer I need to get the player's name and rank.
My Firebase looks like that:
TEAM1
---player1: 10
---player2: 5
---player3: 6
playerX is the player's name for now and the number is the player's rank.
How can I get the info for each player and use it later?
I tried few things but none worked for me.
Edit: I changed the code above to my current one, and this is the error I get now:
Value of tuple type '(key: String, value: Any)' has no member 'subscript'

You need
dic.forEach {
let player = Player(playerName:$0.key, playerRank:$0.value)
arr.append(player)
}
OR
let arr = dic.map { Player(playerName:$0.key, playerRank:$0.value) }
Change response
func ReadTeamPlayers(teamName name: String, completion: #escaping ([Player]?) -> Void) {
func ReadTeamPlayers(teamName name: String, completion: #escaping ([Player]?) -> Void) {
ref.child(name).observeSingleEvent(of: .value) { (snapshot) in
if let mPlayer = snapshot.value as? [String:Int] {
var arr = [Player]()
mPlayer.forEach {
let player = Player(playerName:$0.key, playerRank:$0.value)
arr.append(player)
}
completion(arr)
}else{
completion(nil)
}
}
}

Try this, you are using strange code with generics. Keep it simple
func ReadTeamPlayers(teamName name: String, completion: #escaping ([Player]?) -> Void) {
var playersArray = [Player]()
ref.child(name).observeSingleEvent(of: .value) { (snapshot) in
guard let dictionary = snapshot.value as? [String: Any] else {
print("empty snapshot")
return
}
for dict in dictionary {
print("playerName = \(dict.key)")
if let playerRank = dict.value as? Int {
print("playerRank = \(playerRank)")
let player = Player(name: dict.key, rank: playerRank)
playersArray.append(player)
}
}
}
}

Related

modifying a variable from inside a closure swift/firebase

In the function below, everything works except when I try to get the variable partnerName from point A to point B, i.e. moving the variable in an out of a closure. I am a novice coder so I am hoping that someone can help me discover how to solve this particular issue and point me to a place where I might be able to learn the basics of how to share variables between different parts of a function.
func getAllConversations(handler: #escaping (_ conversationsArray: [Conversation]) -> ()) {
var partner = [""]
var title: String = ""
var partnerName = ""
var conversationsArray = [Conversation]()
REF_CONVERSATION.observeSingleEvent(of: .value) { (conversationSnapshot) in
guard let conversationSnapshot = conversationSnapshot.children.allObjects as? [DataSnapshot] else { return}
for conversation in conversationSnapshot {
let memberArray = conversation.childSnapshot(forPath: "members").value as! [String]
partner = memberArray.filter {$0 != (Auth.auth().currentUser?.uid)!}
if memberArray.contains((Auth.auth().currentUser?.uid)!) {
let newPartner = (String(describing: partner))
title = newPartner.replacingOccurrences(of: "[\\[\\]\\^+<>\"]", with: "", options: .regularExpression, range: nil)
databaseRef.child("bodhi").child(title).observeSingleEvent(of: .value, with: { (snapshot) in
if let bodhiDict = snapshot.value as? [String: AnyObject]
{
//I realize that the variable is being wrongly redeclared here to make this run.
let partnerName = (bodhiDict["Name"] as! String)
print ("partnerName returned from firebase: \(partnerName)")
// Point A: This prints "Sandy"
}
})
print ("partnerName: \(partnerName)")
// Point B: This prints nothing but if I add partnerName = "Sandy", then the function complete
title = partnerName
print ("new title: \(title)")
let conversation = Conversation(conversationTitle: title, key: conversation.key, conversationMembers: memberArray, conversationMemberCount: memberArray.count)
conversationsArray.append(conversation)
}
}
handler(conversationsArray)
}
}
func createGroup(withTitle title: String, andDescription description: String, forUserIds ids: [String], handler: #escaping (_ groupCreated: Bool) -> ()) {
REF_GROUPS.childByAutoId().updateChildValues(["title": title, "description": description, "members": ids])
// need to add code here for slow internet: if successful connection true else error
handler(true)
}
func getAllGroups(handler: #escaping (_ groupsArray: [Group]) -> ()) {
var groupsArray = [Group]()
REF_GROUPS.observeSingleEvent(of: .value) { (groupSnapshot) in
guard let groupSnapshot = groupSnapshot.children.allObjects as? [DataSnapshot] else { return}
for group in groupSnapshot {
let memberArray = group.childSnapshot(forPath: "members").value as! [String]
if memberArray.contains((Auth.auth().currentUser?.uid)!) {
let title = group.childSnapshot(forPath: "title").value as! String
let description = group.childSnapshot(forPath: "description").value as! String
let group = Group(title: title, description: description, key: group.key, members: memberArray, memberCount: memberArray.count)
groupsArray.append(group)
}
}
handler(groupsArray)
}
}
}
I recommend you read the documentation on closures:
Closures
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html
Closures: Capturing Values
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-ID103

How can I detect firebase observe childAdded is stop calling when reach queryLimit?

Now, I'm so confused about firebase with observe using childAdded data event type. The reason why I use childAdded to observe my firebase because I want to make my list page dynamic whether firebase has new data insert.
And my question is how to know observe is stop calling when reach the queryLimit? Because I have a indicator and I want to turn it off when reach the queryLimit.
My firebase structure below:
root {
talkID1(id by auto create) {
attribute1 ...
attribute2 ...
attribute3 ...
time
}
talkID2(id by auto create){
...
time
}
... // many talk ID which auto create by firebase
}
As what I know, if using childAdd to observe, data will one child by child to passing data in call back. So If I have N datas in firebase and I think it will calls N<=5 times, right?
My completion handler below:
func receiveIfExist(completion: #escaping (_ data: (My data type) -> Void) {
let requestWithQuery = Database.database.reference().child("root").queryOrdered(byChild: "time")
requestWithQuery.queryLimited(toLast: 5).observe(.childAdded, with: { (snapshot) in
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
completion(data)
})
})
}
I'm calling receiveIfExist this function in viewDidLoad().
override func viewDidLoad() {
super.viewDidLoad()
self.myIndicator.startAnimating() // start running indicator
self.receiveIfExist { (data) in
self.handleTalk(with: data) // do something with request data
self.myIndicator.stopAnimating() // WEIRD!!!! Please look comments below
/*
I think it can not be added here because this completion will call N<=5 times just what I said before.
I think it should detect what my queryLimit data is and check the request data is this queryLimit data or not.
If yes then stop indicator animating, if not then keep waiting the queryLimit reach.
*/
}
}
How can I detect the observe is reach queryLimit?
If I can detect then I can turn off my indicator when it reach.
Thank you!
queryLimited(toLast: 5)
means (in much simpler words) please get the last 5 values (order is decided by the previous part of your query)
1. Now, since you are sorting the data by times , the values with the last 5 times will be retrieved, therefore your observer will be triggered 5 times
2. Note that if you have less than 5 records say 2 records, then it will be triggered only twice because maximum limit is 5, not minimum limit
3. Another point is that say if a new child is added and when you sort the items again according to the time and the new child is one of the last 5 items then this observer will be triggered again.
so to get the query limit you can make some changes in your code like this:
func receiveIfExist(completion: #escaping (data: YourType, limitCount: Int) -> Void) {
let requestWithQuery = Database.database.reference().child("root").queryOrdered(byChild: "time")
requestWithQuery.queryLimited(toLast: 5).observe(.childAdded, with: { (snapshot) in
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
self.index = self.index + 1
completion(data, self.index)
})
})
}
Then using the above function as follows:
var index = 0
override func viewDidLoad() {
super.viewDidLoad()
self.myIndicator.startAnimating() // start running indicator
self.receiveIfExist { (data, limitCount) in
self.handleTalk(with: data) // do something with request data
if limitCount == 5 {
self.myIndicator.stopAnimating()
}
}
}
UPDATED:
Since very good point raised by Kevin, that above solution will fail if we have say only two records and index will never be equal to 5 and myIndicator will not stop animating,
One solution that comes to my mind is this:
First we get the children count using observeSingleEvent:
func getChildrenCount(completion: #escaping (_ childrenCount: Int) -> Void){
Database.database.reference().child("root").observeSingleEvent(of:.value with: { (snapshot) in
completion(snapshot.children.count)
}
}
then we apply the query to get last 5 items:
func receiveIfExist(completion: #escaping (data: YourType, limitCount: Int) -> Void) {
let requestWithQuery = Database.database.reference().child("root").queryOrdered(byChild: "time")
requestWithQuery.queryLimited(toLast: queryLimit).observe(.childAdded, with: { (snapshot) in
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
self.index = self.index + 1
completion(data, self.index)
})
})
}
then use this count in your code as follows:
var index = 0
var childrenCount = 0
var queryLimit = 5
override func viewDidLoad() {
super.viewDidLoad()
self.myIndicator.startAnimating() // start running indicator
self.getChildrenCount {(snapChildrenCount) in
self.childrenCount = snapChildrenCount
self.receiveIfExist { (data, limitCount) in
self.handleTalk(with: data) // do something with request data
if (self.childrenCount < self.queryLimit && limitCount == self.childrenCount) || limitCount == self.queryLimit {
DispatchQueue.main.async {
self.myIndicator.stopAnimating()
}
}
}
}
}
func receiveIfExist(limitCount: UInt, completion: #escaping (data: MyDataType) -> Void) {
let requestWithQuery = Database.database.reference().child("root").queryOrdered(byChild: "time")
requestWithQuery.queryLimited(toLast: limitCount).observe(.childAdded, with: { (snapshot) in
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
completion(data)
})
})
}
I also do this function for only observe single child's value
let requestTalks = Database.database.reference().child("root")
func getSingleTalk(by key: String = "", at limitType: TalkLimitType, completion: #escaping (_ eachData: MyDataType) -> Void) {
var requestSingleTalk: DatabaseQuery {
switch limitType {
case .first :
return self.requestTalks.queryOrdered(byChild: "time").queryLimited(toFirst: 1)
case .last :
return self.requestTalks.queryOrdered(byChild: "time").queryLimited(toLast: 1)
case .specificKey :
return self.requestTalks.child(key)
}
}
requestSingleTalk.observeSingleEvent(of: .value, with: { (snapshot) in
if limitType == .specificKey {
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
completion(data)
})
} else {
guard let snapshotValue = snapshot.value as? NSDictionary,
let eachTalkKey = snapshotValue.allKeys[0] as? String,
let eachTalkValue = snapshotValue.value(forKey: eachTalkKey) as? [String: Any] else { return }
self.getSingleTalkInfo(key: eachTalkKey, value: eachTalkValue, completion: { (data) in
completion(data)
})
}
})
}
As a result, I can do something like this in my viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
self.myIndicator.startAnimating()
self.receiveIfExist(limitCount: 5) { (eachData) in
self.handleTalk(with: eachData)
self.getSingleTalk(at: .last, completion: { (lastData) in
if eachData.keyID == lastData.keyID{
DispatchQueue.main.async {
self.myIndicator.stopAnimating()
}
}
})
}
}

Does Firebase know when data is no longer a part of query?

In my Firebase database, I have children containing Unix times, and I'm querying by times that occur after the current time. I'm putting the data of each child which matches that criteria into a UICollectionView. When the current time surpasses the time of one of the children, I want for the child to expire, and to get removed from the UICollectionView. Currently, it isn't getting removed until I restart the app. Here is some of the code:
// in viewDidLoad
self.events_query = Database.database().reference().child("events").queryOrdered(byChild: "end-time").queryStarting(atValue: Date().timeIntervalSince1970)
// in viewWillAppear
func observeAllEvents() {
self.events_query.observe(.value, with: { (snapshot) in
guard let eids_dict = snapshot.value as? [String : AnyObject] else { return }
let eids = Array(eids_dict.keys)
for eid in eids {
print(eid)
}
Event.getAllEvents(with: eids, ref: self.events_query.ref, completion: { (events) in
if let feed_datasource = self.datasource as? EventFeedDatasource {
feed_datasource.events = events
}
DispatchQueue.main.async {
self.collectionView?.reloadData()
}
})
})
}
// in viewDidDisappear
self.events_query.removeAllObservers()
Here's the function getAllEvents:
static func getAllEvents(with eids: [String], ref: DatabaseReference, completion: #escaping (_ events: [Event]) -> Void) {
var events = [Event]()
let dispatch_groups = [DispatchGroup(), DispatchGroup()]
for eid in eids {
dispatch_groups[0].enter()
ref.child(eid).observeSingleEvent(of: .value, with: { (snapshot) in
guard let dictionary = snapshot.value as? [String : AnyObject] else { return }
dispatch_groups[1].enter()
// I'm not including `load` because all it does is parse the snapshot
Event.load(with: dictionary, completion: { (event) in
events.append(event)
dispatch_groups[1].leave()
})
dispatch_groups[0].leave()
})
}
dispatch_groups[0].notify(queue: DispatchQueue.main) {
dispatch_groups[1].notify(queue: DispatchQueue.main) {
completion(events)
}
}
}

Return two arrays from observeSingleEvent

I have questions and answers in Firebase Realtime Database, which I need to be downloaded and put in to arrays. I have created a function which takes a parameter and should return two arrays
let data = fetchQuestions(category: "Animals")
qAni = data.ques
aAni = data.ans
fetchQuestions method:
func fetchQuestions(category: String) -> (ques: [String], ans: [String]) {
var q = [String]()
var a = [String]()
ref.child("QA/Category").observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? [String: Any]
for (k, v) in value! {
if k == category {
let QA = v as! NSArray
for i in QA {
let question = i as! [String: String]
q.append(question["question"]!)
a.append(question["answer"]!)
}
return (q, a) ** Unexpected non-void return value in void function
}
}
})
}
I have tried to use dispatch group, but then I receive a different error.
You are currently returning from the completion block of observeSingleEvent and its return type is void, What you need is you need to create completionHandler with your fetchQuestions. Now with Swift instead of using NSArray use Swift native array. Also instead of maintaining two array maintain one array with custom objects of struct or class.
So first create one struct like this.
struct Category {
let question: String
let answer: String
}
Now make completionHandler with your fetchQuestions like this way.
func fetchQuestions(category: String, completion: #escaping([Category]) -> Void) {
var array = [Category]
ref.child("QA/Category").observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? [String: Any]
for (k, v) in value! {
if k == category {
let QA = v as! [[String:String]]
for i in QA {
let que = i["question"]!
let ans = i["answer"]!
array.append(Category(question: que, answer: ans))
}
}
}
completion(array)
})
}
Now call fetchQuestions this way.
self.fetchQuestions(category: "Animals") { categories in
//access categories here
print(categories)
}
Note: Instead of comparing value in for loop you can also use queryEqual(toValue:) with your request to get specific result check this SO thread for more details on it.
Its because you are returning a value from a closure instead you should use different closure for that. Try it:
func fetchQuestions(category: StringcompletionBlock:#escaping (_ ques:[String], _ ans :[String]) -> ()) {
var q = [String]()
var a = [String]()
ref.child("QA/Category").observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? [String: Any]
for (k, v) in value! {
if k == category {
let QA = v as! NSArray
for i in QA {
let question = i as! [String: String]
q.append(question["question"]!)
a.append(question["answer"]!)
}
}
return completionBlock(q,a)
}
})
}
#escaping is required : when the closure is passed as an argument to
the function, but is called after the function returns.

Trying to save struct array: failed

I'm trying to save my struct array. And I read all posts I could find for this topic. Finally I used one example and stripped it a little bit, but it doesn't work. It does not save, so it could not read back:
func path() -> String {
return URL(fileURLWithPath: "/Volumes/MacOS/fasttemp/Test.TXT").absoluteString
}
struct Movie {
let name: String
let releaseYear: Int
}
protocol Dictionariable {
func dictionaryRepresentation() -> NSDictionary
init?(dictionaryRepresentation: NSDictionary?)
}
extension Movie: Dictionariable {
func dictionaryRepresentation() -> NSDictionary {
let representation: [String: AnyObject] = [
"name": name as AnyObject,
"releaseYear": releaseYear as AnyObject
]
return representation as NSDictionary
}
init?(dictionaryRepresentation: NSDictionary?) {
guard let values = dictionaryRepresentation else {return nil}
if let name = values["name"] as? String,
let releaseYear = values["releaseYear"] as? Int {
self.name = name
self.releaseYear = releaseYear
} else {
return nil
}
}
}
func extractStructureFromArchive<T: Dictionariable>() -> T? {
guard let encodedDict = NSKeyedUnarchiver.unarchiveObject(withFile: path()) as? NSDictionary else {return nil}
return T(dictionaryRepresentation: encodedDict)
}
func archiveStructure<T: Dictionariable>(structure: T) {
let encodedValue = structure.dictionaryRepresentation()
NSKeyedArchiver.archiveRootObject(encodedValue, toFile: path())
}
func extractStructuresFromArchive<T: Dictionariable>() -> [T] {
guard let encodedArray = NSKeyedUnarchiver.unarchiveObject(withFile: path()) as? [AnyObject] else {return []}
return encodedArray.map{$0 as? NSDictionary}.flatMap{T(dictionaryRepresentation: $0)}
}
func archiveStructureInstances<T: Dictionariable>(structures: [T]) {
let encodedValues = structures.map{$0.dictionaryRepresentation()}
NSKeyedArchiver.archiveRootObject(encodedValues, toFile: path())
}
let movies = [
Movie(name: "Avatar", releaseYear: 2009),
Movie(name: "The Dark Knight", releaseYear: 2008)
]
// this fails!
archiveStructureInstances(structures: movies)
let someArray: [Movie] = extractStructuresFromArchive()
print("\(someArray[0].name)")
No file is created. The folder exist. What's wrong with it?
I'm using XCode 8.2.1 with Swift 3.
Added: I took this example from another question/answer. I also reduced it from 3 to 2 struct members. And I updated it for Swift 3. But it still doesn't work! That's the reason I'm asking.
Your path is wrong. You do not archive to a file. You have to use a directory.
Changing the path() methode to this (from this answer):
func path() -> String {
let documentsPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first
let path = documentsPath! + "/Movie"
return path
}
your code prints Avatar as expected.