How to use SignalProducer.timer for ReactiveCocoa 9.0 - ios

I'm working on updating an app from Swift 3 to Swift 5. This requires updating ReactiveCocoa. There is some code that was using a timer within a SignalProducer that no longer works when updating. Here is the before and after:
Swift 3 Code
func start(beats: [Beat]) -> SignalProducer<PlayedBeat, Error> {
var index: Int = 0
let oneSecondTimer = timer(interval: DispatchTimeInterval.seconds(1), on: QueueScheduler(), leeway: DispatchTimeInterval.milliseconds(100))
.on(starting: {
// initialize
index = -1
}, value: { date in
index += 1
})
.flatMap(.latest) { [weak self] (_: Date) -> SignalProducer<PlayedBeat, Error> in
guard let strongSelf = self else {
return SignalProducer<PlayedBeat, Error>(error: Error.unknown("self reference missing"))
}
if index < beats.count {
let data = beats[index].toDeviceFormat()
strongSelf.device.writeRate(data: data, delegate: strongSelf)
return SignalProducer<PlayedBeat, Error>(value: "boom")
} else {
return SignalProducer<PlayedBeat, Error>(error: Error.noMoreData).delay(1.0, on: QueueScheduler.main)
}
}
let startSignal = SignalProducer<PlayedBeat, Error> { [weak self] (innerSink, disposable) in
guard let strongSelf = self else {
innerSink.send(error: Error.unknown("self reference missing"))
return
}
strongSelf.beatSink = innerSink
let beatDisposable = oneSecondTimer.observe(innerSink, during: strongSelf.reactive.lifetime)
disposable += {
let data = DatedBeat.stopPlayingDeviceFormat()
strongSelf.device.writeRate(data: data, delegate: strongSelf)
}
disposable.add(beatDisposable)
}
return startSignal
.on(
started: { _ in
print("Start sending beats")
},
failed: { (error) in
print("error \(error)")
},
completed: { _ in
print("completed stopped beating ")
})
}
Swift 5 Code
func start(beats: [DatedBeat]) -> SignalProducer<PlayedBeat, Error> {
var index: Int = 0
let oneSecondTimer = SignalProducer.timer(interval: DispatchTimeInterval.seconds(1), on: QueueScheduler.main, leeway: DispatchTimeInterval.milliseconds(100))
.on(starting: {
// initialize
index = -1
}, value: { date in
index += 1
})
.flatMap(.latest) { [weak self] (_: Date) -> SignalProducer<PlayedBeat, Error> in
guard let strongSelf = self else {
return SignalProducer<PlayedBeat, Error>(error: Error.unknown("self reference missing"))
}
if index < beats.count {
let data = beats[index].toDeviceFormat()
strongSelf.device.writeRate(data: data, delegate: strongSelf)
return SignalProducer<PlayedBeat, Error>(value: "boom")
} else {
return SignalProducer<PlayedBeat, Error>(error: Error.noMoreData)
.delay(1.0, on: QueueScheduler.main)
}
}
let startSignal = SignalProducer<PlayedBeat, Error> { [weak self] (innerSink, lifetime) in
guard let strongSelf = self else {
innerSink.send(error: Error.unknown("self reference missing"))
return
}
strongSelf.beatSink = innerSink
if lifetime.hasEnded {
let data = DatedBeat.stopPlayingDeviceFormat()
strongSelf.device.writeRate(data: data, delegate: strongSelf)
innerSink.sendInterrupted()
return
}
_ = oneSecondTimer.observe(on: innerSink as! Scheduler)
}
return startSignal.on(
started: {
print("Start sending beats")
},
failed: { (error) in
print("error \(error)")
},
completed: {
print("completed stopped beating ")
})
}
Currently the line _ = oneSecondTimer.observe(on: innerSink as! Scheduler) throws an error when trying to case innerSink to Scheduler. If I remove that line, then the timer is never started.
If anyone has thoughts on the proper way to start a timer in ReactiveCocoa, that would be super helpful. Thanks!

Related

How to refactor duplicate Firestore document IDs in Swift?

