How to implement private chat in ios using firebase - swift

I have implemented private chat in my ios app. However, it is not so private. When I send a message that I intend to send to one person, everyone in the app can see it. I have three view controllers in play here.
The FirstViewController has a list of users, and when the cell is clicked it is segued to the DetailedViewController. In this viewController, it only lists the details of the user clicked on. Next, when I press the compose button in the DetailedViewController, the goal is to segue to MessageUserController. This is where I am stuck. This is the code to segue to the MessageUserController:
var username: String?
#IBAction func sendMessage(_ sender: Any) {
performSegue(withIdentifier: "sendMessageToUser", sender: self.username)
}
override public func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard segue.identifier == "sendMessageToUser", let chatVc = segue.destination as? MessageViewController else {
return
}
chatVc.senderId = self.loggedInUser?.uid
chatVc.senderDisplayName = self.username
}
I assume the sender could be the username because it is unique to the user. When I click on a user to chat with, it works fine but when I click on another user, the chat between the first users are already displayed in the new user's chatController
In the firstViewController, username is passed like this:
if segue.identifier == "UsersProfile" {
if let indexPath = sender as? IndexPath{
let vc = segue.destination as! UsersProfileViewController
let post = self.posts[indexPath.row] as! [String: AnyObject]
let username = post["username"] as? String
vc.username = username
}
}
entire view controller:
import UIKit
import Photos
import Firebase
import FirebaseDatabase
import JSQMessagesViewController
class SendMessageViewController: JSQMessagesViewController {
var username: String?
//var receiverData = AnyObject?()
var messages = [JSQMessage]()
private var photoMessageMap = [String: JSQPhotoMediaItem]()
private let imageURLNotSetKey = "NOTSET"
lazy var outgoingBubbleImageView: JSQMessagesBubbleImage = self.setupOutgoingBubble()
lazy var incomingBubbleImageView: JSQMessagesBubbleImage = self.setupIncomingBubble()
var rootRef = FIRDatabase.database().reference()
var messageRef = FIRDatabase.database().reference().child("messages")
private var newMessageRefHandle: FIRDatabaseHandle?
private lazy var usersTypingQuery: FIRDatabaseQuery =
self.rootRef.child("typingIndicator").queryOrderedByValue().queryEqual(toValue: true)
lazy var storageRef: FIRStorageReference = FIRStorage.storage().reference(forURL: "gs://gsignme-14416.appspot.com")
private var updatedMessageRefHandle: FIRDatabaseHandle?
private lazy var userIsTypingRef: FIRDatabaseReference =
self.rootRef.child("typingIndicator").child(self.senderId) // 1
private var localTyping = false // 2
var isTyping: Bool {
get {
return localTyping
}
set {
// 3
localTyping = newValue
userIsTypingRef.setValue(newValue)
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.senderId = FIRAuth.auth()?.currentUser?.uid
// Do any additional setup after loading the view.
self.navigationController?.navigationBar.barTintColor = UIColor(red:0.23, green:0.73, blue:1.00, alpha:1.0)
self.navigationController?.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: UIColor.white]
self.navigationItem.title = senderDisplayName
self.navigationItem.rightBarButtonItem?.tintColor = UIColor.white
self.navigationItem.leftBarButtonItem?.tintColor = UIColor.white
// No avatars
collectionView!.collectionViewLayout.incomingAvatarViewSize = CGSize.zero
collectionView!.collectionViewLayout.outgoingAvatarViewSize = CGSize.zero
observeMessages()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
observeTyping()
}
deinit {
if let refHandle = newMessageRefHandle {
messageRef.removeObserver(withHandle: refHandle)
}
if let refHandle = updatedMessageRefHandle {
messageRef.removeObserver(withHandle: refHandle)
}
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageDataForItemAt indexPath: IndexPath!) -> JSQMessageData! {
return messages[indexPath.item]
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return messages.count
}
private func setupOutgoingBubble() -> JSQMessagesBubbleImage {
let bubbleImageFactory = JSQMessagesBubbleImageFactory()
return bubbleImageFactory!.outgoingMessagesBubbleImage(with: UIColor.jsq_messageBubbleBlue())
}
private func setupIncomingBubble() -> JSQMessagesBubbleImage {
let bubbleImageFactory = JSQMessagesBubbleImageFactory()
return bubbleImageFactory!.incomingMessagesBubbleImage(with: UIColor.jsq_messageBubbleLightGray())
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
let message = messages[indexPath.item] // 1
if message.senderId == senderId { // 2
return outgoingBubbleImageView
} else { // 3
return incomingBubbleImageView
}
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
return nil
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForMessageBubbleTopLabelAt indexPath: IndexPath!) -> CGFloat {
return 15
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAt: indexPath) as! JSQMessagesCollectionViewCell
let message = messages[indexPath.item]
if message.senderId == senderId {
cell.textView?.textColor = UIColor.white
} else {
cell.textView?.textColor = UIColor.black
}
return cell
}
//ADD A NEW MESSAGE
private func addMessage(withId id: String, name: String, text: String) {
if let message = JSQMessage(senderId: id, displayName: name, text: text) {
messages.append(message)
}
}
override func didPressSend(_ button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: Date!) {
let itemRef = rootRef.child("messages").childByAutoId() // 1
let messageItem = [ // 2
"senderId": senderId!,
"ReceiverName": senderDisplayName!,
"text": text!,
]
itemRef.setValue(messageItem) // 3
JSQSystemSoundPlayer.jsq_playMessageSentSound() // 4
finishSendingMessage() // 5
isTyping = false
}
private func observeMessages() {
// 1.
let messageQuery = rootRef.child("messages").queryLimited(toLast: 25)
// 2. We can use the observe method to listen for new
// messages being written to the Firebase DB
newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) -> Void in
// 3
let messageData = snapshot.value as! Dictionary<String, String>
if let id = messageData["senderId"] as String!, let name = messageData["ReceiverName"] as String!, let text = messageData["text"] as String!, text.characters.count > 0 {
// 4
self.addMessage(withId: id, name: name, text: text)
// 5
self.finishReceivingMessage()
} else if let id = messageData["senderId"] as String!,
let photoURL = messageData["photoURL"] as String! { // 1
// 2
if let mediaItem = JSQPhotoMediaItem(maskAsOutgoing: id == self.senderId) {
// 3
self.addPhotoMessage(withId: id, key: snapshot.key, mediaItem: mediaItem)
// 4
if photoURL.hasPrefix("gs://") {
self.fetchImageDataAtURL(photoURL, forMediaItem: mediaItem, clearsPhotoMessageMapOnSuccessForKey: nil)
}
}
} else {
print("Error! Could not decode message data")
}
})
// We can also use the observer method to listen for
// changes to existing messages.
// We use this to be notified when a photo has been stored
// to the Firebase Storage, so we can update the message data
updatedMessageRefHandle = messageRef.observe(.childChanged, with: { (snapshot) in
let key = snapshot.key
let messageData = snapshot.value as! Dictionary<String, String> // 1
if let photoURL = messageData["photoURL"] as String! { // 2
// The photo has been updated.
if let mediaItem = self.photoMessageMap[key] { // 3
self.fetchImageDataAtURL(photoURL, forMediaItem: mediaItem, clearsPhotoMessageMapOnSuccessForKey: key) // 4
}
}
})
}
override func textViewDidChange(_ textView: UITextView) {
super.textViewDidChange(textView)
// If the text is not empty, the user is typing
isTyping = textView.text != ""
}
private func observeTyping() {
let typingIndicatorRef = rootRef.child("typingIndicator")
userIsTypingRef = typingIndicatorRef.child(senderId)
userIsTypingRef.onDisconnectRemoveValue()
usersTypingQuery = typingIndicatorRef.queryOrderedByValue().queryEqual(toValue: true)
// 1
usersTypingQuery.observe(.value) { (data: FIRDataSnapshot) in
// 2 You're the only one typing, don't show the indicator
if data.childrenCount == 1 && self.isTyping {
return
}
// 3 Are there others typing?
self.showTypingIndicator = data.childrenCount > 0
self.scrollToBottom(animated: true)
}
}
func sendPhotoMessage() -> String? {
let itemRef = messageRef.childByAutoId()
let messageItem = [
"photoURL": imageURLNotSetKey,
"senderId": senderId!,
]
itemRef.setValue(messageItem)
JSQSystemSoundPlayer.jsq_playMessageSentSound()
finishSendingMessage()
return itemRef.key
}
func setImageURL(_ url: String, forPhotoMessageWithKey key: String) {
let itemRef = messageRef.child(key)
itemRef.updateChildValues(["photoURL": url])
}
override func didPressAccessoryButton(_ sender: UIButton) {
let picker = UIImagePickerController()
picker.delegate = self as! UIImagePickerControllerDelegate & UINavigationControllerDelegate
if (UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera)) {
picker.sourceType = UIImagePickerControllerSourceType.camera
} else {
picker.sourceType = UIImagePickerControllerSourceType.photoLibrary
}
present(picker, animated: true, completion:nil)
}
private func addPhotoMessage(withId id: String, key: String, mediaItem: JSQPhotoMediaItem) {
if let message = JSQMessage(senderId: id, displayName: "", media: mediaItem) {
messages.append(message)
if (mediaItem.image == nil) {
photoMessageMap[key] = mediaItem
}
collectionView.reloadData()
}
}
private func fetchImageDataAtURL(_ photoURL: String, forMediaItem mediaItem: JSQPhotoMediaItem, clearsPhotoMessageMapOnSuccessForKey key: String?) {
// 1
let storageRef = FIRStorage.storage().reference(forURL: photoURL)
// 2
storageRef.data(withMaxSize: INT64_MAX){ (data, error) in
if let error = error {
print("Error downloading image data: \(error)")
return
}
// 3
storageRef.metadata(completion: { (metadata, metadataErr) in
if let error = metadataErr {
print("Error downloading metadata: \(error)")
return
}
// 4
if (metadata?.contentType == "image") {
mediaItem.image = UIImage.init(data: data!)
} else {
mediaItem.image = UIImage.init(data: data!)
}
self.collectionView.reloadData()
// 5
guard key != nil else {
return
}
self.photoMessageMap.removeValue(forKey: key!)
})
}
}
}
extension SendMessageViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [String : Any]) {
picker.dismiss(animated: true, completion:nil)
// 1
if let photoReferenceUrl = info[UIImagePickerControllerReferenceURL] as? URL {
// Handle picking a Photo from the Photo Library
// 2
let assets = PHAsset.fetchAssets(withALAssetURLs: [photoReferenceUrl], options: nil)
let asset = assets.firstObject
// 3
if let key = sendPhotoMessage() {
// 4
asset?.requestContentEditingInput(with: nil, completionHandler: { (contentEditingInput, info) in
let imageFileURL = contentEditingInput?.fullSizeImageURL
// 5
let path = "\(FIRAuth.auth()?.currentUser?.uid)/\(Int(Date.timeIntervalSinceReferenceDate * 1000))/\(photoReferenceUrl.lastPathComponent)"
// 6
self.storageRef.child(path).putFile(imageFileURL!, metadata: nil) { (metadata, error) in
if let error = error {
print("Error uploading photo: \(error.localizedDescription)")
return
}
// 7
self.setImageURL(self.storageRef.child((metadata?.path)!).description, forPhotoMessageWithKey: key)
}
})
}
} else {
// Handle picking a Photo from the Camera - TODO
// 1
let image = info[UIImagePickerControllerOriginalImage] as! UIImage
// 2
if let key = sendPhotoMessage() {
// 3
let imageData = UIImageJPEGRepresentation(image, 1.0)
// 4
let imagePath = FIRAuth.auth()!.currentUser!.uid + "/\(Int(Date.timeIntervalSinceReferenceDate * 1000)).jpg"
// 5
let metadata = FIRStorageMetadata()
metadata.contentType = "image/jpeg"
// 6
storageRef.child(imagePath).put(imageData!, metadata: metadata) { (metadata, error) in
if let error = error {
print("Error uploading photo: \(error)")
return
}
// 7
self.setImageURL(self.storageRef.child((metadata?.path)!).description, forPhotoMessageWithKey: key)
}
}
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion:nil)
}
}

