Firebase retrieving related data in swift - swift

I'm new to Firebase. Not good at DB also.
I'm really hoping that people could give me advices about my DB design and retrieving data codes.
I designed the DB as below.
The comment have a createTs for ordering.But the content shouldn't ordered by create time. So I put a "index" key into contents info in the post.(I'm not sure if it is a good way)
About the data retrieving, I made a dataManager object to load data.
DataManger.swift
func loadPosts(completeBlock : ([Post]) -> Void ) {
ref.child("posts").observeEventType(.Value , withBlock: { snapshot in
var postList : [Post] = []
if snapshot.value is NSNull {
} else {
for post in snapshot.children {
let postSnap = post as! FIRDataSnapshot
let post = Post()
post.setValue(withSanpShot: postSnap)
post.commentList = []
for key in post.comments.keys {
self.loadPostComment(withKey: key, completeBlock: { (comment) in
post.commentList.append(comment)
})
}
post.contentList = []
for key in post.contents.keys {
self.loadPostContent(withKey: key, completeBlock: { (content) in
post.contentList.append(content)
})
}
postList.append(post)
}
}
completeBlock(postList)
})
}
func loadPostComment(withKey key: String, completeBlock:(Comment) -> Void) {
ref.child("post-comments").child(key).observeEventType(.Value, withBlock: { commentSnapShot in
let comment = Comment()
comment.setValue(withSanpShot: commentSnapShot)
completeBlock(comment)
})
}
func loadPostContent(withKey key: String, completeBlock:(PostContent) -> Void) {
ref.child("post-contents").child(key).observeEventType(.Value, withBlock: { contentSnapShot in
let content = PostContent()
content.setValue(withSanpShot: contentSnapShot)
completeBlock(content)
})
}
And struct of the Post.swift.
class Post: NSObject {
var postID : String
var authorID: String
var title: String
var createTS : NSNumber
var comments : [String : AnyObject]
var contents : [String : AnyObject]
var commentList : [Comment] {
didSet {
//TODO: add some closure to update view.Maybe also need sorting codes
print("commentList : \(commentList)")
}
}
var contentList : [PostContent] {
didSet {
//TODO: add some closure to update view.Maybe also need sorting codes
print("contentList : \(contentList)")
}
}
}
I'm not sure the work I'v done is the right way to using it.Although I could got the data I want.
Please give me advices. That will be really helpful to me. Thanks.

Related

Need acces from document to collection Firestore

I'm trying to do an iOS app and i've binded it with firebase, so I'm trying to get some posts ad fetch them, and this works fine, however this posts got 2 collections (likes and replies) and i'm trying to fetch likes, the thing is that I can't get the likes because for some reasons I can't a class for document forEach neither I can't access it, someone got any idea?
Code:
import Foundation
import Firebase
struct Post : Hashable {
var id : String
var dateAdded : String
var posterEmail : String
var posterUsername : String
var posterIcon : String
var postTitle : String
var postBody : String
var likes : [String]
var userLikedPost : Bool
}
struct Like {
var likeId : String
var likerEmail : String
}
class Likes {
var likes : [Like] = []
func fetchLikes() {
//Firestore.firestore()
}
}
class Posts : ObservableObject {
#Published var posts : [Post] = []
func fetchPosts() {
Firestore.firestore().collection("posts").getDocuments(completion: { (docPosts, error) in
if (error != nil) {
print("error fetching posts")
} else {
docPosts?.documents.forEach { (post) in
let id = post.documentID
let email = post.get("posterEmail") as! String
let username = post.get("posterUsername") as! String
let icon = post.get("posterIcon") as! String
let title = post.get("title") as! String
let body = post.get("body") as! String
// Here i want to insert the code that gets the likes class and access the likes variable
self.posts.append(Post(id: id, dateAdded:String(id.split(separator: "_").joined(separator: "/").prefix(10)) ,posterEmail: email, posterUsername: username, posterIcon: icon, postTitle: title, postBody: body,
likes: [],userLikedPost: false))
}
}
})
}
}
The Firestore structure was not included in the question so I will present one for use
user_wines
uid_0
name: "Jay"
favorite_wines:
0: "Insignia"
1: "Scavino Bricco Barolo"
2: "Lynch Bages"
uid_1
name: "Cindy"
favorite_wines
0: "Palermo"
1: "Mercury Head"
2: "Scarecrow"
And then the code to read all of the user documents, get the name, the wine list (as an array as Strings) and output it to console
func readArrayOfStrings() {
let usersCollection = self.db.collection("user_wines")
usersCollection.getDocuments(completion: { snapshot, error in
guard let allDocs = snapshot?.documents else { return }
for doc in allDocs {
let name = doc.get("name") as? String ?? "No Name"
let wines = doc.get("favorite_wines") as? [String] ?? []
wines.forEach { print(" ", $0) }
}
})
}
and the output
Jay
Insignia
Scavino Bricco Barolo
Lynch Bages
Cindy
Palermo
Mercury Head
Scarecrow
EDIT
Here's the same code using Codable
class UserWineClass: Codable {
#DocumentID var id: String?
var name: String
var favorite_wines: [String]
}
and the code to read data into the class
for doc in allDocs {
do {
let userWine = try doc.data(as: UserWineClass.self)
print(userWine.name)
userWine.favorite_wines.forEach { print(" ", $0) }
} catch {
print(error)
}
}