I'm doing my very first IOS app using Cloud Firestore and have to make the same queries to my database repeatedly. I would like to get rid of the duplicate lines of code. This is examples of func where documents ID are duplicated. Also I using other queries as .delete(), .addSnapshotListener(), .setData(). Should I refactor all that queries somehow or leave them because they were used just for one time?
#objc func updateUI() {
inputTranslate.text = ""
inputTranslate.backgroundColor = UIColor.clear
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { [self] (document, error) in
if let document = document, document.exists {
let document = document
let label = document.data()?.keys.randomElement()!
self.someNewWord.text = label
// Fit the label into screen
self.someNewWord.adjustsFontSizeToFitWidth = true
self.checkButton.isHidden = false
self.inputTranslate.isHidden = false
self.deleteBtn.isHidden = false
} else {
self.checkButton.isHidden = true
self.inputTranslate.isHidden = true
self.deleteBtn.isHidden = true
self.someNewWord.adjustsFontSizeToFitWidth = true
self.someNewWord.text = "Add your first word to translate"
updateUI()
}
}
}
#IBAction func checkButton(_ sender: UIButton) {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { (document, error) in
let document = document
let label = self.someNewWord.text!
let currentTranslate = document!.get(label) as? String
let translateField = self.inputTranslate.text!.lowercased().trimmingCharacters(in: .whitespaces)
if translateField == currentTranslate {
self.inputTranslate.backgroundColor = UIColor.green
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
self.inputTranslate.backgroundColor = UIColor.clear
updateUI()}
} else {
self.inputTranslate.backgroundColor = UIColor.red
self.inputTranslate.shakingAndRedBg()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
self.inputTranslate.backgroundColor = UIColor.clear
self.inputTranslate.text = ""
}
}
}
}
func deletCurrentWord () {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { (document, err) in
let document = document
if let err = err {
print("Error getting documents: \(err)")
} else {
let array = document!.data()
let counter = array!.count
if counter == 1 {
// The whole document will deleted together with a last word in list.
let user = Auth.auth().currentUser?.email
self.db.collection(K.FStore.collectionName).document(user!).delete() { err in
if let err = err {
print("Error removing document: \(err)")
} else {
self.updateUI()
}
}
} else {
// A current word will be deleted
let user = Auth.auth().currentUser?.email
let wordForDelete = self.someNewWord.text!
self.db.collection(K.FStore.collectionName).document(user!).updateData([
wordForDelete: FieldValue.delete()
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
self.updateUI()
}
}
}
}
}
}
Another query example
func loadMessages() {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.addSnapshotListener { (querySnapshot, error) in
self.messages = []
if let e = error {
print(e)
} else {
if let snapshotDocuments = querySnapshot?.data(){
for item in snapshotDocuments {
if let key = item.key as? String, let translate = item.value as? String {
let newMessage = Message(key: key, value: translate)
self.messages.append(newMessage)
}
}
DispatchQueue.main.async {
self.messages.sort(by: {$0.value > $1.value})
self.secondTableView.reloadData()
let indexPath = IndexPath(row: self.messages.count - 1, section: 0)
self.secondTableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
}
}
}
}
}
enum Error {
case invalidUser
case noDocumentFound
}
func fetchDocument(onError: #escaping (Error) -> (), completion: #escaping (FIRQueryDocument) -> ()) {
guard let user = Auth.auth().currentUser?.email else {
onError(.invalidUser)
return
}
db.collection(K.FStore.collectionName).document(user).getDocument { (document, error) in
if let error = error {
onError(.noDocumentFound)
} else {
completion(document)
}
}
}
func updateUI() {
fetchDocument { [weak self] error in
self?.hideShowViews(shouldHide: true, newWordText: nil)
} completion: { [weak self] document in
guard document.exists else {
self?.hideShowViews(shouldHide: true, newWordText: nil)
return
}
self?.hideShowViews(shouldHide: false, newWordText: document.data()?.keys.randomElement())
}
}
private func hideShowViews(shouldHide: Bool, newWordText: String?) {
checkButton.isHidden = shouldHide
inputTranslate.isHidden = shouldHide
deleteBtn.isHidden = shouldHide
someNewWord.adjustsFontSizeToFitWidth = true
someNewWord.text = newWordText ?? "Add your first word to translate"
}
The updateUI method can easily be refactored using a simple guard statement and then taking out the common code into a separate function. I also used [weak self] so that no memory leaks or retain cycles occur.
Now, you can follow the similar approach for rest of the methods.
Use guard let instead of if let to avoid nesting.
Use [weak self] for async calls to avoid memory leaks.
Take out the common code into a separate method and use a Bool flag to hide/show views.
Update for step 3:
You can create methods similar to async APIs for getDocument() or delete() etc and on completion you can update UI or perform any action. You can also create a separate class and move the fetchDocument() and other similar methods in there and use them.

Swift: run function when for in loop completes and responses returned