Looks to me like this isn't a privacy issue as you state, it's simply that you're not clearing the data on your messages view controller when you load the new conversation.
Ultimately it really depends on how secure you want this to be; if you're happy having the private messages saved in memory, then don't destroy them until the user logs out — you can even keep multiple private conversations saved in a CoreData database. It's still relatively secure this way, and it's convenient for users and performant. If you prefer to destroy messages sooner, clear the data on viewDidDisappear, then checking in your prepareForSegue method that the data is again cleared. You could also destroy the entire messages controller each time you dismiss it, if storing a strong reference isn't what you want to do.
An example of this, as a storyboard:
App loads
User1 is logged in
User1 selects private messages
User1 has conversation with User2
User1 switches to a conversation with User3
[pseudo-code]
userDidChangeRecipient {
// destroy messages view controller
// or destroy Firebase array data and destroy the reference to the message/conversation ID
}
And each time you load the view controller:
prepareForSegue {
if strongRefToMessagesVC == nil {
// instantiate a new instance of vc from nib or scratch
// load the appropriate message/conversation ID
// load messages
}
}
More digging:
There's two possibilities here:
You're not destroying the view controller when you switch messages, and this tutorial expects you to. In that case, you need to look at when the segue ends or the user closes the messages view controller and either destroy it, or empty the array.
You're trying to write all the private messages into the same JSQMessage array. I notice in that view controller you have:
var messageRef = FIRDatabase.database().reference().child("messages")
Is that the database connection you're working with? Each private message conversation should have a unique reference ID so that they do not overlap, otherwise every user will load up the same set of messages from Firebase.