queryOrdered doesn't always return data on tableViewCell in correct order

I am trying to order data on the notifications page from new to old based on timestamp, right now - when i run it, sometimes it is in the correct order but other times it is random and incorrect. Please let me know if there is anything i can add to make sure it runs smoothly at all times, thank you in advance :)
My firebase JSON structure is:
"notifications" : {
"BlP58dSQGCUBwhst91yha43AQu42" : {
"-LeNCQJ6nUSR1263iKyj" : {
"from" : "FRuuk20CHrhNlYIBmgN4TTz3Cxn1",
"timestamp" : 1557331817,
"type" : "true"
},
"-LeNCRwNpNaXm2qhYPpu" : {
"from" : "FRuuk20CHrhNlYIBmgN4TTz3Cxn1",
"timestamp" : 1557331824,
"type" : "true"
},
"BlP58dSQGCUBwhst91yha43AQu42-FRuuk20CHrhNlYIBmgN4TTz3Cxn1" : {
"from" : "FRuuk20CHrhNlYIBmgN4TTz3Cxn1",
"timestamp" : 1557331811,
"type" : "false"
}
},
My code:
func observeNotification(withId id: String, completion: #escaping (Notifications) -> Void) {
REF_NOTIFICATION.child(id).queryOrdered(byChild: "timestamp").observe(.childAdded, with: { snapshot in
if let dict = snapshot.value as? [String: Any] {
let newNoti = Notifications.transform(dict: dict, key: snapshot.key)
completion(newNoti)
}
})
}
Edit:
The function is then called in the NotificationViewController like this:
func loadNotifications() {
guard let currentUser = Api.User.CURRENT_USER else { return }
Api.Notification.observeNotification(withId: currentUser.uid , completion: { notifications in
guard let uid = notifications.from else { return }
self.fetchUser(uid: uid, completed: {
self.notifications.insert(notifications, at: 0)
self.tableView.reloadData()
})
})
}
and loadNotifications() is called in the viewDidLoad
UPDATE:
Trying to do it using "for child in snapshot.children" but nothing is showing on notifications page anymore
func observeNotification(withId id: String, completion: #escaping (Notifications) -> Void) {
REF_NOTIFICATION.child(id).observe(.value, with: { snapshot in
for child in snapshot.children {
let snap = child as! DataSnapshot
let key = snap.key
let notificationOrder = self.REF_NOTIFICATION.child(key).queryOrdered(byChild: "timestamp")
notificationOrder.observeSingleEvent(of: .value, with: { snapshot in
if let dict = snapshot.value as? [String: Any] {
print(dict)
let newNoti = Notifications.transform(dict: dict, key: snapshot.key)
completion(newNoti)
}
})
}
})
}
}
The DataSnapshot that you get back from Firebase contains three types of information:
The key of each child that matched the query.
The value of each child that matched the query.
The order in which the children result from the query.
When you convert the entire result to a dictionary (dict = snapshot.value as? [String: Any]), there is only place for the keys and the values. So the order of the children gets lost.
To maintain the order of the child nodes, loop over the query results of snapshot.children as shown in the documentation on listening for lists of data with a value event.