I've been trying multiple ways to run a function in a for in loop and when all have returned to run another function but for some reason it appears the final function is running before one or more of the others have returned a result.
This is my latest attempt: (both functions work it is just the order which is the issue)
var counter: Int = 0
for owner in arrOwnerList {
self.removeDevice(device: self.device, account: owner as! String) { (isSuccess) -> Void in
print(isSuccess)
if isSuccess {
}
}
}
if self.arrOwnerList.count == self.counter {
self.removeDeviceFromServer(device: self.device)
self.sendEmail(to:"gordon#myemail.co.uk", subject:self.device+" has been removed", text:self.device+" has been removed from the server, please check the sim for bar and termination")
}
}
func removeDevice(device: String, account: String, completion: #escaping (Bool) -> Void) {
let dictHeader : [String:String] = ["username":username,"password":password]
let dictArray = [device]
self.counter += 1
WebHelper.requestPUTAPIRemoveDevice(baseURL+"rootaccount/removedevices/"+account+"?server=MUIR", header: dictHeader, dictArray: dictArray, controllerView: self, success: { (response) in
print(response)
if response.count == 0 {
self.Label1.alpha = 1
print("response count == 0")
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: Messages.ServerError, on: self)
}
}
else {
}
}) { (error) in
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: error?.localizedDescription ?? Messages.ServerError, on: self)
}
}
let isSuccess = true
self.Label1.alpha = 1
completion(isSuccess)
}
func removeDeviceFromServer(device: String) {
let dictHeader : [String:String] = ["username":username,"password":password]
WebHelper.requestDELETEAPI(baseURL+"defaultdevice/"+device+"?server=MUIR", header: dictHeader, controllerView: self, success: { (response) in
if response.count == 0 {
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: Messages.ServerError, on: self)
}
}
else {
if response.count != 0 {
self.Label2.alpha = 1
DispatchQueue.main.async {
self.Label2.alpha = 1
}
}
else{
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: Messages.NoDataFound, on: self)
}
}
}
}) { (error) in
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: error?.localizedDescription ?? Messages.ServerError, on: self)
}
}
}
DispatchGroup will solve the problems. (https://developer.apple.com/documentation/dispatch/dispatchgroup)
let dispatchGroup = DispatchGroup()
for owner in arrOwnerList {
dispatchGroup.enter()
self.removeDevice(device: self.device, account: owner as! String) { (isSuccess) -> Void in
defer {
dispatchGroup.leave()
}
print(isSuccess)
if isSuccess {
}
}
}
// This block execute when the loop is completed.
dispatchGroup.notify(queue: .main) { [weak self] in
guard let self = self else { return }
self.removeDeviceFromServer(device: self.device)
self.sendEmail(to:"gordon#myemail.co.uk", subject:self.device+" has been removed", text:self.device+" has been removed from the server, please check the sim for bar and termination")
}

Optional Still Returning Nil After Assigning Value

I am working on a similar feature to 'liking/unliking a post'.
I have an MVVM architecture as;
struct MyStructModel {
var isLiked: Bool? = false
}
class MyStructView {
var isLiked: Bool
init(myStructModel: MyStructModel) {
self.isLiked = myStructModel.isLiked ?? false
}
}
I successfully get the value of whether the post is liked or not here;
func isPostLiked(documentID: String, completion: #escaping (Bool) -> Void) {
guard let authID = auth.id else { return }
let query = reference(to: .users).document(authID).collection("liked").document(documentID)
query.getDocument { (snapshot, error) in
if error != nil {
print(error as Any)
return
}
guard let data = snapshot?.data() else { return }
if let value = data["isLiked"] as? Bool {
completion(value)
} else {
completion(false)
}
}
}
func retrieveReviews(completion: #escaping([MyStructModel]) -> ()) {
var posts = [MyStructModel]()
let query = reference(to: .posts).order(by: "createdAt", descending: true)
query.getDocuments { (snapshot, error) in
if error != nil {
print(error as Any)
return
}
guard let snapshotDocuments = snapshot?.documents else { return }
for document in snapshotDocuments {
if var post = try? JSONDecoder().decodeQuery(MyStructModel.self, fromJSONObject: document.decode()) {
// isLiked is nil here...
self.isPostLiked(documentID: post.documentID!) { (isLiked) in
post.isLiked = isLiked
print("MODEL SAYS: \(post.isLiked!)")
// isLiked is correct value here...
}
posts.append(post)
}
completion(posts)
}
}
}
However, when it gets to my cell the value is still nil.
Adding Cell Code:
var post: MyStructView? {
didSet {
guard let post = post else { return }
print(post.isLiked!)
}
}
Your isLiked property is likely nil in your cells because the retrieveReviews function doesn't wait for the isPostLiked function to complete before completing itself.
You could easily solve this issue by using DispatchGroups. This would allow you to make sure all of your Posts have their isLiked value properly set before being inserted in the array, and then simply use the DispatchGroup's notify block to return all the loaded posts via the completion handler:
func retrieveReviews(completion: #escaping([MyStructModel]) -> ()) {
var posts = [MyStructModel]()
let query = reference(to: .posts).order(by: "createdAt", descending: true)
query.getDocuments { [weak self] (snapshot, error) in
guard let self = self else { return }
if error != nil {
return
}
guard let documents = snapshot?.documents else { return }
let dispatchGroup = DispatchGroup()
for document in documents {
dispatchGroup.enter()
if var post = try? JSONDecoder().decodeQuery(MyStructModel.self, fromJSONObject: document.decode()) {
self.isPostLiked(documentID: post.documentID!) { isLiked in
post.isLiked = isLiked
posts.append(post)
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
completion(posts)
}
}
}

Async function not handled by completion handler

I am using the google maps distance matrix api and using a compeletion handler to pass my async function calls like this:
func configureRoute(origin:String,destination:String, completionHandler: #escaping (_ duration:Int) -> ()){
let jsonURL = "https://maps.googleapis.com/maps/api/distancematrix/json?units=imperial&origins=place_id:\(origin)&destinations=place_id:\(destination)&key=MYAPI"
guard let url = URL(string: jsonURL ) else {return}
print(jsonURL)
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {return}
do {
let route = try JSONDecoder().decode(Welcome.self, from: data)
// print(self.durations)
completionHandler(route.rows[0].elements[0].duration.value)
}
catch let jsonErr {
}
let dataAsString = String(data: data, encoding: .utf8)
}.resume()
}
and then I try and use that result like so, however my output results are still blank and I am unable to use any of the calls that I have received. I am not that good with compeltion handlers, so if anyone could let me know what I have done wrong?
func majorConfigure() {
permutations(placeID.count, &placeID, origin: startPlaceID, destination: endPlaceID)
for eachArray in finalRoutes {
for i in 0..<(eachArray.count-1) {
configureRoute(origin: eachArray[i], destination: eachArray[i+1]){
duration in
self.durations.append(duration)
}
}
groupedDurations.append(durations)
durations.removeAll()
}
print(groupedDurations)
}
After using Dispatch group this is my updated code:
func majorConfigure(){
permutations(placeID.count, &placeID, origin: startPlaceID, destination: endPlaceID)
let dispatchGroup = DispatchGroup()
for eachArray in finalRoutes{
for i in 0..<(eachArray.count-1){
dispatchGroup.enter()
configureRoute(origin: eachArray[i], destination: eachArray[i+1]){
duration in
self.durations.append(duration)
print(self.groupedDurations)
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main){
self.groupedDurations.append(self.durations)
self.durations.removeAll()
}
}
}
The result when I print groupedDurations is:
[]
[]
[]
[]
[]
[]
give this a try :
func majorConfigure() {
permutations(placeID.count, &placeID, origin: startPlaceID, destination: endPlaceID)
for eachArray in finalRoutes {
for i in 0..<(eachArray.count-1) {
configureRoute(origin: eachArray[i], destination: eachArray[i+1]){
duration in
self.durations.append(duration)
if i == eachArray.count - 1{
groupedDurations.append(durations)
}
if i == eachArray.count - 1 && eachArray == finalRoutes.last! {
print(groupedDurations)
durations.removeAll()
}
}
}
}
}
EDITED ANSWER
You can achieve this in this manner :
var indexFinalRoutes = 0
var indexEachArray = 0
func loopFinalRoutes(){
self.indexEachArray = 0
let eachArray = finalRoutes[indexFinalRoutes]
loopEachArray(eachArray: eachArray) { (success) in
self.indexFinalRoutes = self.indexFinalRoutes + 1
if self.indexFinalRoutes < self.finalRoutes.count - 1{
self.loopFinalRoutes()
}else{
self.indexFinalRoutes = 0
print(self.groupedDurations)
}
}
}
func loopEachArray(eachArray : [String] ,
completion: #escaping (_ success:Bool) -> Void){
if indexEachArray + 1 < eachArray.count - 1 {
configureRoute(origin: eachArray[indexEachArray], destination: eachArray[indexEachArray+1]){
duration in
self.durations.append(duration)
self.indexEachArray = self.indexEachArray + 1
let nextEachArray = self.finalRoutes[self.indexFinalRoutes]
self.loopEachArray(eachArray: nextEachArray, completion: completion)
}
}else{
groupedDurations.append(durations)
durations.removeAll()
completion(true)
}
}
call loopFinalRoutes functuin from your majorConfigure function and you will have the output.

Firebase Cloud functions is slow

We use Firebase as a backend for our service. It has a PeopleViewController that displays users within a radius of the current user. The radius can vary from 1 to 40 miles. Also there is a check for blocked users, age, sex. At the very beginning of the development of the project, a temporary implementation was made for this capability within the application. I created several managers who fulfilled these requests, and then gave the result to PeopleViewController. At the beginning of the project, because of the tight deadlines, pagination was not implemented, but the pagination was not particularly needed since the users even in the radius were less than 100. Now, in one radius there can be more than 600 users and I think it is not right that these requests are performed inside application. We move these functions from the app Side to the cloud function so that we can adjust how many users should be in each issue.
The problem is that even a warmed-up cloud function works slower than all functions related to this capability inside the application. On average, the code inside the application for a radius of 40 miles and the inclusion of all age groups returns 646 users in 1.5 seconds, while the best result is cloud functions 3.6. Below I will give examples of the time and code with the app side and cloud funciton.
The results are obtained using the code that is executed in the application:
1 number of users 646, 1.6775569915771484 seconds
2 number of users 646, 1.4022901058197021 seconds
3 number of users 646, 1.5957129001617432 seconds
The results with the cloud function time are specified in milliseconds. The parameters and location are the same as the code that is run from the application:
1 "timers": {
"afterVerify": 1,
"gotIdsFromGeofire": 1372,
"gotBlockedUsersIDs": 2463,
"gotBlockedByUsersIDs": 2467,
"gotUserSearchLocationModels": 5256,
"gotAllInitData": 5256,
"filtersAndSortsDone": 6168
},
2 "timers": {
"afterVerify": 6,
"gotIdsFromGeofire": 1209,
"gotBlockedUsersIDs": 1909,
"gotBlockedByUsersIDs": 1913,
"gotUserSearchLocationModels": 3800,
"gotAllInitData": 3800,
"filtersAndSortsDone": 4207
},
3 "timers": {
"afterVerify": 1,
"gotIdsFromGeofire": 812,
"gotBlockedUsersIDs": 1415,
"gotBlockedByUsersIDs": 1419,
"gotUserSearchLocationModels": 3912,
"gotAllInitData": 3913,
"filtersAndSortsDone": 4417
},
In the managers who request a list of users and filter the received data within the application.
extension PeopleListViewController {
#objc fileprivate func requestPeopleNearBy() {
let firPeopleSearchDatabaseManagerStart = Date().timeIntervalSince1970
firPeopleSearchDatabaseManager.searchPeople(success: { [weak self](userSearchLocationModels) in
let firPeopleSearchDatabaseManagerEnd = Date().timeIntervalSince1970
let resultTimeStamp = firPeopleSearchDatabaseManagerEnd - firPeopleSearchDatabaseManagerStart
debugPrint("firPeopleSearchDatabaseManager userSearchLocationModels", userSearchLocationModels.count, "resultTimeStamp", resultTimeStamp)
}) { (error) in
}
}
}
class FIRPeopleSearchDatabaseManager {
fileprivate enum MainGateways {
case userSearchLocationModel, userLocations
var description: String {
switch self {
case .userSearchLocationModel:
return "userSearchLocationModel"
case .userLocations:
return "userLocations"
}
}
}
func saveUserSearchLocationModel(_ userSearchLocationModel: UserSearchLocationModel, success: (() -> Void)?, fail: ((_ error: Error) -> Void)?) {
DispatchQueue.global(qos: .background).async {
let ref = Database.database().reference().child(MainGateways.userSearchLocationModel.description).child(userSearchLocationModel.userID)
let json = userSearchLocationModel.toJSON()
ref.updateChildValues(json, withCompletionBlock: { (error, ref) in
guard error == nil else {
fail?(error!)
return
}
success?()
})
}
}
private func downloadUserSearchLocationModel(_ userID: String, success: ((_ userSearchLocationModel: UserSearchLocationModel) -> Void)?, notExist: (() -> Void)?, fail: ((_ error: Error) -> Void)?) {
DispatchQueue.global(qos: .background).async {
let ref = Database.database().reference().child(MainGateways.userSearchLocationModel.description).child(userID)
ref.observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.value is NSNull {
notExist?()
return
}
guard let json = snapshot.value as? [String : Any] else {
debugPrint("doest exist", userID)
notExist?()
return
}
guard let userSearchLocationModel = Mapper<UserSearchLocationModel>().map(JSON: json) else {
debugPrint("doest exist", userID)
notExist?()
return
}
guard !userSearchLocationModel.userID.isEmpty else {
notExist?()
return
}
success?(userSearchLocationModel)
}, withCancel: { (error) in
fail?(error)
})
}
}
func searchPeople(success: ((_ searchLocationModels: [UserSearchLocationModel]) -> Void)?, fail: ((_ error: Error) -> Void)?) {
let realmManager = RealmManager()
guard let currentUserID = realmManager.getCurrentUser()?.id else { return }
guard let location = LocationManager.shared.currentLocation else { return }
let realmUserSettingsManager = RealmUserSettingsManager()
let miles = realmUserSettingsManager.getMaxDistanceInMiles() ?? 15
let distanceConverter = DistanceConvertor()
let radius = distanceConverter.convertedMilesToMetersForFIRRequest(miles)
DispatchQueue.global(qos: .background).async {
let usersGeofireRef = Database.database().reference().child(MainGateways.userLocations.description)
guard let geoFire = GeoFire(firebaseRef: usersGeofireRef) else { return }
guard let circleQuery = geoFire.query(at: location, withRadius: radius) else { return }
var usersListIDs = [String]()
var generalBlockModel = [BlockModel]()
let dispatchGroup = DispatchGroup()
let blockSystemManager = BlockSystemManager()
dispatchGroup.enter()
blockSystemManager.requestBlockingUserIDs(completion: { (blockModels, error) in
generalBlockModel = blockModels
dispatchGroup.leave()
})
dispatchGroup.enter()
circleQuery.observe(.keyEntered, with: { (key, location) in
if let _key = key {
if _key != currentUserID {
usersListIDs.append(_key)
}
}
})
circleQuery.observeReady({
circleQuery.removeAllObservers()
dispatchGroup.leave()
})
dispatchGroup.notify(queue: .global(qos: .background), execute: {
if usersListIDs.count == 0 {
circleQuery.removeAllObservers()
success?([])
return
}
var unblockedUserListIDs = [String]()
for usersListID in usersListIDs {
let isContains = generalBlockModel.contains(where: { $0.userID == usersListID })
if !isContains {
unblockedUserListIDs.append(usersListID)
}
}
self.downloadUsers(usersListIDs: unblockedUserListIDs, success: success)
})
}
}
private func downloadUsers(usersListIDs: [String], success: ((_ searchLocationModels: [UserSearchLocationModel]) -> Void)?) {
var userIDsCount = usersListIDs.count
var userSearchLocationModels = [UserSearchLocationModel]()
for userID in usersListIDs {
self.downloadUserSearchLocationModel(userID, success: { (userSearchLocationModel) in
userSearchLocationModels.append(userSearchLocationModel)
if userIDsCount == userSearchLocationModels.count {
self.filter(userSearchLocationModels, success: { (sortedUserSearchLocationModels) in
success?(sortedUserSearchLocationModels)
})
}
}, notExist: {
userIDsCount -= 1
if userIDsCount == userSearchLocationModels.count {
self.filter(userSearchLocationModels, success: { (sortedUserSearchLocationModels) in
success?(sortedUserSearchLocationModels)
})
}
}, fail: { (error) in
userIDsCount -= 1
if userIDsCount == userSearchLocationModels.count {
self.filter(userSearchLocationModels, success: { (sortedUserSearchLocationModels) in
success?(sortedUserSearchLocationModels)
})
}
})
}
}
private func filter(_ userSearchLocationModels: [UserSearchLocationModel], success: ((_ sortedUserSearchLocationModels: [UserSearchLocationModel]) -> Void)?) {
filterByUserSearchFilter(userSearchLocationModels) { (searchFilteredUserSearchLocationModels) in
self.filterByOnlineAndRecentLaunchApp(searchFilteredUserSearchLocationModels, success: { (onlineRecentFilteredUserSearchLocationModels) in
success?(onlineRecentFilteredUserSearchLocationModels)
})
}
}
private func filterByUserSearchFilter(_ userSearchLocationModels: [UserSearchLocationModel], success: ((_ sortedUserSearchLocationModels: [UserSearchLocationModel]) -> Void)?) {
DispatchQueue.main.async {
let timeManager = TimeManager()
let realmUserSettingsManager = RealmUserSettingsManager()
guard let cuPeopleFilterSettings = realmUserSettingsManager.getUserPeopleFilterSettings() else { return }
var sortedUserSearchLocationModels = [UserSearchLocationModel]()
for userSearchLocationModel in userSearchLocationModels {
let userYearAge = timeManager.getUserAgeFromBirthdayTimeStamp(userSearchLocationModel.birthdayTimeStamp)
if cuPeopleFilterSettings.filterGenderModeIndex != 2 {
// sorting with mode
if cuPeopleFilterSettings.maxAgeValue != UserPeopleFilterSettings.Standard.maxAge {
if userYearAge >= cuPeopleFilterSettings.minAgeValue && userYearAge <= cuPeopleFilterSettings.maxAgeValue && userSearchLocationModel.genderIndex == cuPeopleFilterSettings.filterGenderModeIndex {
sortedUserSearchLocationModels.append(userSearchLocationModel)
}
} else {
// max age settings
if userYearAge >= cuPeopleFilterSettings.minAgeValue && userSearchLocationModel.genderIndex == cuPeopleFilterSettings.filterGenderModeIndex {
sortedUserSearchLocationModels.append(userSearchLocationModel)
}
}
} else {
if cuPeopleFilterSettings.maxAgeValue != UserPeopleFilterSettings.Standard.maxAge {
if userYearAge >= cuPeopleFilterSettings.minAgeValue && userYearAge <= cuPeopleFilterSettings.maxAgeValue {
sortedUserSearchLocationModels.append(userSearchLocationModel)
}
} else {
// max age in settings
if userYearAge >= cuPeopleFilterSettings.minAgeValue {
sortedUserSearchLocationModels.append(userSearchLocationModel)
}
}
}
}
//sorting by online status and last seen timestamp
let onlineUsers = sortedUserSearchLocationModels.filter { $0.isOnline }
let otherUsersSortedByTimeStamp = sortedUserSearchLocationModels.filter { !$0.isOnline }.sorted { $0.lastSeenTimeStamp > $1.lastSeenTimeStamp }
var resultSortedArray = [UserSearchLocationModel]()
resultSortedArray.append(contentsOf: onlineUsers)
resultSortedArray.append(contentsOf: otherUsersSortedByTimeStamp)
success?(resultSortedArray)
}
}
/// 1. online users sorted by most recently active on the app 2. offline users sorted by most recently active on the app
private func filterByOnlineAndRecentLaunchApp(_ userSearchLocationModels: [UserSearchLocationModel], success: ((_ sortedUserSearchLocationModels: [UserSearchLocationModel]) -> Void)?) {
DispatchQueue.global(qos: .background).async {
let sortedUsers = userSearchLocationModels.sorted(by: { $0.recentActivityTimeStamp > $1.recentActivityTimeStamp })
success?(sortedUsers)
}
}
}
From BlockSystemManager
extension BlockSystemManager {
func requestBlockingUserIDs(completion: ((_ blockModels: [BlockModel], _ error: Error?) -> Void)?) {
DispatchQueue.main.async {
guard let currentUserID = RealmManager().getCurrentUser()?.id else { return }
DispatchQueue.global(qos: .background).async {
let dispatchGroup = DispatchGroup()
var generalBlockModels = [BlockModel]()
let blockedUsersRef = Database.database().reference().child(MainPath.blockingInfo.description).child(currentUserID).child(SubPath.blockedUsers.description)
dispatchGroup.enter()
blockedUsersRef.observeSingleEvent(of: .value, with: { (snap) in
if snap.value is NSNull {
dispatchGroup.leave()
return
}
guard let dict = snap.value as? [String : [String : Any]] else {
dispatchGroup.leave()
return
}
guard let blockModelsDict = Mapper<BlockModel>().mapDictionary(JSON: dict)?.values else {
dispatchGroup.leave()
return
}
let blockModels = Array(blockModelsDict)
generalBlockModels += blockModels
dispatchGroup.leave()
}, withCancel: { (error) in
dispatchGroup.leave()
})
let blockedByUsersRef = Database.database().reference().child(MainPath.blockingInfo.description).child(currentUserID).child(SubPath.blockedByUsers.description)
dispatchGroup.enter()
blockedByUsersRef.observeSingleEvent(of: .value, with: { (snap) in
if snap.value is NSNull {
dispatchGroup.leave()
return
}
guard let dict = snap.value as? [String : [String : Any]] else {
dispatchGroup.leave()
return
}
guard let blockModelsDict = Mapper<BlockModel>().mapDictionary(JSON: dict)?.values else {
dispatchGroup.leave()
return
}
let blockModels = Array(blockModelsDict)
generalBlockModels += blockModels
dispatchGroup.leave()
}, withCancel: { (error) in
dispatchGroup.leave()
})
dispatchGroup.notify(queue: .global(qos: .background), execute: {
completion?(generalBlockModels, nil)
})
}
}
}
}
This function is located in the PeopleSearchManager and calls the cloud function
func searchPeopleFirstRequest(success: ((_ searchLocationModels: [UserSearchLocationModel]) -> Void)?, fail: ((_ error: Error) -> Void)?) {
DispatchQueue.main.async {
guard let location = LocationManager.shared.currentLocation else { return }
let realmUserSettingsManager = RealmUserSettingsManager()
let miles = realmUserSettingsManager.getMaxDistanceInMiles() ?? 15
let parameters = ["miles" : miles, "latitude" : location.coordinate.latitude, "longitude" : location.coordinate.longitude] as [String : Any]
DispatchQueue.global(qos: .background).async {
Auth.auth().currentUser?.getIDTokenForcingRefresh(true, completion: { (token, error) in
if let error = error {
// Handle error
fail?(error)
return
}
guard let _token = token else { return }
let headers = ["X-Auth-MyApp-Token" : _token]
guard let url = URL(string: BackendEndPoint.searchPeopleFirstRequest.path) else { return }
AlamofireMyAppManager.shared.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers).responseJSON(completionHandler: { (response) in
switch response.result {
case .success(let value):
guard let dict = value as? [String : Any] else {
success?([])
return
}
guard let result = dict["result"] as? [[String : Any]] else {
success?([])
return
}
let userSearchLocationModels = Mapper<UserSearchLocationModel>().mapArray(JSONArray: result)
success?(userSearchLocationModels)
// for second request
if let lastUserSearchLocationModelID = dict["lastUserSearchLocationModelID"] as? String {
debugPrint("lastUserSearchLocationModelID", lastUserSearchLocationModelID)
self.peopleSecondRequestModel = PeopleSecondRequestModel(lastUserSearchLocationModelID: lastUserSearchLocationModelID, location: location, radius: miles, offset: userSearchLocationModels.count)
}
case .failure(let error):
debugPrint(error)
fail?(error)
}
})
})
}
}
}
Cloud function
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const GeoFire = require('geofire');
const moment = require('moment');
const map = require('lodash').map;
const filter = require('lodash').filter;
const sortBy = require('lodash').sortBy;
const last = require('lodash').last;
module.exports = functions.https.onRequest((req, res) => {
const token = req.header('X-Auth-MyApp-Token');
const miles = req.query['miles'];
const latitude = req.query['latitude'];
const longitude = req.query['longitude'];
let currentUserID;
let outputUsers;
const startTime = moment();
const timers = {};
admin.auth().verifyIdToken(token)
.then((decodedToken) => {
timers.afterVerify = moment() - startTime;
currentUserID = decodedToken.uid;
console.log('userID', currentUserID);
const getUsersSearchModelsByLocationPromise = new Promise((resolve, reject) => {
try {
const userLocationRef = admin.database().ref().child('userLocations');
const geoFire = new GeoFire(userLocationRef);
const geoQuery = geoFire.query({
center: [Number(latitude), Number(longitude)],
radius: Number(miles) * 1.609
});
const usersIDs = [];
const keyListener = geoQuery.on('key_entered', (key) => usersIDs.push(key));
geoQuery.on('ready', () => {
keyListener.cancel();
geoQuery.cancel();
timers.gotIdsFromGeofire = moment() - startTime;
resolve(usersIDs)
});
} catch (error) {
reject(error)
}
})
.then((userIDs) => {
return Promise.all(map(userIDs, (id) => admin.database().ref().child('userSearchLocationModel').child(id)
.once('value')
.then((snapshot) => snapshot.val())
))
.then((users) => {
timers.gotUserSearchLocationModels = moment() - startTime;
return Promise.resolve(users);
})
});
const getBlockedUsersIDsPromise = Promise.all([
admin.database().ref().child('blockingInfo').child(currentUserID).child('blockedUsers')
.once('value')
.then((snapshot) => snapshot.val())
.then((users) => {
timers.gotBlockedUsersIDs = moment() - startTime;
return map(users, 'userID')
}),
admin.database().ref().child('blockingInfo').child(currentUserID).child('blockedByUsers')
.once('value')
.then((snapshot) => snapshot.val())
.then((users) => {
timers.gotBlockedByUsersIDs = moment() - startTime;
return map(users, 'userID')
}),
]);
const getFilterSettingsPromise = admin.database().ref().child('userPeopleFilterSettings').child(currentUserID)
.once('value')
.then((snapshot) => snapshot.val());
return Promise.all([
getUsersSearchModelsByLocationPromise,
getBlockedUsersIDsPromise,
getFilterSettingsPromise
]);
})
.then(([users, blockedUsersIds, filterSettings]) => {
timers.gotAllInitData = moment() - startTime;
outputUsers = users;
// filter nulls
outputUsers = filter(outputUsers, (user) => user);
// filter blocked
outputUsers = filter(outputUsers, (user) => blockedUsersIds.indexOf(user.userID) === -1);
// filter by gender
outputUsers = filterSettings.filterGenderModeIndex !== 2 ?
filter(outputUsers, (user) => user.genderIndex === filterSettings.filterGenderModeIndex) :
outputUsers;
// filter by age
outputUsers = filter(outputUsers, (user) => {
const userAge = moment().diff(user.birthdayTimeStamp * 1000, 'years');
if (filterSettings.maxAgeValue === 55 && userAge >= 55) return true;
return (userAge >= filterSettings.minAgeValue && userAge <= filterSettings.maxAgeValue);
});
// sort by recent activity timestamp
outputUsers = sortBy(outputUsers, 'recentActivityTimeStamp');
// slice
outputUsers = outputUsers.slice(0, 99);
timers.filtersAndSortsDone = moment() - startTime;
return res.status(200).send({
timers: timers,
result: outputUsers,
lastUserSearchLocationModelID: last(outputUsers)
});
})
.catch((error) => {
console.log('Error: ', error);
return res.status(500).send({
code: error.code,
message: error.message,
stack: error.stack
});
});
});
Also I want to say that I read this post, but as I wrote at the beginning, that even the heated function works like this. I also asked my question here. What is the reason for such a big difference in the result? How can this be remedied or what are the solutions to this problem?

Resources