I'm trying to do small apps containing firebase codes to learn more about it. so here I made a todo list app, I was able to add the tasks to the firebase and I was able to delete it, the problem I have is updating the status of the task (isComplete: Bool) I've no idea how to write firebase code to update data. almost all the tutorial I read was about the data uploaded to real-time database and I'm using the cloud so I couldn't figure it out. here I wrote this code so when the task is done and I select the cell the circle turn into checkmark.circle It's work but of course the database isn't updated..
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! TodoCell
if cell.isComplete == false{
cell.doneButton.image = UIImage(systemName: "checkmark.circle")
cell.isComplete = true
} else {
cell.doneButton.image = UIImage(systemName: "circle")
cell.isComplete = false
}
}
}
Adding tasks to firebase codes
public func postTask(task:String, isComplete: Bool,
completion: #escaping (Result<Bool, Error>) -> ()) {
guard let user = Auth.auth().currentUser else {
return
}
let documentRef = db.collection(DatabaseService.itemsCollection).document()
db.collection(DatabaseService.usersCollection).document(user.uid).
collection(DatabaseService.tasksCollection).
document(documentRef.documentID).setData(["task" : task,
"isComplete": isComplete,
"taskId": documentRef.documentID])
{ (error) in
if let error = error {
completion(.failure(error))
} else {
completion(.success(true))
}
}
}
SnapshotListener
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
guard let user = Auth.auth().currentUser else {
return
}
listener = Firestore.firestore().collection(DatabaseService.usersCollection)
.document(user.uid).collection(DatabaseService.tasksCollection)
.addSnapshotListener({ [weak self] (snapshot, error) in
if let error = error {
DispatchQueue.main.async {
self?.showAlert(title: "Try Again", message:
error.localizedDescription)
}
} else if let snapshot = snapshot {
let task = snapshot.documents.map { TasksList($0.data()) }
self?.todoItems = task
}
})
}
based on #bkbkchoy answer I wrote these codes:
func updateTask(task: TasksList,
isComplete: Bool,
completion: #escaping (Result<Bool, Error>) -> ()) {
guard let user = Auth.auth().currentUser else { return }
db.collection(DatabaseService.usersCollection).document(user.uid)
.collection(DatabaseService.tasksCollection).document(task.taskId)
.updateData(["isComplete": isComplete]) { (error) in
if let error = error {
completion(.failure(error))
} else {
completion(.success(true))
}
}
}
}
and under didSelectRow
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! TodoCell
let isComplete = false
if task2.isComplete == false{
cell.doneButton.image = UIImage(systemName: "checkmark.circle")
cell.doneButton.tintColor = .systemBlue
cell.isComplete = true
} else {
cell.doneButton.image = UIImage(systemName: "circle")
cell.doneButton.tintColor = .systemGray
cell.isComplete = false
}
updateStatus(isComplete: isComplete)
}
private func updateStatus(isComplete: Bool) {
databaseService.updateTask(task: task2, isComplete: isComplete)
{ [weak self] (result) in
switch result {
case .failure(let error):
DispatchQueue.main.async {
self?.showAlert(title: "Try again", message: error.localizedDescription)
}
case .success:
break
}
}
}
}
but I got an error :
No document to update: project/todo-list/database/(default)/documents/users/jYZmghQeXodeF2/tasks/1
struct TasksList {
let task: String
let taskId: String
let isComplete: Bool
}
extension TasksList {
init(_ dictionary: [String: Any]) {
self.task = dictionary["task"] as? String ?? ""
self.taskId = dictionary["taskId"] as? String ?? ""
self.isComplete = dictionary["isComplete"] as? Bool ?? false
}
}
There are a couple of ways to update documents in Cloud Firestore:
Rewrite a specific property using setData:
db.collection(DatabaseService.usersCollection)
.document(user.uid)
.collection(DatabaseService.tasksCollection)
.document(task.taskId)
.setData(["isComplete": isComplete], merge: true)
Note: if you use setData, you must include merge: true to overwrite a single property on an existing document or else the whole document will be overwritten.
Use updateData
db.collection(DatabaseService.usersCollection)
.document(user.uid)
.collection(DatabaseService.tasksCollection)
.document(task.taskId)
.updateData(["isComplete": isComplete]) { err in
if let err = err {
print("error updating document: \(err)")
} else {
print("doc successfully updated")
}
}
Firestore has some great documentation online. If you want to learn more about updating/adding data here's a good place to start.
Your approach cannot work.
The cell is just the view, it shows the UI elements and their values, the data source is the model TasksList (why not simply Task).
Cells are reused and you will lose the isCompleted information in the cell when the user scrolls. You have to update the model and reload the view
First of all declare the model as Task and isComplete as variable. According to the naming guidelines task should be name or title and taskId should be just id
struct Task {
let task: String
let taskId: String
var isComplete: Bool
}
In cellForRow set the UI elements in the cell according to the model
let task = todoItems[indexPath.row]
let imageName = task.isComplete ? "checkmark.circle" : "circle"
cell.doneButton.image = UIImage(systemName: imageName)
cell.doneButton.tintColor = task.isComplete ? .systemBlue : .systemGray
In didSelect toggle isComplete in the model, reload the row and save the task
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
todoItems[indexPath.row].isComplete.toggle()
tableView.reloadRows(at: [indexPath], with: .none)
let task = todoItems[indexPath.row]
updateTask(task: task, isComplete: task.isComplete) { result in print(result) }
}
As the whole task is handed over, the second parameter isComplete is not needed in updateTask.
Related
Hello im doing to do app with firebase. I'm using collection view for show saved user task. When i fetch my tasks i see my fetch data at console properly. Nothing problem with that. I can get my data. But when i try to present my tasks at my collection view i see blank cells or i see random one or two tasks. When i try to add new task, for example i have total 7 task but my collection view have 12 cell. I'm saving one task but my collection view total cell have more than one.
As you can see down below image i have total 2 task saved at firebase but my collection view showing 6 cell. Also not present my tasks at cell text.
So this is my how to save my task function.
#objc func addTaskButtonClicked() {
if NewTaskViewController.textView.text == "" {
makeAlert(titleInput: "Error", messageInput: "Please write your task.")
}else {
guard let currentUid = Auth.auth().currentUser?.uid else {return}
guard let text = NewTaskViewController.textView.text else {return}
let taskId = NSUUID().uuidString
let data = [
"text" : text,
"timestamp" : Timestamp(date: Date()),
"taskId" : taskId,
] as [String : Any]
Firestore.firestore().collection("tasks").document(currentUid).collection("ongoing_tasks").document(taskId).setData(data)
// NewTaskViewController.textView.text = ""
}
}
This is my how to fetch data from firebase function.
func fetchTasks(uid : String,completion : #escaping([Task])-> Void) {
guard let uid = Auth.auth().currentUser?.uid else {return}
var tasks = [Task]()
Firestore.firestore().collection("tasks").document(uid).collection("ongoing_tasks").order(by: "timestamp").addSnapshotListener { snapshot, error in
if let err = error {
print(err)
}else {
if let snapShotDocument = snapshot?.documents {
for doc in snapShotDocument {
let data = doc.data()
tasks.append(Task(data: data))
print(data)
print(tasks)
completion(tasks)
TasksViewController.collectionView.reloadData()
}
}
}
}
}
This is my Task struct.
struct Task {
let tasksID : String
let text : String
let timestamp : Timestamp
init(data : [String : Any]) {
self.tasksID = data["taskId"] as? String ?? ""
self.text = data["text"] as? String ?? ""
self.timestamp = data["timestamp"] as? Timestamp ?? Timestamp(date: Date())
}
}
I'm using two get set for user and task.
var user : User? {
didSet {
configure()
}
}
private func configure() {
guard let user = self.user else {return}
TasksViewController.nameLabel.text = "Hi \(user.name)👋🏻"
fetchTasks()
}
private func fetchTasks() {
guard let uid = self.user?.uid else {return}
print(uid)
fetchTasks(uid: uid) { tasks in
print(uid)
TasksViewController.tasks = tasks
}
}
var task : Task? {
didSet {
configure()
}
}
private func configure() {
guard let task = self.task else {return}
TasksViewCell.textLabel.text = task.text
}
And this is how to present my tasks at my collection view.
static var tasks = [Task]()
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return TasksViewController.tasks.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TasksViewController.reuseIdentifier, for: indexPath) as! TasksViewCell
cell.task = TasksViewController.tasks[indexPath.row]
return cell
}
So how can i solve this problem. Thanks for any help. Love y'all.
I am writing an application to track the cryptocurrency exchange rate. The api has separate url requests for each coin. Here is a JSON response coming from the server, to a request for one coin:
{
"status": {
"elapsed": 2,
"timestamp": "2022-08-23T06:10:16.417580964Z"
},
"data": {
"id": "1e31218a-e44e-4285-820c-8282ee222035",
"serial_id": 6057,
"symbol": "BTC",
"name": "Bitcoin",
"slug": "bitcoin",
"contract_addresses": null,
"_internal_temp_agora_id": "9793eae6-f374-46b4-8764-c2d224429791",
"market_data": {
"price_usd": 20946.467798282705,
"price_btc": 1,
"price_eth": 13.351682485155417,
"volume_last_24_hours": 7635594314.553516,
"real_volume_last_24_hours": 6038552423.10257,
"volume_last_24_hours_overstatement_multiple": 1.2644742944254175,
"percent_change_usd_last_1_hour": null,
"percent_change_btc_last_1_hour": null,
"percent_change_eth_last_1_hour": null,
"percent_change_usd_last_24_hours": -2.1478472228280485,
"percent_change_btc_last_24_hours": 0.11113305637977958,
"percent_change_eth_last_24_hours": 0.0518833986287626,
"ohlcv_last_1_hour": null,
"ohlcv_last_24_hour": null,
"last_trade_at": "2022-08-23T06:10:15Z"
}
I need to send several url requests and convert the received responses into a table where each cell is a certain coin corresponding to a certain url request.
I wrote a model and a service layer, but when sending two requests, instead of two cells in the table, I get one cell that displays data from the 1st request, and then abruptly changes to data from the second request.
The code is given below:
Сontroller
final class WalletController: UIViewController {
private let walletTable = UITableView()
private let service = WalletService()
private var data: [DataWallet] = []
private let identifier = "walletCell"
private var pointSorted = 1
private let queue = DispatchQueue.main
// MARK: Life cycle
override func viewDidLoad() {
super.viewDidLoad()
setUpView()
configData()
}
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.setNavigationBarHidden(false, animated: false)
self.navigationItem.setHidesBackButton(true, animated: true)
}
// MARK: setUpView
private func setUpView() {
// NavigationBar
createCustomNavigationBar()
// LogOutBarButton
let logOutButton = createCustomButton(titleName: "LogOut", selector: #selector(logOutButtonTapped))
navigationItem.leftBarButtonItem = logOutButton
// SortedBarButton
let sortedButton = createCustomButton(titleName: "Sorted", selector: #selector(sortedButtonTapped))
navigationItem.rightBarButtonItem = sortedButton
// TableView
walletTable.backgroundColor = #colorLiteral(red: 0.9381344914, green: 0.9331676364, blue: 0.9246369004, alpha: 1)
walletTable.separatorColor = #colorLiteral(red: 0.1599435508, green: 0.185090214, blue: 0.167404592, alpha: 1)
walletTable.delegate = self
walletTable.dataSource = self
walletTable.register(UITableViewCell.self, forCellReuseIdentifier: identifier)
view.addSubview(walletTable)
walletTable.snp.makeConstraints { maker in
maker.left.top.right.bottom.equalToSuperview().inset(0)
}
}
#objc private func logOutButtonTapped() {
let startController = StartController()
navigationController?.pushViewController(startController, animated: true)
}
private func configData() {
service.addCoin { [weak self] result in
switch result {
case .success(let dataBoy):
self?.data = [dataBoy]
DispatchQueue.main.async {
self?.walletTable.reloadData()
}
case.failure(let error):
print(error)
}
}
}
#objc private func sortedButtonTapped() {
if pointSorted == 1 {
data = data.sorted{ $0.capital < $1.capital }
pointSorted = pointSorted - 1
} else {
data = data.sorted{ $0.country < $1.country }
pointSorted = pointSorted + 1
}
walletTable.reloadData()
}
}
// MARK: Delegate
extension WalletController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
}
// MARK: DataSource
extension WalletController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = walletTable.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
cell.backgroundColor = #colorLiteral(red: 0.9381344914, green: 0.9331676364, blue: 0.9246369004, alpha: 1)
let coin = data[indexPath.row]
var content = cell.defaultContentConfiguration()
content.text = coin.symbol
content.secondaryText = String(coin.market_data.percent_change_usd_last_1_hour ?? 0.0)
cell.contentConfiguration = content
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let coin = data[indexPath.row]
let infoController = InfoController()
queue.async {
infoController.firstTextLabel.text = coin.name
infoController.firstNumberLabel.text = coin.symbol
infoController.secondTextLabel.text = coin.name
infoController.secondNumberLabel.text = String(coin.market_data.price_btc ?? 0.0)
infoController.thirdTextLabel.text = coin.name
infoController.thirdNumberLabel.text = String(coin.market_data.price_usd ?? 0.0)
}
navigationController?.pushViewController(infoController, animated: true)
}
}
Service layer
final class WalletService {
func addCoin(completion: #escaping (Result<DataWallet, Error>) -> Void) {
guard let urlBtc = URL(string: "https://data.messari.io/api/v1/assets/btc/metrics") else { return }
guard let urlEth = URL(string: "https://data.messari.io/api/v1/assets/eth/metrics") else { return }
let taskBtc = URLSession.shared.dataTask(with: urlBtc) { data, _, error in
if let error = error {
completion(.failure(error))
} else if let data = data {
do {
let result = try JSONDecoder().decode(Response.self, from: data)
completion(.success(result.data))
} catch {
completion(.failure(error))
}
}
}
taskBtc.resume()
let taskEth = URLSession.shared.dataTask(with: urlEth) { data, _, error in
if let error = error {
completion(.failure(error))
} else if let data = data {
do {
let result = try JSONDecoder().decode(Response.self, from: data)
completion(.success(result.data))
} catch {
completion(.failure(error))
}
}
}
taskEth.resume()
}
}
Model
struct Response: Codable {
let status: Status
let data: DataWallet
}
struct Status: Codable {
let elapsed: Int?
let timestamp: String?
}
struct DataWallet: Codable {
let id: String?
let symbol: String?
let name: String?
let market_data: MarketData
}
struct MarketData: Codable {
let price_usd: Double?
let price_btc: Double?
let percent_change_usd_last_1_hour: Double?
let percent_change_btc_last_1_hour: Double?
}
Can you tell me what I'm doing wrong and how to fix this situation?
I will be grateful for any help!
Your controlflow is wrong. For now if you call your service addCoin function you are calling the completionhandler twice. Everytime you are calling the completionhandler your data array gets set containing only the newest value:
self?.data = [dataBoy]
The most simple solution here would be to append the data instead of setting it.
self?.data.append(dataBoy)
and you would need to clear [data] at the start of the function.
private func configData() {
data = [] //<- add this to clear the collection
service.addCoin { [weak self] result in
switch result {
case .success(let dataBoy):
self?.data.append(dataBoy)
DispatchQueue.main.async {
self?.walletTable.reloadData()
}
case.failure(let error):
print(error)
}
}
}
I have added chatting/messaging to my application. I can go to a users profile, select message and message them.
I also added a tab that show all the users I messaged which is in a table view and you can click a user and see messages between you and them.
The issue happens when I open this tab to see the users Ive messaged when I've messaged more than 10 users. The simulator crashes and I get this error:
"Thread 1: Exception: "Invalid Query. 'in' filters support a maximum of 10 elements in the value array."
here is my code:
import UIKit
import FirebaseFirestore
class MessagingVC: UIViewController {
#IBOutlet weak var tableView: UITableView! { didSet {
tableView.tableFooterView = UIView()
tableView.contentInset.top = 10
}}
#IBOutlet weak var noDataLabel: UILabel!
private let db = Firestore.firestore()
var users = [AppUser]()
var user: AppUser {
return UserManager.currentUser!
}
private var lastMessageDict: [String: Date]?
private var unreadMessageDict: [String: Bool]?
var chatListener: ListenerRegistration?
override func viewDidLoad() {
super.viewDidLoad()
getChats()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
noDataLabel.isHidden = true
// remove any blocked users when entering the screen.
users.removeAll() { self.user.blockedUsers?.contains($0.uid) == true }
// filter hidden users
users = users.filter { $0.isHidden ?? false == false }
tableView.reloadData()
// getChats()
}
// Get all users id that the current user has been chating with.
func getChats() {
let chatCollection = db.collection("chat")
chatListener = chatCollection.whereField("participants", arrayContains: user.uid).addSnapshotListener { [unowned self] (querySnapshot, error) in
guard error == nil else {
print(error!.localizedDescription)
return
}
var userChatIds = [String]()
self.lastMessageDict = [String: Date]()
self.unreadMessageDict = [String: Bool]()
for chat in querySnapshot?.documents ?? [] {
let chatData = chat.data()
if let participants = chatData["participants"] as? [String] {
for particiant in participants {
if particiant != self.user.uid,
!(self.user.blockedUsers?.contains(particiant) == true),
!userChatIds.contains(particiant) {
userChatIds.append(particiant)
if let lastMessageDate = chatData["last message"] as? Timestamp {
self.lastMessageDict![particiant] = lastMessageDate.dateValue()
}
if let unreadMessageDict = chatData["unread message"] as? [String: Bool],
let unreadMesage = unreadMessageDict[self.user.uid] {
self.unreadMessageDict![particiant] = unreadMesage
}
}
}
}
}
if !userChatIds.isEmpty {
self.getChatsInfo(chatIds: userChatIds)
} else {
self.tableView.reloadData()
self.noDataLabel.isHidden = self.users.count > 0
}
}
}
func getChatsInfo(chatIds: [String]) {
getUsersForChat(chatIds) { (users, error) in
guard error == nil else {
print(error?.localizedDescription ?? "")
return
}
for user in users {
if let index = self.users.firstIndex(of: user) {
self.users[index] = user
} else {
self.users.append(user)
}
}
// self.users = users
self.users.sort { (first, second) -> Bool in
let firstDate = self.lastMessageDict?[first.uid]
let secondDate = self.lastMessageDict?[second.uid]
if firstDate == nil { return false }
else if secondDate == nil { return true }
else {
return firstDate! > secondDate!
}
}
self.users = self.users.filter { $0.isHidden ?? false == false }
self.tableView.reloadData()
self.noDataLabel.isHidden = self.users.count > 0
}
}
func getUsersForChat(_ ids: [String], completion:#escaping (_ users: [AppUser], _ error: Error?)->()) {
var allUsers = [AppUser]()
let allids = self.users.map { $0.uid }
let ids = ids.filter { !allids.contains($0) }
if ids.count == 0 {
completion(allUsers, nil)
return
}
var error: Error?
let userCollection = db.collection("users")
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
userCollection.whereField("uid", in: ids).getDocuments {querySnapshot,err in
error = err
var users = [AppUser]()
for document in querySnapshot?.documents ?? [] {
if let restaurant = Restaurant(snapshot: document) {
users.append(restaurant)
}
}
allUsers.append(contentsOf: users)
dispatchGroup.leave()
}
dispatchGroup.enter()
let userCollection2 = db.collection("users2")
userCollection2.whereField("uid", in: ids).getDocuments {querySnapshot,err in
error = err
var users = [AppUser]()
for document in querySnapshot?.documents ?? [] {
if let user = AppUser(snapshot: document) {
users.append(user)
}
}
allUsers.append(contentsOf: users)
dispatchGroup.leave()
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completion(allUsers, error)
}
}
deinit {
chatListener?.remove()
}
}
extension MessagingVC: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as! MessagingTableViewCell
let user = users[indexPath.row]
cell.nameLabel.text = user.firstName + " " + user.lastName
if let unreadMessage = unreadMessageDict?[user.uid],
unreadMessage == true {
cell.unreadMessageIndicatorLabel.isHidden = false
} else {
cell.unreadMessageIndicatorLabel.isHidden = true
}
cell.photoImageView.image = nil
user.getProfileImage { (image) in
DispatchQueue.main.async {
if let cell = tableView.cellForRow(at: indexPath) as? MessagingTableViewCell {
cell.photoImageView.image = image
}
}
}
if let rest = user as? Restaurant {
cell.categoryImageView.image = UIImage(named: rest.Categories1)
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let user = users[indexPath.row]
// unreadMessageDict?[user.uid] = false
performSegue(withIdentifier: "messagingToChatSegue", sender: user)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "messagingToChatSegue" {
let chatVC = segue.destination as! ChatViewController
let otherUser = sender as! AppUser
chatVC.currentUser = user
chatVC.otherUser = otherUser
chatVC.channelId = ClickedResturantVC.generateIdForMessages(user1Id: user.uid, user2Id: otherUser.uid)
}
}
}
Here is a picture of the error I am getting
Here is a picture of my Firestore database
The error message is telling you that Firestore doesn't support more than 10 items in the ids array that you pass to this query filter:
userCollection.whereField("uid", in: ids)
According to the documentation for this sort of query:
Note the following limitations for in and array-contains-any:
in and array-contains-any support up to 10 comparison values.
If you need more than 10, you will need to batch them into multiple queries.
I am trying to make simple firebase realtime chat app in swift. I have node in my database where number of unread messages is stored/
I have custom tableview cell which has label for displaying number of unread messages
class UserCell: UITableViewCell {
var message: Message? {
didSet {
setNumberOfUnreadMessages()
}
fileprivate func setNumberOfUnreadMessages() {
if let partnerId = message?.chatPartnerId(), let selfId = message?.selfId() {
let unreadMessagesRef = Database.database().reference().child("unread-messages").child(selfId).child(partnerId).child("numberOfUnreadMessages")
unreadMessagesRef.observe(.value, with: { (snapshot) in
if let count = snapshot.value as? Int {
self.unreadMessagesCountLabel.isHidden = false
self.unreadMessagesCountLabel.text = String(count)
} else {
self.unreadMessagesCountLabel.isHidden = true
}
print(snapshot)
}, withCancel: nil)
}
}
My tableView
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as? UserCell
let message = messages[indexPath.row]
cell?.message = message
return cell!
}
func observeUserMessages() {
guard let uid = Auth.auth().currentUser?.uid else { return }
// getting reference to current user's node
let ref = Database.database().reference().child("user-messages").child(uid)
ref.observe(.childAdded, with: { (snapshot) in
let userId = snapshot.key
// getting reference to partners node in user's node
let userMessagesRef = Database.database().reference().child("user-messages").child(uid).child(userId)
userMessagesRef.observe(.childAdded, with: { (snapshot) in
if let dictionary = snapshot.value as? [String:AnyObject] {
let message = Message(dictionary: dictionary)
if let chatPartnerId = message.chatPartnerId() {
self.messagesDictionary[chatPartnerId] = message
}
self.attemptReloadOfTableView()
}
}, withCancel: nil)
}, withCancel: nil)
ref.observe(.childRemoved, with: { (snapshot) in
print(snapshot.key)
self.messagesDictionary.removeValue(forKey: snapshot.key)
self.attemptReloadOfTableView()
}, withCancel: nil)
}
// Big thank to Brian Woong)))))
var timer: Timer?
private func attemptReloadOfTableView() {
self.timer?.invalidate()
self.timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.handleReloadTableView), userInfo: nil, repeats: false)
}
#objc func handleReloadTableView() {
self.messages = Array(self.messagesDictionary.values)
self.messages.sort(by: { (message1, message2) -> Bool in
return (message1.timeStamp?.intValue)! > (message2.timeStamp?.intValue)!
})
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
When I start my app every tableView.cell shows correct number of unread messages from database. When I start to send messages, tableView reloads and begins to show the number of unread messages from sender not only on his cell, but on ALL cells. Moreover in console I see that it gets more and more snapshots from database with every upcoming message.
What happens? How to fix this strange bug?
let unreadMessagesRefHandle = unreadMessagesRef.observe(.value, with: { (snapshot) in
if let count = snapshot.value as? Int {
self.unreadMessagesCountLabel.isHidden = false
self.unreadMessagesCountLabel.text = String(count)
} else {
self.unreadMessagesCountLabel.isHidden = true
}
print(snapshot)
}, withCancel: nil)
unreadMessagesRef.removeObserver(withHandle: unreadMessagesRefHandle)
Premise
I am currently making SNS with Swift.
I encountered the following error message while implementing user added functionality on it.
Error message
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
Code
Swift4
import UIKit
class PeopleViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var users = [UserModel]()
var userUid = ""
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.tableFooterView = UIView(frame: .zero)
tableView.rowHeight = 80
loadUser()
}
func loadUser() {
UserApi.shared.observeUser { (user) in
self.isFollowing(userUid: user.uid!, completed: { (value) in
**if user.uid != UserApi.shared.CURRENT_USER_UID! {** <-errorPoint
user.isFollowing = value
self.users.append(user)
self.tableView.reloadData()
}
})
}
}
func isFollowing(userUid: String, completed: #escaping (Bool) -> Void ) {
FollowApi.shared.isFollowing(withUser: userUid, completed: completed)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowUserInfoSegue" {
let showUserInfoVC = segue.destination as! ShowUserinfoViewController
showUserInfoVC.userUid = self.userUid
}
}
}
extension PeopleViewController: PeopleCellDelegate {
func didTappedShowUserInfo(userUid: String) {
self.userUid = userUid
performSegue(withIdentifier: "ShowUserInfoSegue", sender: self)
}
}
extension PeopleViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PeopleTableViewCell", for: indexPath) as! PeopleTableViewCell
cell.user = users[indexPath.row]
cell.delegate = self
return cell
}
}
Code
Swift4
import Foundation
import FirebaseDatabase
import FirebaseAuth
class UserApi {
var REF_USERS = Database.database().reference().child("users")
static var shared: UserApi = UserApi()
private init() {
}
var CURRENT_USER_UID: String? {
if let currentUserUid = Auth.auth().currentUser?.uid {
return currentUserUid
}
return nil
}
var CURRENT_USER: User? {
if let currentUserUid = Auth.auth().currentUser {
return currentUserUid
}
return nil
}
func observeUser(uid: String, completion: #escaping (UserModel) -> Void) {
REF_USERS.child(uid).observeSingleEvent(of: .value) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let newUser = UserModel(dictionary: dic)
completion(newUser)
}
}
func observeUser(completion: #escaping (UserModel) -> Void ) {
REF_USERS.observe(.childAdded) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let user = UserModel(dictionary: dic)
completion(user)
}
}
func observeCurrentUser(completion: #escaping (UserModel) -> Void ) {
guard let currentUserUid = CURRENT_USER_UID else { return }
REF_USERS.child(currentUserUid).observeSingleEvent(of: .value) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let currentUser = UserModel(dictionary: dic)
completion(currentUser)
}
}
func queryUser(withText text: String, completion: #escaping(UserModel) -> Void ) {
REF_USERS.queryOrdered(byChild: "username_lowercase").queryStarting(atValue: text).queryEnding(atValue: text + "\u{f8ff}").queryLimited(toLast: 5).observeSingleEvent(of: .value) { (snapshot) in
snapshot.children.forEach({ (data) in
let child = data as! DataSnapshot
guard let dic = child.value as? [String: Any] else { return }
let user = UserModel(dictionary: dic)
completion(user)
})
}
}
}
What I tried
How can I fix "Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value" in Swift
https://learnappmaking.com/found-nil-while-unwrapping-optional-value/
I browsed and examined these sites, but it did not work.
I think that user information can not be taken out successfully.
Supplementary information
We will add additional information if we have other missing information.
Since I often do not understand Swift in 3 weeks, I would like you to tell me with concrete code etc.
Also, I am happy if you can tell me the cause of the error.
FW / tool version
You're trying to access the CURRENT_USER_UID from UserApi Singleton class which is Optional computed property which seems to be returning nil.
If there's not current user signed-in than Firebase Auth returns nil instead of uid
Auth.auth().currentUser?.uid // Because of Optional Chaining
I'd Suggest you to safely unwrap Optionals.
func loadUser() {
UserApi.shared.observeUser { (user) in
self.isFollowing(userUid: user.uid!, completed: { (value) in
if let currentUser = UserApi.shared.CURRENT_USER_UID {
if user.uid != currentUser {
user.isFollowing = value
self.users.append(user)
self.tableView.reloadData()
}
} else {
// Current user not Signed-In
}
})
}
}