Related

Saving TableView cell using UserDefaults

I'm trying to get cell of tableView using UserDefaults, but after i reload app it is always empty
This is my Model:
struct Note: Codable {
var title: String
var description: String
}
class Notes {
var stock: [Note] = []
}
View contoller
var model = Notes()
This is how i get data
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
tableView.reloadData()
if let fetchedData = UserDefaults.standard.data(forKey: "notes") {
let fetchedBookies = try! PropertyListDecoder().decode([Note].self, from: fetchedData)
print(fetchedBookies)
} else {
model.stock = []
}
}
This is my cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell") as! TableViewCell
cell.titleOutlet.text = self.model.stock[indexPath.row].title
cell.descriptionOutlet?.text = self.model.stock[indexPath.row].description
return cell
}
How i save data
#IBAction func check(_ sender: Any) {
let newstock = Note(title: "check", description: "check2")
model.stock.append(newstock)
print(model.stock.count)
let bookiesData = try! PropertyListEncoder().encode(model.stock)
UserDefaults.standard.set(bookiesData, forKey: "notes")
tableView.reloadData()
}
Thank you very much!
I recommend you to use Json Encoder/Deocder.
First set your Notes class to conform to Codable:
class Notes: Codable {
var stock: [Note] = []
}
Here is an example of how to use Json Encoder / Decoder:
func save(notes: Notes) throws {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(notes)
UserDefaults.standard.set(data, forKey: "notes")
} catch let error {
throw error
}
}
func load() -> Notes {
guard let data = UserDefaults.standard.data(forKey: "notes") else {
return Notes() // Default
}
let decoder = JSONDecoder()
do {
let object = try decoder.decode(Notes.self, from: data)
return object
} catch {
return Notes() // Default
}
}
In your code just call load() to get your notes from User Defaults
And save(notes:) to save them into User Defaults.