Firebase Realtime Database in Swift

After several hours of trying to figure out what's happening without finding an answer to fill my void anywhere, I finally decided to ask here.
While I assume I do have a concurrency issue, I have no clue as to how to solve it...
I have an application trying to pull data from a Firebase Realtime Database with the following content:
{
"products" : {
"accessory" : {
"foo1" : {
"ean" : 8793462789134,
"name" : "Foo 1"
},
"foo2" : {
"ean" : 8793462789135,
"name" : "Foo 2"
}
},
"cpu" : {
"foo3" : {
"ean" : 8793462789115,
"name" : "Foo 3"
}
},
"ios" : {
"foo4" : {
"ean" : 8793462789120,
"name" : "Foo 4"
},
"foo5" : {
"ean" : 8793462789123,
"name" : "Foo 5"
}
}
}
}
I have a data model in Product.swift:
class Product {
var identifier: String
var category: String
var ean: Int
var name: String
init(identifier: String, category: String, ean: Int, name: String) {
self.init(identifier: identifier)
self.category = category
self.ean = ean
self.name = name
}
}
I want to fetch the data in another class called FirebaseFactory.swift. I plan to use to communicate with Firebase:
import Foundation
import FirebaseDatabase
class FirebaseFactory {
var ref = Database.database().reference()
func getAvailableProducts() -> [Product] {
var products = [Product]()
var data: DataSnapshot = DataSnapshot()
self.ref.child("products").queryOrderedByKey().observeSingleEvent(of: .value) { (snapshot) in
data = snapshot
// 1st print statement
print("From within closure: \(data)")
}
// 2nd print statement
print("From outside the closure: \(data)")
// Getting the products yet to be implemented...
return products
}
}
For now, I am simply trying to call the getAvailableProducts() -> [Product] function from one of my view controllers:
override func viewWillAppear(_ animated: Bool) {
let products = FirebaseFactory().getAvailableProducts()
}
My Problem now is that the 2nd print is printed prior to the 1st – which also means that retrieving the data from the snapshot and assigning it to data variable does not take place. (I know that the code to create my Product objects is missing, but that part actually is not my issue – concurrency is...)
Any hints – before I pull out any more of my hairs – is highly appreciated!!
You're on the right track with your theory: the behavior you're describing is how asynchronous data works with closures. You've experienced how this causes problems with returning the data you want. It's a very common question. In fact, I wrote a blog on this recently, and I recommend you check it out so you can apply the solution: incorporating closures into your functions. Here's what that looks like in the particular case you've shown:
func getAvailableProducts(completion: #escaping ([Product])-> Void) {
var products = [Product]()
var data: DataSnapshot = DataSnapshot()
self.ref.child("products").queryOrderedByKey().observeSingleEvent(of: .value) { (snapshot) in
data = snapshot
// do whatever you were planning on doing to return your data into products... probably something like
/*
for snap in snapshot.children.allObjects as? [DataSnapshot] {
let product = makeProduct(snap)
products.append(product)
}
*/
completion(products)
}
}
Then in viewWillAppear:
override func viewWillAppear(_ animated: Bool) {
FirebaseFactory().getAvailableProducts(){ productsArray in
// do something with your products
self.products = productsArray
// maybe reload data if you have a tableview
self.tableView.reloadData()
}
}
If I understand your question, you need to return the data after the event occurs, because is an async event
class FirebaseFactory {
var ref = Database.database().reference()
func getAvailableProducts() -> [Product] {
var products = [Product]()
var data: DataSnapshot = DataSnapshot()
self.ref.child("products").queryOrderedByKey().observeSingleEvent(of: .value) { (snapshot) in
data = snapshot
// 1st print statement
print("From within closure: \(data)")
// Process here the snapshot and insert the products in the array
return products
}
}
}

Can I use Swift's map() on Protocols?

