How would I go about implementing a friends relationship between users, using Firebase in swift?
The following is my current DataService.swift file.
import Foundation
import Firebase
let DB_BASE = Database.database().reference()
class DataService {
// creates instance of the class inside itself
static let instance = DataService()
private var _REF_BASE = DB_BASE
private var _REF_USERS = DB_BASE.child("users")
var REF_BASE: DatabaseReference {
return _REF_BASE
}
var REF_USERS: DatabaseReference {
return _REF_USERS
}
func createDBUser(uid: String, userData: Dictionary<String,Any>) {
REF_USERS.child(uid).updateChildValues(userData)
}
}
Thanks :)
Not sure this is a very complete answer but may get you going in the right direction
Assume we have three users in Firebase and there is data is stored in a /users node. Larry (uid_0) has two friends, Moe and Curly
users
uid_0
name: "Larry"
friends:
uid_1: true
uid_2: true
uid_1
name: "Moe"
uid_2
name: "Curly"
The code to write the friends node is something like this
let usersRef = fbRed.child("users")
let uid0Ref = usersRef.child("uid_0")
let friendsRef = uid0Ref.child("friends")
friendsRef.child("uid_1").setValue(true)
friendsRef.child("uid_2").setValue(true)
That's pretty verbose and could be condensed but leaving that way for readability.
Note that if you are going to do queries or other functions in the future, this is probably not the best solution.
Each user should have a list of references to other users via their uid. Do not store the entire friend user data object under each user. This will greatly inflate your database.
users ->
"uid1" ->
friendUids ->
0 ->
"uid2"
1 ->
"uid3"
"uid2" ->
friendUids ->
0 ->
"uid1"
"uid3" ->
friendUids ->
0 ->
"uid1"
Assuming you have a static decode function on your user to handle the JSON from Firebase, all you need to do to retrieve the user data is:
extension DataService {
func requestUserWithUid(_ uid: String, completion: #escaping (User?) -> Void) {
REF_USERS.child(uid).observeSingleEvent(of: .value) { snapshot in
completion(User.decode(snapshot.value))
}
}
func requestFriendsForCurrentUser(completion: #escaping ([User?]?) -> Void) {
var friends = [User?]()
guard let uid = Auth.auth().currentUser?.uid else {
completion(.none)
return
}
requestUserWithUid(uid) { user in
guard let user = user else {
completion(.none)
return
}
for friendUid in user.friendUids {
requestUserWithUid(friendUid) { friend in
friends.append(friend)
}
}
completion(friends)
}
}
}
This is just limited example of many of how you could implement it. Let's assume your user data object has a name and we want to display a list of friends' names.
class FriendView: UIView, UITableViewDataSource {
fileprivate var friends = [User?]()
fileprivate let tableView = UITableView(frame: .zero, style: .plain)
// ----------------------------------------------------------------------------
// MARK: UIView
// ----------------------------------------------------------------------------
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(tableView)
tableView.frame = frame
tableView.dataSource = self
loadFriends()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder :) has not been implemented")
}
// ----------------------------------------------------------------------------
// MARK: Private
// ----------------------------------------------------------------------------
fileprivate func loadFriends() {
DataService.instance.requestFriendsForCurrentUser() { friends in
if let friends = friends {
self.friends = friends
self.tableView.reloadData()
}
}
}
// ----------------------------------------------------------------------------
// MARK: UITableViewDataSource
// ----------------------------------------------------------------------------
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return friends.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = friends[indexPath.row]?.name
return cell
}
}
Related
I've been trying to add a search users mechanism for quite a while now, but to no success. I can't seem to find much out there, and where there is explanation it is almost always for FirebaseDatabase rather than Firestore. I'm very new to Swift, Firebase and coding in general so any explanation about where I'm going wrong with this, and how I can implement this would be appreciated beyond words.
This is my latest attempt (I'm sure this will make many of you shudder):
import UIKit
import Firebase
import FirebaseAuth
import FirebaseFirestore
class FindUsersViewController: UITableViewController {
var usersArray = [String]()
var filteredUsers = [String]()
override func viewDidLoad() {
super.viewDidLoad()
fetchUser()
// Do any additional setup after loading the view.
}
func fetchUser() {
let db = Firestore.firestore()
let usernameSearch = db.collection("users")
usernameSearch.getDocuments { (snapshot, error) in
if error != nil {
print("Error obtaining usernames")
} else {
for document in snapshot!.documents {
let field = document.data()["username"]
self.usersArray.append(field as! String)
print(self.usersArray)
}
}
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 5
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
cell.textLabel?.text = usersArray
return cell
}
}
I can't quite seem to work it out, and because I'm new to everything, the Firebase Documentation seems more mystifying than enlightening, so any help with this would be greatly appreciated!
(I think I'm going wrong with Arrays...maybe...idk...)
--
I should add:
I have a collection of "users", with documents for each user containing the "username" field (along with uid, password and email fields).
You're almost there!
First let's create a User object that will represent all the properties for your user. For now let's say a user has an id, an name and an email, but feel free to match your project requirements:
import FirebaseFirestore
struct User {
let id: String
let name: String
let email: String
}
We also add an extension with a convenience initializer for our User, this will allow us to convert quickly and easily a Firestore Document to a User:
extension User {
init(queryDocument: QueryDocumentSnapshot) {
guard let name = queryDocument.data()["name"] as? String else { fatalError() }
guard let email = queryDocument.data()["email"] as? String else { fatalError() }
self.init(id: queryDocument.documentID, name: name, email: email)
}
}
Now, this is how your FindUsersViewController could look like:
import UIKit
import FirebaseFirestore
class FindUsersViewController: UITableViewController {
// Keep a reference to your database, and your users collection:
private let database = Firestore.firestore()
private lazy var usersReference = database.collection("users")
private let cellReuseIdentifier = "cellReuseIdentifier"
// This is the array that will keep track of your users, you will use it to populate your table view data:
private var users = [User]()
override func viewDidLoad() {
super.viewDidLoad()
// Set your table view datasource and delegate:
tableView.dataSource = self
tableView.delegate = self
// Register your cell identifier:
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseIdentifier)
fetchUsers()
}
private func fetchUsers() {
// Create your query, for instance we will return only the first 10 users and ordered by name:
let query = usersReference.order(by: "name", descending: true).limit(to: 10)
// Now simply fetch the documents that match the query:
query.getDocuments { [weak self] (querySnapshot, error) in
guard let self = self else { return }
guard let querySnapshot = querySnapshot, error == nil else {
// Oops, something wrong happened here
return
}
// You get successfully your documents here, so just transform them to your User model:
let users = querySnapshot.documents.map { User(queryDocument: $0) }
// Then fill your array, and make sure you reload your table data on the main thread:
DispatchQueue.main.async {
self.users = users
self.tableView.reloadData()
}
}
}
// MARK: UITableViewDataSource methods
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Returns the number of users in your array:
return users.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath)
// Configure your cell here via your users array:
cell.textLabel?.text = users[indexPath.row].name
return cell
}
// MARK: UITableViewDelegate methods
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
This will give you a nice list of your users. I added a lot of comments in the view controller so I think it's pretty self-explanatory, but feel free to let me know if you still have a doubt.
Good luck with your app! :)
Hello I have a tableviewcell where i can populate it with custom data from my pc, but i can't use my firebase data on the cell that i have made. I want to fill my cell with String and Int, not only Strings. My code is:
PlacesTableViewController Class
import UIKit
import FirebaseDatabase
class PlacesTableViewController: UITableViewController {
//MARK: Properties
#IBOutlet weak var placesTableView: UITableView!
//database reference
var dbRef:FIRDatabaseReference?
var places = [Places]()
var myList:[String] = []
//handler
var handle:FIRDatabaseHandle?
override func viewDidLoad() {
super.viewDidLoad()
dbRef = FIRDatabase.database().reference()
// Loads data to cell.
loadData()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return places.count
//return myList.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "PlacesTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? PlacesTableViewCell else {
fatalError("The dequeued cell is not an instance of PlacesTableView Cell.")
}
let place = places[indexPath.row]
cell.placeLabel.text = place.name
cell.ratingControl.rating = place.rating
//cell.placeLabel.text = myList[indexPath.row]
//cell.ratingControl.rating = myRatings[indexPath.row]
return cell
}
//MARK: Private Methods
private func loadData() {
handle = dbRef?.child("placeLabel").observe(.childAdded, with: { (snapshot) in
if let item = snapshot.value as? String
{
self.myList.append(item)
self.placesTableView.reloadData()
print (item)
}
})
/* handle = dbRef?.child("rating").observe(.childAdded, with: { (snapshot) in
if let item = snapshot.value as? String
{
self.myList.append(item)
self.placesTableView.reloadData()
}
})*/
/*guard let place1 = Places(name: "Veranda", rating: 4) else {
fatalError("Unable to instantiate place1")
}
places += [place1]*/
}
}
Places Class
import UIKit
class Places {
//MARK: Properties
var name: String
var rating: Int
//MARK:Types
struct PropertyKey {
static let name = "name"
static let rating = "rating"
}
//MARK: Initialization
init?(name: String, rating: Int) {
// Initialize stored properties.
self.name = name
self.rating = rating
// Initialization should fail if there is no name or if the rating is negative.
// The name must not be empty
guard !name.isEmpty else {
return nil
}
// The rating must be between 0 and 5 inclusively
guard (rating >= 0) && (rating <= 5) else {
return nil
}
}
}
PlacesTableViewCell Class
import UIKit
import FirebaseDatabase
class PlacesTableViewCell: UITableViewCell, UITableViewDelegate {
//MARK: Properties
#IBOutlet weak var placeLabel: UILabel!
#IBOutlet weak var ratingControl: RatingControl!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Firebase Database
Assuming your database layout should instead look like this (see comments above):
...
placeLabel
|
-- XXY: "Veranda"
-- YYY: "Dio Con Dio"
rating
|
-- XXX: 4
-- YYY: 1
...
then try this:
private func loadData() {
dbRef!.child("placeLabel").observe(.childAdded) {
(snapshot) in
let label = snapshot.value as! String
self.updatePlace(snapshot.key, label: label)
}
dbRef!.child("rating").observe(.childAdded) {
(snapshot) in
let rating = snapshot.value as! Int
self.updatePlace(snapshot.key, rating: rating)
}
}
private var loadedLabels = [String: String]()
private var loadedRatings = [String: Int]()
private func updatePlace(_ key: String, label: String? = nil, rating: Int? = nil) {
if let label = label {
loadedLabels[key] = label
}
if let rating = rating {
loadedRatings[key] = rating
}
guard let label = loadedLabels[key], let rating = loadedRatings[key] else {
return
}
if let place = Places(name: label, rating: rating) {
places.append(place)
placesTableView.reloadData()
}
}
By the way, you can temporarily hack your database — using Firebase (nice!) web console — if you want to quickly validate the above solution.
Writing to Database. Try the following code to write the nodes in your database (i.e., this code reuses the same key across all place properties):
let key = dbRef!.child("placeLabel").childByAutoId().key
dbRef!.child("placeLabel").child(key).setValue(placeLabel.text)
dbRef!.child("comment").child(key).setValue(commentTextField.text)
dbRef!.child("rating").child(key).setValue(ratingControl.rating)
Hacking the Database. To edit the database manually, try:
open http://console.firebase.google.com
select your app
open database option
add a new node with the right key
delete the old node
I am using Swift with Realm to build an unidirectional data flow App.
I am wondering why I can not use an object as current application state.
var people is always updated when I add new person but var oldestPerson is never updated.
This is my Store.swift file
class Person: Object {
dynamic var name: String = ""
dynamic var age: Int = 0
}
// MARK: Application/View state
extension Realm {
var people: Results<Person> {
return objects(Person).sorted("age")
}
var oldestPerson: Person? {
return objects(Person).sorted("age").first
}
}
// MARK: Actions
extension Realm {
func addPerson(name: String, age: Int) {
do {
try write {
let person = Person()
person.name = name
person.age = age
add(person)
}
} catch {
print("Add Person action failed: \(error)")
}
}
}
let store = try! Realm()
The state of oldest in my view layer never change but people change as it should.
import RealmSwift
class PersonsTableViewController: UITableViewController {
var notificationToken: NotificationToken?
var people = store.people
var oldest = store.oldestPerson
override func viewDidLoad() {
super.viewDidLoad()
updateView()
notificationToken = store.addNotificationBlock { [weak self] (_) in
self?.updateView()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return people.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("PersonTableViewCell") as! PersonTableViewCell
cell.person = people[indexPath.row]
return cell
}
func updateView() {
print(oldest)
tableView.reloadData()
}
}
Change your declaration of
var oldest = store.oldestPerson
to:
var oldest: Person? { return store.oldestPerson }
So here is my Problem.. I've retrieved the user data from _User class in Parse and showing it in the app as follows:
var data:NSMutableArray = NSMutableArray()
func loadData() {
data.removeAllObjects()
var userQuery = PFUser.query()
userQuery?.orderByAscending("createdAt")
userQuery?.findObjectsInBackgroundWithBlock({ (objects, erroe) -> Void in
if let objects = objects {
for object in objects {
if let user = object as? PFUser {
if user.objectId != PFUser.currentUser()?.objectId {
self.data.addObject(object)
}
self.tableView.reloadData()
}
}
}
})
}
override func viewDidLoad() {
super.viewDidLoad()
loadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Potentially incomplete method implementation.
// Return the number of sections.
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete method implementation.
// Return the number of rows in the section.
return data.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let myCell = tableView.dequeueReusableCellWithIdentifier("users", forIndexPath: indexPath) as! userListTableViewCell
let userData:PFObject = self.data.objectAtIndex(indexPath.row) as! PFObject
// Usernames and gender..
myCell.fullName.text = userData.objectForKey("fullName") as! String!
myCell.genderLabel.text = userData.objectForKey("gender") as! String!
// Profile pictures..
let profilePics = userData.objectForKey("profilePicture") as! PFFile
profilePics.getDataInBackgroundWithBlock { (data, error) -> Void in
if let downloadedImage = UIImage(data: data!) {
myCell.dp.image = downloadedImage
}
}
return myCell
}
Now I want to add a follow button to follow a particular user and want to save that data in Parse. So my questions are:
How can I add the follow button effectively which would not mess up things if there are so many users.. I've tried giving them tag like myCell.followButton.tag = indexPath.row but it dint work well. so I want to know about some other way for achieving my goal.
What is the best possible way to save the follower list in Parse.. I'm thinking to make a class named Followers having the columns user : the user being followed and follower where we can add the PFUser.currentUser().objectId . Is there anything better than this or this is a nice method to do it?
here is the screen shot of my userListTableViewController..
Here you can see the followButton which i've already connected to userListTableViewCell.. Please help me out.. Thanks for your time..
Lets make that button you want a UILabel instead and add a UIGestureRecognizer to it, one like this
Create a swift file named PFUserTapGestureRecognizer.swift
import UIKit
class PFUserTapGestureRecognizer: UITapGestureRecognizer
{
var tapper: PFUserTapper
init(id: Int, onTap: (id: Int) -> Void)
{
self.tapper = PFUserTapper()
self.tapper.id = id
self.tapper.onTap = onTap
super.init(target: self.tapper, action: Selector("userTapped"))
}
}
class PFUserTapper : NSObject
{
var id = 0
var onTap: ((idUse: Int) -> Void)?
func userTapped()
{
onTap?(idUser: id)
}
}
Now, when you load your cell in your view controller where you are loading your UITableView, in the delegate tableView:cellForRowAtIndexPath:, do this:
// Check if you are following that user already
// if im NOT following this user then
{
let idUser = userData.objectForKey("id") as! Int
myCell.labelFollow.tag = idUser
myCell.labelFollow.text = "Follow"
addFollowBehavior(follow: true, idUser: idUser, label: myCell.labelFollow)
}
// else if i'm following the user
{
myCell.labelFollow.text = "Unfollow"
addFollowBehavior(follow: false, idUser: idUser, label: myCell.labelFollow)
}
// else.. you should consider the case when you click Follow and it takes time to get an answer from your service
And create this function
func addFollowBehavior(follow follow: Bool, idUser: Int, label: UILabel)
{
if let recognizers = label.gestureRecognizers {
for recognizer in recognizers {
label.removeGestureRecognizer(recognizer as! UIGestureRecognizer)
}
}
label.addGestureRecognizer(PFUserTapGestureRecognizer(idUser,
onTap: { (idUser) -> Void in
// THIS IS WHERE YOU SAVE TO PARSE, WEB SERVICE OR WHATEVER
// if follow then
// Do Follow
// else
// UnFollow...
// On the callback write this to protect from reused cells:
if label.tag = idUser {
myCell.labelFollow.text = follow ? "Unfollow" : "Follow" // NOW (UN)FOLLOWING!
addFollowBehavior(follow: !follow, idUser: idUser, label: myCell.labelFollow)
}
}
)
}
I didn't had the chance to test it but I have a very similar implementation on a requirement like that and it works perfectly
Note: this works if the follow item is an UIImageView or whatever inherits from UIView for a cooler look, instead of changing the text, you change the UIImage
I came across the following UITableViewController code and wanted to implement the associated classes and functions that go with them in order for me to learn a little bit more about Swift.
I'm not sure what the implementation of api.getRooms() looks like. I think it may be a closure, but I'm not entirely sure?
My question is what would api.getRooms() be returning considering there's {} usage? If anyone could explain to me what's going on that would be greatly appreciated.
api.getRooms(User.currentUser()!) { (roomsObj, error) in
if let rooms = roomsObj as? Array<Room> {
self.rooms = rooms
self.tableView.reloadData()
if (viaPullToRefresh) {
self.refreshControl.endRefreshing()
}
}
}
PullViewController.swift
class PullViewController: UITableViewController {
var rooms = Array<Room>()
init(coder aDecoder: NSCoder!) {
super.init(coder: aDecoder)
// Custom initialization
assert(User.currentUser())
}
override func viewDidLoad() {
self.refreshControl = UIRefreshControl()
self.refreshControl.addTarget(self, action: Selector("refreshInvoked"), forControlEvents: UIControlEvents.ValueChanged)
refresh()
}
override func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
return rooms.count
}
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath:NSIndexPath!) -> UITableViewCell! {
var cell: UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("Cell") as? UITableViewCell
if !cell {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier:"Cell")
}
let room = rooms[indexPath.row]
cell!.textLabel.textColor = UIColor.blackColor()
cell!.textLabel.text = "\(room.name)(\(room.messageCount))"
return cell
}
func refreshInvoked() {
refresh(viaPullToRefresh: true)
}
func refresh(viaPullToRefresh: Bool = false) {
let api = API()
api.getRooms(User.currentUser()!) { (roomsObj, error) in
if let rooms = roomsObj as? Array<Room> {
self.rooms = rooms
self.tableView.reloadData()
if (viaPullToRefresh) {
self.refreshControl.endRefreshing()
}
}
}
}
}
User.swift
class User {
init() {
}
class func currentUser() -> Bool {
return true
}
}
Room.swift
class Room {
var name: String
var messageCount: Int
init() {
}
}
API.swift (not sure whether this is implemented correctly).
class API {
init() {
}
func getRooms(user: User) -> (Array<Room>, String) { // ??
// ??
}
}
In you room class, you did not initialize the name and message count variable, in swift only optional variable can be nil
class Room {
var name: String
var messageCount: Int
init(name:String, messageCount:Int) {
self.name = name
self.messageCount = messageCount
}
}
also assert need to evaluate to bool so
assert(User.currentUser() != nil)
1) You are sending assert a void since your currentUser method doesn't return anything, so assert wouldn't know if this is good or bad. Therefore you need to make currentUser return a BOOL or something else if you want to use it like this, but it needs some sort of BOOL result to tell if it is asserting correctly or not. Hopefully that makes sense.
2)You are trying to feed your getRooms function a lambda function instead of running a function after the results.
--Update--
If you are wanting a completion lambda then you'll want to write getRooms like this:
func getRooms(user: User, completion: ((Array<Room>,String?) -> Void)?) -> Array<Room> {//String because I have no idea what type they want for errors
var room = Room()
room.messageCount = 0
room.name = "Room1"
var rooms = Array<Room>()
rooms.append(room)
completion?(rooms,nil)
return rooms
}
or something along those lines
This is how I implemented the api.getRooms() function:
API.swift
class API {
func getRooms(user: User, completion: (Array<Room>, String) -> ()) {
var room = Room(name: "Room1", messageCount: 0)
var rooms = Array<Room>()
rooms.append(room)
completion(rooms, "error")
}
}
PullViewController.swift
func refresh(viaPullToRefresh: Bool = false) {
let api = API()
if let user = User.currentUser() {
api.getRooms(user) { (roomsObj, error) in
self.rooms = roomsObj
self.tableView.reloadData()
if (viaPullToRefresh) {
self.refreshControl.endRefreshing()
}
}
}
}