Firestore chat application displaying messaged users 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.

Why does my segue not wait until completion handler finished?

I have a page based app, using RootViewController, ModelViewController, DataViewController, and a SearchViewController.
In my searchViewController, I search for an item and then add or remove that Item to an array which is contained in a Manager class(and UserDefaults), which the modelViewController uses to instantiate an instance of DataViewController with the correct information loaded using the dataObject. Depending on whether an Item was added or removed, I use a Bool to determine which segue was used, addCoin or removeCoin, so that the RootViewController(PageView) will show either the last page in the array, (when a page is added) or the first (when removed).
Everything was working fine until I ran into an error which I can not diagnose, the problem is that when I add a page, the app crashes, giving me a "unexpectadely found nil when unwrapping an optional value"
This appears to be the problem function, in the searchViewController 'self.performSegue(withIdentifier: "addCoin"' seems to be called instantly, even without the dispatchque:
#objc func addButtonAction(sender: UIButton!) {
print("Button tapped")
if Manager.shared.coins.contains(dataObject) {
Duplicate()
} else if Manager.shared.coins.count == 5 {
max()
} else {
Manager.shared.addCoin(coin: dataObject)
CGPrices.shared.getData(arr: true, completion: { (success) in
print(Manager.shared.coins)
DispatchQueue.main.async {
self.performSegue(withIdentifier: "addCoin", sender: self)
}
})
}
searchBar.text = ""
}
Meaning that In my DataViewController, this function will find nil:
func getIndex() {
let index = CGPrices.shared.coinData.index(where: { $0.id == dataObject })!
dataIndex = index
}
I can't find out why it does not wait for completion.
I also get this error about threads:
[Assert] Cannot be called with asCopy = NO on non-main thread.
which is why I try to do the push segue using dispatch que
Here is my searchViewController full code:
import UIKit
class SearchViewController: UIViewController, UISearchBarDelegate {
let selectionLabel = UILabel()
let searchBar = UISearchBar()
let addButton = UIButton()
let removeButton = UIButton()
var filteredObject: [String] = []
var dataObject = ""
var isSearching = false
//Add Button Action.
#objc func addButtonAction(sender: UIButton!) {
print("Button tapped")
if Manager.shared.coins.contains(dataObject) {
Duplicate()
} else if Manager.shared.coins.count == 5 {
max()
} else {
Manager.shared.addCoin(coin: dataObject)
CGPrices.shared.getData(arr: true, completion: { (success) in
print(Manager.shared.coins)
DispatchQueue.main.async {
self.performSegue(withIdentifier: "addCoin", sender: self)
}
})
}
searchBar.text = ""
}
//Remove button action.
#objc func removeButtonActon(sender: UIButton!) {
print("Button tapped")
if Manager.shared.coins.contains(dataObject) {
Duplicate()
} else if Manager.shared.coins.count == 5 {
max()
} else {
Manager.shared.removeCoin(coin: dataObject)
self.performSegue(withIdentifier: "addCoin", sender: self)
}
searchBar.text = ""
}
//Prepare for segue, pass removeCoinSegue Bool depending on remove or addCoin.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "addCoin" {
if let destinationVC = segue.destination as? RootViewController {
destinationVC.addCoinSegue = true
}
} else if segue.identifier == "addCoin" {
if let destinationVC = segue.destination as? RootViewController {
destinationVC.addCoinSegue = false
}
}
}
//Remove button action.
#objc func removeButtonAction(sender: UIButton!) {
if Manager.shared.coins.count == 1 {
removeAlert()
} else {
Manager.shared.removeCoin(coin: dataObject)
print(Manager.shared.coins)
print(dataObject)
searchBar.text = ""
self.removeButton.isHidden = true
DispatchQueue.main.async {
self.performSegue(withIdentifier: "removeCoin", sender: self)
}
}
}
//Search/Filter the struct from CGNames, display both the Symbol and the Name but use the ID as dataObject.
func filterStructForSearchText(searchText: String, scope: String = "All") {
if !searchText.isEmpty {
isSearching = true
filteredObject = CGNames.shared.coinNameData.filter {
// if you need to search key and value and include partial matches
// $0.key.contains(searchText) || $0.value.contains(searchText)
// if you need to search caseInsensitively key and value and include partial matches
$0.name.range(of: searchText, options: .caseInsensitive) != nil || $0.symbol.range(of: searchText, options: .caseInsensitive) != nil
}
.map{ $0.id }
} else {
isSearching = false
print("NoText")
}
}
//Running filter function when text changes.
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filterStructForSearchText(searchText: searchText)
if isSearching == true && filteredObject.count > 0 {
addButton.isHidden = false
dataObject = filteredObject[0]
selectionLabel.text = dataObject
if Manager.shared.coins.contains(dataObject) {
removeButton.isHidden = false
addButton.isHidden = true
} else {
removeButton.isHidden = true
addButton.isHidden = false
}
} else {
addButton.isHidden = true
removeButton.isHidden = true
selectionLabel.text = "e.g. btc/bitcoin"
}
}
override func viewDidLoad() {
super.viewDidLoad()
//Setup the UI.
self.view.backgroundColor = .gray
setupView()
}
override func viewDidLayoutSubviews() {
}
//Hide keyboard
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
//Alerts
func removeAlert() {
let alertController = UIAlertController(title: "Can't Remove", message: "\(dataObject) can't be deleted, add another to delete \(dataObject)", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
func Duplicate() {
let alertController = UIAlertController(title: "Duplicate", message: "\(dataObject) is already in your pages!", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
func max() {
let alertController = UIAlertController(title: "Maximum Reached", message: "\(dataObject) can't be added, you have reached the maximum of 5 coins. Please delete a coin to add another.", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
}
and here is the DataViewController
import UIKit
class DataViewController: UIViewController {
#IBOutlet weak var dataLabel: UILabel!
//Variables and Objects.
//The dataObject carries the chosen cryptocurrencies ID from the CoinGecko API to use to get the correct data to load on each object.
var dataObject = String()
//The DefaultCurrency (gbp, eur...) chosen by the user.
var defaultCurrency = ""
//The Currency Unit taken from the exchange section of the API.
var currencyUnit = CGExchange.shared.exchangeData[0].rates.gbp.unit
var secondaryUnit = CGExchange.shared.exchangeData[0].rates.eur.unit
var tertiaryUnit = CGExchange.shared.exchangeData[0].rates.usd.unit
//Index of the dataObject
var dataIndex = Int()
//Objects
let cryptoLabel = UILabel()
let cryptoIconImage = UIImageView()
let secondaryPriceLabel = UILabel()
let mainPriceLabel = UILabel()
let tertiaryPriceLabel = UILabel()
//Custom Fonts.
let customFont = UIFont(name: "AvenirNext-Heavy", size: UIFont.labelFontSize)
let secondFont = UIFont(name: "AvenirNext-BoldItalic" , size: UIFont.labelFontSize)
//Setup Functions
//Get the index of the dataObject
func getIndex() {
let index = CGPrices.shared.coinData.index(where: { $0.id == dataObject })!
dataIndex = index
}
//Label
func setupLabels() {
//cryptoLabel from dataObject as name.
cryptoLabel.text = CGPrices.shared.coinData[dataIndex].name
//Prices from btc Exchange rate.
let btcPrice = CGPrices.shared.coinData[dataIndex].current_price!
let dcExchangeRate = CGExchange.shared.exchangeData[0].rates.gbp.value
let secondaryExchangeRate = CGExchange.shared.exchangeData[0].rates.eur.value
let tertiaryExchangeRate = CGExchange.shared.exchangeData[0].rates.usd.value
let realPrice = (btcPrice * dcExchangeRate)
let secondaryPrice = (btcPrice * secondaryExchangeRate)
let tertiaryPrice = (btcPrice * tertiaryExchangeRate)
secondaryPriceLabel.text = "\(secondaryUnit)\(String((round(1000 * secondaryPrice) / 1000)))"
mainPriceLabel.text = "\(currencyUnit)\(String((round(1000 * realPrice) /1000)))"
tertiaryPriceLabel.text = "\(tertiaryUnit)\(String((round(1000 * tertiaryPrice) / 1000)))"
}
//Image
func getIcon() {
let chosenImage = CGPrices.shared.coinData[dataIndex].image
let remoteImageUrl = URL(string: chosenImage)
guard let url = remoteImageUrl else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
DispatchQueue.main.async {
self.cryptoIconImage.image = UIImage(data: data)
}
}
}.resume()
}
override func viewDidLoad() {
super.viewDidLoad()
// for family in UIFont.familyNames.sorted() {
// let names = UIFont.fontNames(forFamilyName: family)
// print("Family: \(family) Font names: \(names)")
// }
// Do any additional setup after loading the view, typically from a nib.
self.setupLayout()
self.getIndex()
self.setupLabels()
self.getIcon()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.dataLabel!.text = dataObject
view.backgroundColor = .lightGray
}
}
Edit: CGPrices Class with getData method:
import Foundation
class CGPrices {
struct Coins: Decodable {
let id: String
let name: String
let symbol: String
let image: String
let current_price: Double?
let low_24h: Double?
//let price_change_24h: Double?
}
var coinData = [Coins]()
var defaultCurrency = ""
var coins = Manager.shared.coins
var coinsEncoded = ""
static let shared = CGPrices()
func encode() {
for i in 0..<coins.count {
coinsEncoded += coins[i]
if (i + 1) < coins.count { coinsEncoded += "%2C" }
}
print("encoded")
}
func getData(arr: Bool, completion: #escaping (Bool) -> ()) {
encode()
let urlJSON = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=\(coinsEncoded)"
guard let url = URL(string: urlJSON) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let coinsData = try JSONDecoder().decode([Coins].self, from: data)
self.coinData = coinsData
completion(arr)
} catch let jsonErr {
print("error serializing json: \(jsonErr)")
print(data)
}
}.resume()
}
func refresh(completion: () -> ()) {
defaultCurrency = UserDefaults.standard.string(forKey: "DefaultCurrency")!
completion()
}
}
I figured it out.
The problem was inside my getData method I was not updated the coins array:
var coinData = [Coins]()
var defaultCurrency = ""
var coins = Manager.shared.coins
var coinsEncoded = ""
static let shared = CGPrices()
func encode() {
for i in 0..<coins.count {
coinsEncoded += coins[i]
if (i+1)<coins.count { coinsEncoded+="%2C" }
}
print("encoded")
}
I needed to add this line in getData:
func getData(arr: Bool, completion: #escaping (Bool) -> ()) {
//Adding this line to update the array so that the URL is appended correctly.
coins = Manager.shared.coins
encode()
let urlJSON = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=\(coinsEncoded)"
This would fix the finding nil in the DataViewController, but the app would still crash do to updating UI Elements on a background thread, as the segue was called inside the completion handler of the getData method. to fix this, I used DispatchQue.Main.Async on the segue inside the getData method in the addButton function, to ensure that everything is updated on the main thread, like so:
#objc func addButtonAction(sender: UIButton!) {
print("Button tapped")
if Manager.shared.coins.contains(dataObject) {
Duplicate()
} else if Manager.shared.coins.count == 5 {
max()
} else {
Manager.shared.addCoin(coin: dataObject)
print("starting")
CGPrices.shared.getData(arr: true) { (arr) in
print("complete")
print(CGPrices.shared.coinData)
//Here making sure it is updated on main thread.
DispatchQueue.main.async {
self.performSegue(withIdentifier: "addCoin", sender: self)
}
}
}
searchBar.text = ""
}
Thanks for all the comments as they helped me to figure this out, and I learned a lot in doing so. Hopefully this can help someone else in their thought process when debugging, as one can get so caught up in one area of a problem, and forget to take a step back and look to other areas.

firebase upload after checkout

This is shopping app after user select the products send to cart then checkout after fill the fields and press Checkout (Pay) send the details of the products to my firebase database please any one can help me I use json as my load products on tableview cell
I know I don't have payment serves but on this fields of payment information will be name , phone number, and location
[{
"name": "EGG",
"price": "3.00",
"image": "http://partiklezoo.com/Egg/u0001.jpg",
"description": "one Egg",
"category": "Food",
"uid": "u0001"
},]
Code
import UIKit
class CheckoutViewController: DetailViewController, UITableViewDataSource, UITableViewDelegate, UIPickerViewDataSource, UIPickerViewDelegate {
#IBOutlet var cardNumber: UITextField!
#IBOutlet var cardExpiryMonth: UITextField!
#IBOutlet var cardExpiryYear: UITextField!
#IBOutlet var cardCvv: UITextField!
#IBOutlet var pickerPickupPoint: UIPickerView!
#IBOutlet var tableViewOrderDetails: UITableView!
#IBOutlet var labelTotalPrice: UILabel!
var model = SingletonManager.model
override func viewDidLoad() {
super.viewDidLoad()
self.configureCheckout()
self.tableViewOrderDetails.dataSource = self
self.tableViewOrderDetails.delegate = self
self.pickerPickupPoint.dataSource = self
self.pickerPickupPoint.delegate = self
//Looks for single or multiple taps.
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(CheckoutViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
}
#objc func dismissKeyboard() {
//Causes the view (or one of its embedded text fields) to resign the first responder status.
self.view.endEditing(true)
}
func configureCheckout() {
pickerPickupPoint.selectedRow(inComponent: 0)
labelTotalPrice.text = "$" + String(format: "%.2f", model.calculateCartTotal())
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model.cart.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = model.products[Int(model.cart[indexPath.row][0])].name
cell.detailTextLabel?.text = String(Int(model.cart[indexPath.row][1])) + " x $" + String(format: "%.2f", model.cart[indexPath.row][4])
return cell
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return model.pickUpLocations.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return model.pickUpLocations[row]["street"]! + ", " + model.pickUpLocations[row]["suburb"]!
}
#IBAction func payNow(_ sender: Any) {
var error = ""
if self.model.cart.count == 0 {
error = "Your cart is empty."
}
else if (self.cardNumber.text?.isEmpty)! {
error = "Please enter your card number."
}
else if (self.cardExpiryMonth.text?.isEmpty)! {
error = "Please enter the expiry month of your card."
}
else if (self.cardExpiryYear.text?.isEmpty)! {
error = "Please enter the expiry year of your card."
}
else if (self.cardCvv.text?.isEmpty)!{
error = "Please enter the CVV number of your card."
}
if error.isEmpty {
showAlertMsg("Confirm Purchase", message: "Pay " + labelTotalPrice.text!, style: UIAlertControllerStyle.actionSheet)
}
else {
showAlertMsg("Error", message: error, style: UIAlertControllerStyle.alert)
}
}
var alertController: UIAlertController?
func showAlertMsg(_ title: String, message: String, style: UIAlertControllerStyle) {
self.alertController = UIAlertController(title: title, message: message, preferredStyle: style)
if style == UIAlertControllerStyle.actionSheet {
alertController?.addAction(UIAlertAction(title: "Pay", style: .default, handler: { _ in
self.checkout()
}))
alertController?.addAction(UIAlertAction(title: "Cancel", style: .cancel))
} else {
alertController?.addAction(UIAlertAction(title: "Okay", style: .default))
}
self.present(self.alertController!, animated: true, completion: nil)
}
func checkout() {
var success = true
for count in 0...self.model.cart.count - 1 {
let product = self.model.products[Int(self.model.cart[count][0])]
let quantity = Int(self.model.cart[count][1])
let total = self.model.cart[count][4]
let material = self.model.cart[count][3] == 0.0 ? "pla" : "abs"
let painting = self.model.cart[count][2] == 0.0 ? "false" : "true"
let temp = self.model.purchase(product: product, quantity: quantity, total: total, material: material, painting: painting)
if !temp {
success = false
}
}
if !success {
let error = "Oops! Something went wrong. Please try again later."
showAlertMsg("Error", message: error, style: UIAlertControllerStyle.alert)
} else {
print("Success! Checkout complete.")
self.cardNumber.text = ""
self.cardExpiryMonth.text = ""
self.cardExpiryYear.text = ""
self.cardCvv.text = ""
self.labelTotalPrice.text = "$0.00"
self.model.clearCart()
self.tableViewOrderDetails.reloadData()
self.performSegue(withIdentifier: "Thankyou", sender: self)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let confirmationVc = (segue.destination as! UINavigationController).topViewController as! ConfirmationViewController
confirmationVc.location = self.model.pickUpLocations[self.pickerPickupPoint.selectedRow(inComponent: 0)]
}
}
this is my objective
Class Model: NSObject, CLLocationManagerDelegate {
var segueArray = [String]()
var seguesDictionary = Dictionary<String, UIImage>()
var products = [Product]()
var storedProducts = [NSManagedObject]()
var cart = [[Double]]()
var storedCart = [NSManagedObject]()
var pickUpLocations = [[String: String]]()
let locationManager = CLLocationManager()
override init() {
super.init()
segueArray.append("Home")
segueArray.append("List")
segueArray.append("Search")
segueArray.append("Cart")
segueArray.append("Finder")
segueArray.append("Checkout")
seguesDictionary["Home"] = UIImage(named: "home")
seguesDictionary["List"] = UIImage(named: "list")
seguesDictionary["Search"] = UIImage(named: "search")
seguesDictionary["Cart"] = UIImage(named: "cart")
seguesDictionary["Finder"] = UIImage(named: "finder")
seguesDictionary["Checkout"] = UIImage(named: "checkout")
self.loadProducts()
self.refreshProducts()
self.loadCart()
self.configureLocManager()
}
func loadProducts() {
let managedContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Products")
do {
let results = try managedContext.fetch(fetchRequest)
storedProducts = results as! [NSManagedObject]
if storedProducts.count > 0 {
for index in 0...storedProducts.count - 1 {
let binaryData = storedProducts[index].value(forKey: "image") as! Data
let image = UIImage(data: binaryData)
let name = storedProducts[index].value(forKey: "name") as! String
let price = storedProducts[index].value(forKey: "price") as! Double
let details = storedProducts[index].value(forKey: "details") as! String
let category = storedProducts[index].value(forKey: "category") as! String
let uid = storedProducts[index].value(forKey: "uid") as! String
let loadedProduct = Product(name: name, price: price, image: image!, details: details, category: category, uid: uid)
products.append(loadedProduct)
}
}
}
catch let error as NSError
{
print("Could not load. \(error), \(error.userInfo)")
}
}
func refreshProducts() {
let url = NSURL(string: "http://partiklezoo.com/3dprinting/")
let config = URLSessionConfiguration.default
config.isDiscretionary = true
let session = URLSession(configuration: config)
let task = session.dataTask(with: url! as URL, completionHandler:
{(data, response, error) in
let json = JSON(data: data!)
for count in 0...json.count - 1
{
let newProduct = Product()
newProduct.name = json[count]["name"].string
newProduct.price = Double(json[count]["price"].string!)
newProduct.details = json[count]["description"].string
newProduct.category = json[count]["category"].string
newProduct.uid = json[count]["uid"].string
let imgURL = json[count]["image"].string!
self.addItemToList(newProduct, imageURL: imgURL)
}
})
task.resume()
}
func checkForProduct(_ searchItem: Product) -> Int {
var targetIndex = -1
if products.count > 0 {
for index in 0...products.count - 1 {
if products[index].uid == searchItem.uid {
targetIndex = index
}
}
}
return targetIndex
}
func addItemToList(_ newProduct: Product!, imageURL: String) {
if checkForProduct(newProduct) == -1 {
let managedContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let picture = UIImageJPEGRepresentation(loadImage(imageURL), 1)
let entity = NSEntityDescription.entity(forEntityName: "Products", in: managedContext)
let productToAdd = NSManagedObject(entity: entity!, insertInto: managedContext)
productToAdd.setValue(newProduct.category, forKey: "category")
productToAdd.setValue(newProduct.details, forKey: "details")
productToAdd.setValue(picture, forKey: "image")
productToAdd.setValue(newProduct.name, forKey: "name")
productToAdd.setValue(newProduct.price, forKey: "price")
productToAdd.setValue(newProduct.uid, forKey: "uid")
do
{
try managedContext.save()
}
catch let error as NSError
{
print("Could not save. \(error), \(error.userInfo)")
}
storedProducts.append(productToAdd)
newProduct.image = UIImage(data: picture!)
products.append(newProduct)
}
}
func loadCart() {
let managedContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Cart")
do {
let results = try managedContext.fetch(fetchRequest)
storedCart = results as! [NSManagedObject]
if storedCart.count > 0 {
for index in 0...storedCart.count - 1 {
let product = storedCart[index].value(forKey: "product") as! Double
let quantity = storedCart[index].value(forKey: "quantity") as! Double
let finish = storedCart[index].value(forKey: "finish") as! Double
let material = storedCart[index].value(forKey: "material") as! Double
let totalPrice = storedCart[index].value(forKey: "total") as! Double
let temp = [product, quantity, finish, material, totalPrice]
cart.append(temp)
}
}
}
catch let error as NSError
{
print("Could not load. \(error), \(error.userInfo)")
}
}
func addToCart(product: Product, quantity: Double, finish: Double, material: Double, totalPrice: Double) {
let managedContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: "Cart", in: managedContext)
let productToAdd = NSManagedObject(entity: entity!, insertInto: managedContext)
productToAdd.setValue(checkForProduct(product), forKey: "product")
productToAdd.setValue(quantity, forKey: "quantity")
productToAdd.setValue(finish, forKey: "finish")
productToAdd.setValue(material, forKey: "material")
productToAdd.setValue(totalPrice, forKey: "total")
do
{
try managedContext.save()
}
catch let error as NSError
{
print("Could not save. \(error), \(error.userInfo)")
}
let temp = [Double(checkForProduct(product)), quantity, finish, material, totalPrice]
storedCart.append(productToAdd)
cart.append(temp)
}
I am not sure I understand the question but from the comments I believe the OP is asking for example code to put into an action button to write date to Firebase. So here it is.
(note: please see comments as the OP didn't clarify what he was after so it's not clear if this is an answer or not)
Start with initialzing Firebase in your view controller
class ViewController: UIViewController {
var ref: DatabaseReference!
override func viewDidLoad() {
super.viewDidLoad()
self.ref = Database.database().reference()
then lets have a button press write two cars to firebase in a 'car_list' node
#IBAction func button0Action(_ sender: Any) {
let itemDict0 = [
"Name": "Car",
"price": "2000"]
let itemDict1 = [
"Name": "Car",
"price": "3000"]
let carRef = self.ref.child("car_list")
carRef.childByAutoId().setValue(itemDict0)
carRef.childByAutoId().setValue(itemDict1)
}
and the result in Firebase looks like this
car_list
-LKYuRKpLL_JXsDGsYWY
Name: "Car"
Price: "2000"
-LKYuRKpLL_JXsDGsYWZ
Name: "Car"
Price: "3000"
EDIT: I had intially set this up like a grocery list per the data in the question but per a comment from the OP they want to make it cars and prices, so that's how it is now.

I can not solve it. Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

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
}
})
}
}