I have some model code where I have some Thoughts that i want to read and write to plists. I have the following code:
protocol Note {
var body: String { get }
var author: String { get }
var favorite: Bool { get set }
var creationDate: Date { get }
var id: UUID { get }
var plistRepresentation: [String: Any] { get }
init(plist: [String: Any])
}
struct Thought: Note {
let body: String
let author: String
var favorite: Bool
let creationDate: Date
let id: UUID
}
extension Thought {
var plistRepresentation: [String: Any] {
return [
"body": body as Any,
"author": author as Any,
"favorite": favorite as Any,
"creationDate": creationDate as Any,
"id": id.uuidString as Any
]
}
init(plist: [String: Any]) {
body = plist["body"] as! String
author = plist["author"] as! String
favorite = plist["favorite"] as! Bool
creationDate = plist["creationDate"] as! Date
id = UUID(uuidString: plist["id"] as! String)!
}
}
for my data model, then down in my data write controller I have this method:
func fetchNotes() -> [Note] {
guard let notePlists = NSArray(contentsOf: notesFileURL) as? [[String: Any]] else {
return []
}
return notePlists.map(Note.init(plist:))
}
For some reason the line return notePlists.map(Note.init(plist:)) gives the error 'map' produces '[T]', not the expected contextual result type '[Note]'
However, If I replace the line with return notePlists.map(Thought.init(plist:)) I have no issues. Clearly I can't map the initializer of a protocol? Why not and what's an alternate solution?
If you expect to have multiple types conforming to Note and would like to know which type of note it is stored in your dictionary you need to add an enumeration to your protocol with all your note types.
enum NoteType {
case thought
}
add it to your protocol.
protocol Note {
var noteType: NoteType { get }
// ...
}
and add it to your Note objects:
struct Thought: Note {
let noteType: NoteType = .thought
// ...
}
This way you can read this property from your dictionary and map it accordingly.

firebase swift queries in queries performance

