Trigger an alert when using Firebase observer and events - firebase

Working on an app that has two parts - Rider & Driver. When the driver accepts the request, an alert is then sent to the rider that the request was accepted and driver is on the way.
Unable to trigger the alert to the rider.
RiderVC:
func driverAcceptedRequest(requestAccepted: Bool, driverName: String) {
if !riderCancelledRequest {
if requestAccepted {
self.alertTheUser(title: "Ryde Accepted", message: "\(driverName) Has Accepted Your Ryde Request and will message you with details")
} else {
RydeHandler.Instance.cancelRyde()
alertTheUser(title: "Ryde Cancelled", message: "\(driverName) Has Cancelled the Ryde Request")
}
}
riderCancelledRequest = false
}
RydeHandler.swift:
// DRIVER ACCEPTED RYDE
DataService.Instance.requestAcceptedRef.observe(FIRDataEventType.childAdded) { (snapshot: FIRDataSnapshot) in
if let data = snapshot.value as? NSDictionary {
if let name = data[Constants.NAME] as? String {
if self.driver == "" {
self.driver = name
self.delegate?.driverAcceptedRequest(requestAccepted: true, driverName: self.driver)
}
}
}
}
Firebase database structure:
Edit
ViewDidLoad in tableviewcontroller - list of requests:
ref.child("drivers").child("RideRequests").observe(FIRDataEventType.value, with: { snapshot in
self.rideRequests.removeAll()
for item in snapshot.children{
self.rideRequests.append(item as! FIRDataSnapshot)
}
self.rideRequests.reverse()
self.tableView.reloadData()
})

Related

Cannot retrieve data back to my app from Firebase