I am new to Firebase and relatively new to Swift.
I have firebase set up as below. I have users, followers and blocked users. I take care of the followers in the UITableViewCell class.
I am wondering, before I go any further: how does performance get affected by putting observers in observers in queries in queries. (Hope these are the correct terms) . Is below the right way to go about it?(the most efficient way). It works, but also seems to stutter a bit. I appreciate any feedback.
{
"BlockedByUsers" : {
"ba1eb554-9a81-4a74-bfd9-484a32eee13d" : {
"97fee08f-19b2-4eb5-9eab-4b1985c22595" : true
}
},
"Dates" : {
"1457635040" : {
"97fee08f-19b2-4eb5-9eab-4b1985c22595" : true
},
},
"Locations" : {
"97fee08f-19b2-4eb5-9eab-4b1985c22595" : {
".priority" : "u14dkwm41h",
"g" : "u14dkwm41h",
"l" : [ 51.05521018175982, 3.720297470654139 ]
},
},
"Users" : {
"97fee08f-19b2-4eb5-9eab-4b1985c22595" : {
"blockedUsers" : {
"ba1eb554-9a81-4a74-bfd9-484a32eee13d" : true
},
"following" : {
"51879163-8b35-452b-9872-a8cb4c84a6ce" : true,
},
"fullname" : "",
"dates" : 1457635040,
"location" : "",
},
}
}
my Swift code with the multiple queries I'm worried about:
var usersRef: Firebase!
var userFollowingRef: Firebase!
var blockedByUsersRef: Firebase!
var datesRef: Firebase!
var geofireEndRef: Firebase!
var geoFireEnd: GeoFire? {
return GeoFire(firebaseRef: geofireEndRef)
}
var dateRangeStart = Int()
var dateRangeEnd = Int()
override func viewDidLoad(){
super.viewDidLoad()
usersRef = DataService.ds.REF_USERS
userFollowingRef = DataService.ds.REF_CURRENTUSER_FOLLOWING
blockedByUsersRef = DataService.ds.REF_BLOCKED_BY_USERS
datesRef = DataService.ds.REF_DATES
geofireEndRef = DataService.ds.REF_GEOFIREREF_END
}
override func viewWillAppear(animated: Bool){
super.viewWillAppear(animated)
if userdefaultsUid != nil
{
geoFireEnd!.getLocationForKey(userID, withCallback: { (location, error) in
if (error != nil)
{
print("An error occurred getting the location for \(self.userID) : \(error.localizedDescription)")
} else if (location != nil)
{
self.updateUsersWithlocation(location)
} else
{
print("GeoFire does not contain a location for \(self.userID)")
self.updateUsersWithoutLocation()
}
})
}
}
func updateUsersWithlocation(location: CLLocation)
{
var allKeys = [String]()
let locationQuery = self.geoFireEnd!.queryAtLocation(location, withRadius: 100.0)
locationQuery.observeEventType(GFEventType.init(0), withBlock: {(key: String!, location: CLLocation!) in
allKeys.append(key)
self.datesRef.queryOrderedByKey().queryStartingAtValue(String(self.dateRangeStart)).queryEndingAtValue(String(self.dateRangeEnd)).observeEventType(.ChildAdded, withBlock: {
snapshot in
self.users.removeAll(keepCapacity: true)
self.newKeys.removeAll()
self.tableView.reloadData()
for datesKey in snapshot.children
{
self.usersRef.childByAppendingPath(datesKey.key!).observeSingleEventOfType(.Value, withBlock: { snapshot in
if let key = datesKey.key where key != self.userID
{
if allKeys.contains(key!) {
let newuser = FBUser(userKey: key!, dictionary: snapshot.value as! [String : AnyObject])
self.blockedByUsersRef.childByAppendingPath(key).childByAppendingPath(self.userID).observeSingleEventOfType(.Value, withBlock: { (snapshot) -> Void in
if let _ = snapshot.value as? NSNull
{
// we have not blocked this one
self.blockedByUsersRef.childByAppendingPath(self.userID).childByAppendingPath(key).observeSingleEventOfType(.Value, withBlock: { snapshot in
if let _ = snapshot.value as? NSNull
{
// we are not blocked by this one
if self.newKeys.contains(newuser.userKey) {}
else
{
self.users.append(newuser)
self.newKeys.append(newuser.userKey)
}
}
self.tableView.reloadData()
})
}
})
}
}
})
}
})
})
}
In essence users can be at a certain place at a certain date. They put down the date they are going to be there, as explained in code below. that date may overlap with other users that are going to be in that area, in a period ranging of say 7 days before until 21 days after. those users can be followed, blocked. but I’m getting those to display in the tableView. If they put in a different date or place, a different set of users will pop up.
if let userStartDate = beginningDate as? Double
{
let intUserStartDate = Int(userStartDate)
dateRangeStart = intUserStartDate - 604800
dateRangeEnd = intUserStartDate + 1814400
print(dateRangeStart, intUserStartDate, dateRangeEnd)
updateUsers()
}
else
{
updateUsersWithoutDate()
}
This may or may not be an answer or help at all but I want to throw it out there.
Given that you want to really look for two things: locations and times, we need some mechanics to handle it.
The locations are more static; i.e. the bowling ally will always be the bowling ally and the times are dynamic and we need a range. So, given a structure
{
"events" : {
"event_0" : {
"loc_time" : "bowling_5"
},
"event_1" : {
"loc_time" : "tennis_7"
},
"event_2" : {
"loc_time" : "tennis_8"
},
"event_3" : {
"loc_time" : "dinner_9"
}
}
}
This structure handles both criteria. You can easily query for all nodes that have location of tennis at a time of 7. You can also query the range for tennis from start time of 6 and end time of 9, which will return tennis_7 and tennis_8
Here's some ObjC code to do just that
Firebase *ref = [self.myRootRef childByAppendingPath:#"events"];
FQuery *query = [[[ref queryOrderedByChild:#"loc_time"]
queryStartingAtValue:#"tennis_6"] queryEndingAtValue:#"tennis_8"];
[query observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
NSLog(#"%#", snapshot);
}];
You can extrapolate from this substituting your locations for location and distance or timestamps for the time.
Another modification (and this may be obvious but stating it for clarity) is to use a reference to your locations instead of the actual name (bowling, tennis); i.e.
events
"event_0" : {
"loc_time" : "-JAY8jk12998f_20160311140200" // locationRef_timestamp
},
locations
-JAY8jk12998f : {
"loc_name": "Fernando's Hideaway"
}
Structuring your data in the way to want to get to it can significantly reduce your code (and the complexity of queries within queries etc).
Hope that helps.