I hope the problem is explained well on the title but what I want to do is to write the message on my message app and want to retrieve it and write it on the app's message cell but nothing is seen. When I go to my firebase console I can see the messages there but not on app. I didn't attach anything about project but I don't know what to attach. but let me add my chatViewController at least .`import UIKit
import Firebase
class ChatViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var messageTextfield: UITextField!
let db = Firestore.firestore()
var messages: [Message] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
navigationItem.hidesBackButton = true
title = K.appName
tableView.register(UINib(nibName: K.cellNibName, bundle: nil), forCellReuseIdentifier: K.cellIdentifier)
loadMessages()
}
func loadMessages() {
db.collection(K.FStore.collectionName)
.order(by: K.FStore.dateField)
.addSnapshotListener { (querySnapshot, error) in
self.messages = []
if let e = error {
print("There was an issue retrieving data from Firestore \(e)")
}else {
if let snapshotDocuments = querySnapshot?.documents {
for doc in snapshotDocuments {
let data = doc.data()
if let messageSender = data[K.FStore.senderField] as? String, let messageBody = data[K.FStore.bodyField] as? String {
let newMessage = Message(sender: messageSender, body: messageBody)
self.messages.append(newMessage)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}
}
#IBAction func sendPressed(_ sender: UIButton) {
if let messageBody = messageTextfield.text, let messageSender = Auth.auth().currentUser?.email {
db.collection(K.FStore.collectionName).addDocument(data: [K.FStore.senderField: messageSender, K.FStore.bodyField: messageBody]) { (error) in
if let e = error {
print("There was an issue saving data to firestore \(e)")
} else {
print("successfully saved data.")
}
}
}
}
#IBAction func logOutPressed(_ sender: UIBarButtonItem) {
do {
try Auth.auth().signOut()
navigationController?.popToRootViewController(animated: true)
} catch let signOutError as NSError {
print ("Error signing out: %#", signOutError)
}
}
}
extension ChatViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! MessageCell
cell.label.text = messages[indexPath.row].body
return cell
}
}
`
And below is console output
'<FIRFirestore: 0x6000016d7040>
2022-10-17 17:55:19.817200+0300 Flash Chat iOS13[5876:41767] 10.0.0 - [FirebaseAnalytics][I-ACS023007] Analytics v.10.0.0 started
2022-10-17 17:55:19.817551+0300 Flash Chat iOS13[5876:41767] 10.0.0 - [FirebaseAnalytics][I-ACS023008] To enable debug logging set the following application argument: -FIRAnalyticsDebugEnabled (see xxx
2022-10-17 17:55:19.824914+0300 Flash Chat iOS13[5876:41677] [Assert] UINavigationBar decoded as unlocked for UINavigationController, or navigationBar delegate set up incorrectly. Inconsistent configuration may cause problems. navigationController=<UINavigationController: 0x13984a200>, navigationBar=<UINavigationBar: 0x13950f870; frame = (0 48; 0 50); opaque = NO; autoresize = W; layer = <CALayer: 0x600000390980>> delegate=0x13984a200
2022-10-17 17:55:19.873401+0300 Flash Chat iOS13[5876:41763] 10.0.0 - [FirebaseAnalytics][I-ACS800023] No pending snapshot to activate. SDK name: app_measurement
2022-10-17 17:55:19.890442+0300 Flash Chat iOS13[5876:41765] 10.0.0 - [FirebaseAnalytics][I-ACS023012] Analytics collection enabled
2022-10-17 17:55:19.890731+0300 Flash Chat iOS13[5876:41765] 10.0.0 - [FirebaseAnalytics][I-ACS023220] Analytics screen reporting is enabled. Call Analytics.logEvent(AnalyticsEventScreenView, parameters: [...]) to log a screen view event. To disable automatic screen reporting, set the flag FirebaseAutomaticScreenReportingEnabled to NO (boolean) in the Info.plist
successfully saved data.'

How to prevent Firestore write race conditions for a reservation button

Summary
I'm developing an application where users can reserve and cancel reservations for classes. In a ReservationButtonView I two buttons that add and remove a user to a workout class respectively. Currently the button I show is based off whether the user's Firebase Auth uid is listed in a Firestore document.
I was having issues when rapidly tapping on the reservation button. Specifically, the reservationCnt would become inaccurate by showing more or less than the actual users reserved for a class.
The only way I have found to resolve this is be using a Firestore transaction that checks to see if a user is in a workout class already. If they are, addReservation() now does nothing. If they aren't, removeReservation() would also do nothing.
At first I thought I could just disable the button and via the logic still in place the code below (.disabled()), but that alone didn't work as I ran into the above described race conditions. What I found out is that arrayUnion and arrayRemove still succeed even when the object I'm looking to add is there and not there respectively. Meaning it is possible for my transaction to not remove a reservedUser that isn't there and also decrease the reservationCnt which can leave me with say no reserved users and a reservationCnt of -1
The Ask
Is there a better way to handle this reservation process? Can I accomplish this without a transaction for at least the removal of users in some way. Ideally, I'd like to have a spinner replace the button as I add or remove a user's reservation to indicate to the user that the app is processing the request. Perhaps I need two variables to manage the disabled() state instead of one?
MVVM Code Snippets
NOTE: I pulled out some button styling to make the code a bit less verbose
ReservationButtonView
struct ReservationButtonView: View {
var workoutClass: WorkoutClass
#ObservedObject var viewModel: WorkoutClassViewModel
#EnvironmentObject var authViewModel: AuthViewModel
var body: some View {
if checkIsReserved(uid: authViewModel.user?.uid ?? "", reservedUsers: workoutClass.reservedUsers ?? []) {
Button(action: {
viewModel.isDisabled = true
viewModel.removeReservation(
documentId: workoutClass.id!,
reservedUserDetails: ["uid": authViewModel.user?.uid as Any, "photoURL": authViewModel.user?.photoURL?.absoluteString ?? "" as Any, "displayName": authViewModel.user?.displayName ?? "Bruin Fitness Member" as Any],
uid: authViewModel.user?.uid ?? "")
}){
Label(
title: { Text("Cancel Reservation")
.font(.title) },
icon: { Image(systemName: "person.badge.minus")
.font(.title) }
)
}.disabled(viewModel.isDisabled)
} else{
Button(action: {
viewModel.isDisabled = true
viewModel.addReservation(
documentId: workoutClass.id!,
reservedUserDetails: ["uid": authViewModel.user?.uid as Any, "photoURL": authViewModel.user?.photoURL?.absoluteString ?? "" as Any, "displayName": authViewModel.user?.displayName ?? "Bruin Fitness Member" as Any],
uid: authViewModel.user?.uid ?? "")
}){
Label(
title: { Text("Reserve")
.font(.title) },
icon: { Image(systemName: "person.badge.plus")
.font(.title) }
)
}
.disabled(viewModel.isDisabled)
}
}
}
func checkIsReserved(uid: String, reservedUsers: [reservedUser]) -> Bool {
return reservedUsers.contains { $0.uid == uid }
}
WorkoutClassModel
struct reservedUser: Codable, Identifiable {
var id: String = UUID().uuidString
var uid: String
var photoURL: URL?
var displayName: String?
enum CodingKeys: String, CodingKey {
case uid
case photoURL
case displayName
}
}
struct WorkoutClass: Codable,Identifiable {
#DocumentID var id: String?
var reservationCnt: Int
var time: String
var workoutType: String
var reservedUsers: [reservedUser]?
enum CodingKeys: String, CodingKey {
case id
case reservationCnt
case time
case workoutType
case reservedUsers
}
}
WorkoutClassViewModel
class WorkoutClassViewModel: ObservableObject {
#Published var isDisabled = false
private var db = Firestore.firestore()
func addReservation(documentId: String, reservedUserDetails: [String: Any], uid: String){
let incrementValue: Int64 = 1
let increment = FieldValue.increment(incrementValue)
let addUser = FieldValue.arrayUnion([reservedUserDetails])
let classReference = db.document("schedules/Redwood City/dates/\(self.stateDate.dbDateFormat)/classes/\(documentId)")
db.runTransaction { transaction, errorPointer in
let classDocument: DocumentSnapshot
do {
print("Getting classDocument for docId: \(documentId) in addReservedUser()")
try classDocument = transaction.getDocument(classReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let workoutClass = try? classDocument.data(as: WorkoutClass.self) else {
let error = NSError(
domain: "AppErrorDomain",
code: -3,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve workoutClass from snapshot \(classDocument)"
]
)
errorPointer?.pointee = error
return nil
}
let isReserved = self.checkIsReserved(uid: uid, reservedUsers: workoutClass.reservedUsers ?? [])
if isReserved {
print("user is already in class so therefore can't be added again")
return nil
} else {
transaction.updateData(["reservationCnt": increment, "reservedUsers": addUser], forDocument: classReference)
return nil
}
} completion: { object, error in
if let error = error {
print(error.localizedDescription)
self.isDisabled = false
} else {
print("Successfully ran transaction with object: \(object ?? "")")
self.isDisabled = false
}
}
}
func removeReservation(documentId: String, reservedUserDetails: [String: Any], uid: String){
let decrementValue: Int64 = -1
let decrement = FieldValue.increment(decrementValue)
let removeUser = FieldValue.arrayRemove([reservedUserDetails])
let classReference = db.document("schedules/Redwood City/dates/\(self.stateDate.dbDateFormat)/classes/\(documentId)")
db.runTransaction { transaction, errorPointer in
let classDocument: DocumentSnapshot
do {
print("Getting classDocument for docId: \(documentId) in addReservedUser()")
try classDocument = transaction.getDocument(classReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let workoutClass = try? classDocument.data(as: WorkoutClass.self) else {
let error = NSError(
domain: "AppErrorDomain",
code: -3,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve reservedUsers from snapshot \(classDocument)"
]
)
errorPointer?.pointee = error
return nil
}
let isReserved = self.checkIsReserved(uid: uid, reservedUsers: workoutClass.reservedUsers ?? [] )
if isReserved {
transaction.updateData(["reservationCnt": decrement, "reservedUsers": removeUser], forDocument: classReference)
return nil
} else {
print("user not in class so therefore can't be removed")
return nil
}
} completion: { object, error in
if let error = error {
print(error.localizedDescription)
self.isDisabled = false
} else {
print("Successfully ran removeReservation transaction with object: \(object ?? "")")
self.isDisabled = false
}
}
}
func checkIsReserved(uid: String, reservedUsers: [reservedUser]) -> Bool {
return reservedUsers.contains { $0.uid == uid }
}
}
App screenshot
Reservation button is the green/grey button at the bottom of the view
As this is a race condition, You have already acknowledged the use of Transactions for the update which is the most desirable as this can ensure the update is successful before allowing the App to change button status.
I.e. by using a transaction and only updating the UI Button state on success, which is explained here
The recommendation is to keep the state of the button mapped to what is in the document, therefore you are likely to exceed rate limits by updating the same field continuously based on the flipping of the button.
Another way to handle this tracking of the state of enrollment is to add a new document that indicates the state of the enrollment for the user to a collection that is the class they are enrolling in.
I.e. Rather than having the class user enrolling into being a document, make that a collection and each time the enrollment state changes, write a new document. This will allow for updates to occur without using transactions and the current state of enrollments is contained within the latest document. This latest document can be read and used as the status of the button within the App with the added benefit that the state will always update to the status contained within Firestore.
I ended up resolving this by adding a disable check conditional before the conditional that decides whether to show the "Reserve" or "Cancel" button.
This way when my Firestore transaction is running the user will see a spinner instead and can't monkey test the button. The spinner helps to show that the reservation operation is in progress. When the transaction hits its completion block I disable the isDisabled Bool and the listener is in sync (the user then sees the newly toggled button state)
if workoutClassVM.isDisabled {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Color("bruinGreenColor")))
} else if checkIsReserved(uid: authVM.user?.uid ?? "", reservedUsers: workoutClass.reservedUsers ?? []) {
...

UpdateApplicationContext Not Being Called

It's my first time trying WatchConnectivity kit. When I attempt to send ApplicationContext from phone, my debug print indicates that updateApplicationContext is working correctly (with isPaired/reachable/isActivated/isWatchAppInstalled checked before sending the update). However, I'm having trouble receiving it form the paired watch simulator as nothing is shown on that end. Here are the code for the Phone Extension:
func sendDataToWatch(data: String) {
state.append(data)
do {
NSLog("sent by phone")
try WCSession.default.updateApplicationContext(["currentstate": state])
}
catch {
print(error)
}
}
Here are the code for watch:
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
print("Hello")
if let getState = applicationContext["currentstate"] as? [String]{
print("\(getState)")
self.state = getState[0]
}
}
Any suggestion would be appreciated!

Combine: Wrapped async call with Future, but Future.sink doesn't appear to complete

First-time poster, long-time reader ... I've wrapped an async call to the Firebase Authorization API. I'm calling it from inside a SwiftUI View function.
func authenticateFirebaseEmail(email: String, password: String) ->
Future<String, Error> {
return Future<String,Error> { promise in
Auth.auth().signIn(withEmail: email, password: password) { result, error in
if let error=error {
print("failure detected")
promise(.failure(error))
}
if let result=result {
print("result detected - returning success promise")
promise(.success(result.user.email!))
}
}
}
}
...
func logMeInFuncInView() {
var cancellable : AnyCancellable?
cancellable = authenticateFirebaseEmail(email: self.userEmail, password: self.password).map( {
value in return value
})
.sink(receiveCompletion: { (completion) in
print("completion received")
}, receiveValue: { user in
print("value received")
self.errorMessage = user
})
}
The console output is as follows, but never reaches the "completion received" or "value received" calls:
result detected - returning successful promise
Is the issue with the wrapped callback, the future, the way I'm using the future, or something that I'm not seeing entirely?
Your cancellable is local variable, so destroyed once went off context. As soon as subscriber is destroyed it cancels subscription and, as it is only one, publisher cancelled as well.
Your solution is to make your cancellable as property, ie
var cancellable : AnyCancellable? // << here !!
func logMeInFuncInView() {
cancellable = authenticateFirebaseEmail(email: self.userEmail, password: self.password).map( {
value in return value
})
// .. other code
}

Problem with WatchConnectivity transferFile (watch -> iphone)

I want to send a file created on the watch to the iOS companion-app, using WatchConnectivity and a setup WCSession, but it does not get through to the iPhone. When I use send a message containing a Dictionary in stead, the data does get to the iPhone.
Both AppDelegate and ExtensionDelegate uses NotificationCenter to communicate with ViewController and ExtensionController. They are left out for simplicity, but the notifications work fine.
iOS' AppDelegate.swift
extension AppDelegate: WCSessionDelegate {
// 1
func sessionDidBecomeInactive(_ session: WCSession) {
print("WC Session did become inactive")
}
// 2
func sessionDidDeactivate(_ session: WCSession) {
print("WC Session did deactivate")
WCSession.default.activate()
}
// 3
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if let error = error {
print("WC Session activation failed with error: \(error.localizedDescription)")
return
}
print("WC Session activated with state: \(activationState.rawValue)")
}
func setupWatchConnectivity() {
// 1
if WCSession.isSupported() {
// 2
let session = WCSession.default
// 3
session.delegate = self
// 4
session.activate()
}
}
func session(_ session: WCSession, didFinish fileTransfer: WCSessionFileTransfer, error: Error?) {
print("didFinish fileTransfer")
}
func session(_ session: WCSession, didReceive file: WCSessionFile) {
print("didReceive file")
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) { // 2
print("didReceiveMessage reached")
}
}
On the watch I have:
ExtensionDelegate.swift
extension ExtensionDelegate: WCSessionDelegate {
func setupWatchConnectivity() {
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate()
}
}
func session(_ session: WCSession, activationDidCompleteWith
activationState: WCSessionActivationState, error: Error?) {
if let error = error {
print("WC Session activation failed with error: " +
"\(error.localizedDescription)")
return
}
print("WC Session activated with state: " +
"\(activationState.rawValue)")
}
func sendFileToPhone(_ notification:Notification) {
// 1
if WCSession.isSupported() && WCSession.default.isReachable {
// 2
if let fileURL = notification.object as? URL {
print("Sending file for URL: \(fileURL.absoluteURL.absoluteString)")
WCSession.default.transferFile(fileURL, metadata: nil) // <- if I use sendMessage here stuff works...
}
}
}
func session(_ session: WCSession, didFinish fileTransfer: WCSessionFileTransfer, error: Error?) {
// handle filed transfer completion
print("File transfer complete")
print("Outstanding file transfers: \(WCSession.default.outstandingFileTransfers)")
print("Has content pending: \(WCSession.default.hasContentPending)")
}
func session(_session: WCSession, didFinishFileTransfer fileTransfer: WCSessionFileTransfer, error: NSError?) {
print("error: ", error as Any)
}
}
After the file is sent from the Watch I inspect, as you see, outstandingFileTransfers and hasContentPending properties of the WCSession, and they indicate that the file should have been transferred, but my :didReceive: method of my AppDelegate extension doesn't get called. Again, when I use sendMessage, the AppDelegate's :didReceiveMessage: is invoked as expected.
What am I missing?
Note: I've tried to run this Apple's demo project that features the different communication methods, and I experience the same thing: transferFile does not trigger anything on counterpart.
It seems to be a bug of iOS 13/watchOS 6 simulator. I've tried to change Xcode simulators to iOS 12 and watchOS 5, how it's suggested in this thread and the issue disappeared.

Resources