Completion handler for Firebase Realtime DB lookup when one function needs the value of a previous function - firebase

I have the following code that fetches a schedule
func fetchSchedule(completion: #escaping () -> ()) {
scheduleRef.queryOrderedByValue().queryEqual(toValue: true).observe(.value, with: { snapshot in
self.schedule = []
if snapshot.value is NSNull {
// Null
} else {
for child in snapshot.children {
if let snapshot = child as? DataSnapshot,
let schedule = Schedule(snapshot: snapshot) {
self.schedule.append(schedule)
}
}
}
})
}
The above get the current schedule but what I am unclear on is that i need that value to then call the next function call which get the associated games for that schedule on the .onAppear() of the view in SwiftUI
func getGames() {
scheduleStore.fetchSchedule()
//
gameStore.fetchGames(weekId: self.scheduleStore.schedule[0].weekId)
}
the gameStore.fetchGames always returns null, likely because it has not finished processing the fetchSchedule function?
How do I ensure the first function finishes before it calls the fetchGames?

You have a completion handler built into your function signature on fetchSchedule, but you aren't using it.
func fetchSchedule(completion: #escaping () -> ()) {
scheduleRef.queryOrderedByValue().queryEqual(toValue: true).observe(.value, with: { snapshot in
self.schedule = []
if snapshot.value is NSNull {
// Null
} else {
for child in snapshot.children {
if let snapshot = child as? DataSnapshot,
let schedule = Schedule(snapshot: snapshot) {
self.schedule.append(schedule)
}
}
completion() //<-- Here
}
})
}
Then,
func getGames() {
scheduleStore.fetchSchedule(completion: {
gameStore.fetchGames(weekId: self.scheduleStore.schedule[0].weekId)
})
}
You're not showing all of your code, but you may also have something broken between self.schedule, which you set in fetchSchedule, and self.scheduleStore, you you send to fetchGames -- make sure you've only got one place you're storing data -- should it be self.schedule in both places?
Update, based on comments
This code is approximate, since I don't have access to your types, but it should get you started:
func fetchSchedule(completion: #escaping ([Schedule]) -> ()) {
scheduleRef.queryOrderedByValue().queryEqual(toValue: true).observe(.value, with: { snapshot in
if snapshot.value is NSNull {
// Null
} else {
let schedules = snapshot.children.compactMap { child in
if let snapshot = child as? DataSnapshot, let schedule = Schedule(snapshot: snapshot) {
return schedule
}
return nil
}
completion(schedules)
}
})
}
func getGames() {
scheduleStore.fetchSchedule { schedules in
gameStore.fetchGames(weekId: schedules[0].weekId)
}
}

Related

watchOS 8 HealtKit background delivery stops working after a few hours in background

I'm trying to implement background delivery of the HealthKit data for an independent watchOS 8 app. I was following Gettings the most out of HealthKit WWDC talk and seems to have added everything that is needed for background delivery to work, including recent iOS 15 and watchOS 8
com.apple.developer.healthkit.background-delivery entitlement. But for some reason, background delivery stops working after approximately 3-5 hours after the app went to the background. For example, I'm receiving updates during the evening from the app, but then over the night updates stops delivering and I'm getting those only if I open the app again in the morning. See the ExtensionDelegate code below
class ExtensionDelegate: NSObject, WKExtensionDelegate {
private let healthStore = HKHealthStore()
private var anchor: HKQueryAnchor?
func applicationDidFinishLaunching() {
print("application did finish launching")
activateHeathKit()
}
func activateHeathKit() {
let types = Set([HKObjectType.categoryType(forIdentifier: .lowHeartRateEvent)!])
healthStore.requestAuthorization(toShare: nil, read: types) { [weak self] success, _ in
guard let `self` = self else {
return
}
guard let lowHeartRateType = HKObjectType.categoryType(forIdentifier: .lowHeartRateEvent) else {
return
}
`self`.healthStore.enableBackgroundDelivery(for: lowHeartRateType, frequency: .immediate) { success, _ in
print("enableBackgroundDelivery: \(success) for lowHeartRateEvent")
}
let query = HKObserverQuery(sampleType: stepsType, predicate: nil) { _, completionHandler, error in
`self`.updateLowHeartRate {
completionHandler()
}
}
`self`.healthStore.execute(query)
}
}
func updateLowHeartRate(completionHandler: #escaping () -> Void) {
guard let lowHeartRateType = HKObjectType.categoryType(forIdentifier: .lowHeartRateEvent) else {return}
let anchoredQuery = HKAnchoredObjectQuery(type: lowHeartRateType, predicate: nil, anchor:
self.anchor, limit: Int(HKObjectQueryNoLimit)) { [unowned self] query, newSamples,
_, newAnchor, error -> Void in
for item in newSamples ?? [] {
let date = item.startDate
let hour = Calendar.current.component(.hour, from: date)
let minute = Calendar.current.component(.minute, from: date)
let message = "Low heart rate from \(hour):\(String(format: "%02d", minute))"
print(message)
}
self.anchor = newAnchor
completionHandler()
}
healthStore.execute(anchoredQuery)
}
}
I don't see an implementation of the handle(_:) method for background tasks but perhaps it is just not shown. Link to the docs here.
Just in case here is how I have my workout app set up to update complications on the watch face.
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
for task in backgroundTasks {
if WKExtension.shared().applicationState == .background {
if let watchComplication = task as? WKApplicationRefreshBackgroundTask {
// do background work here
}
}
task.setTaskCompletedWithSnapshot(false)
}
completePendingTasksIfNeeded()
}

MVVM with Repository/Firestore - Where is the best place to store different queried arrays from a single collection?

I am building a ToDo List App using Firestore based on this google tutorial, with a SwiftUI app using an MVVM/repository pattern, that uses one load query to find all tasks ("actions"), and I'm trying to set it up so I can have multiple date-based queries (e.g. display dates for today, for next week, possibly on the same screen)
The current version I'm working on has one single "loadData" function in the repository that is saved to a single published "actions" variable, and called when this is initialized.
class ActionRepository: ObservableObject, ActionStoreType {
let db = Firestore.firestore()
#Published var actions = [Action]()
init() {
loadData()
}
func loadData() {
let userId = Auth.auth().currentUser?.uid
db.collection("action")
.order(by: "createdTime")
.whereField("userId", isEqualTo: userId!)
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.actions = querySnapshot.documents.compactMap { document in
do {
let x = try document.data(as: Action.self)
return x
}
catch {
print(error)
}
return nil
}
}
}
}
My view model just calls the repository with no parameters.
class ActionListViewModel: ObservableObject {
#Published var actionRepository = ActionRepository()
#Published var actionCellViewModels = [ActionCellViewModel]()
private var cancellables = Set<AnyCancellable>()
init() {
actionRepository.$actions.map { actions in
actions.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.actionCellViewModels, on: self)
.store(in: &cancellables)
}
I want to add a function to load data by date that I can call as many times as I want:
func loadMyDataByDate2(from startDate: Date, to endDate: Date? = nil) {
let userId = Auth.auth().currentUser?.uid
let initialDate = startDate
var finalDate: Date
if endDate == nil {
finalDate = Calendar.current.date(byAdding: .day, value: 1, to: initialDate)!
} else {
finalDate = endDate!
}
db.collection("action")
.order(by: "createdTime")
.whereField("userId", isEqualTo: userId!)
.whereField("startDate", isGreaterThanOrEqualTo: initialDate)
.whereField("startDate", isLessThanOrEqualTo: finalDate)
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.actions = querySnapshot.documents.compactMap { document in
do {
let x = try document.data(as: Action.self)
return x
}
catch {
print(error)
}
return nil
}
}
}
But I don't know the best way to do this. If I want my view model to have three lists of tasks: one for today, one for the rest of the week, and one for next week, what would be the best way to do this?
Should I create separate variables in the repository or the View Model to store these different lists of actions?
Or add date variables for the repository so I call multiple instances of it within the View Model?
I just want to make sure I'm not going down an unwise path with how I start building this out.
I ended up doing something based on Peter's suggestion. I am getting all these filtered lists in my ViewModel. Instead of taking from the repository and storing them all in one ActionCellViewModel property, I created four different ActionCellViewModel properties.
I have four different functions in my initializer code now, and each one takes the list of actions, filters it based on date and completion status, and assigns it to the appropriate CellViewModel property for use in my view.
class ActionListViewModel: ObservableObject {
#Published var actionRepository: ActionStoreType
#Published var baseDateActionCellViewModels = [ActionCellViewModel]()
#Published var baseDateWeekActionCellViewModels = [ActionCellViewModel]()
#Published var previousActionCellViewModels = [ActionCellViewModel]()
#Published var futureActionCellViewModels = [ActionCellViewModel]()
#Published var baseDate: Date = Date()
#Published var hideCompleted: Bool = true
#Published var baseDateIsEndOfWeek: Bool = false
private var cancellables = Set<AnyCancellable>()
// MARK: Initializers
// Default initializer for production code.
init() {
self.actionRepository = ActionRepository()
self.baseDateIsEndOfWeek = isDateEndOfWeek(date: self.baseDate, weekEnd: self.baseDate.endOfWeekDate(weekStart: .sat))
loadPastActions()
loadBaseActions()
loadWeekTasks()
loadFutureActions()
}
// MARK: Functions for initializing the main groups of actions for the Homepage.
func isDateEndOfWeek(date currentDate: Date, weekEnd endOfWeekDate: Date) -> Bool {
if currentDate == endOfWeekDate {
print("Current Date: \(currentDate) and endOfWeekDate: \(endOfWeekDate) are the same!")
return true
} else {
print("The current date of \(currentDate) is not the end of the week (\(endOfWeekDate))")
return false
}
}
///The loadPastActions function takes the published actions list from the repository, and pulls a list of actions from before the base date. (It hides completed actions by default, but this is impacted by the viewModel's "hideCompleted" parameter.
///
///- returns: Assigns a list of actions from prior to the base date to the pastActionCellViewModels published property in the viewModel.
func loadPastActions() {
self.actionRepository.actionsPublisher.map { actions in
actions.filter { action in
action.beforeDate(self.baseDate) && action.showIfIncomplete(onlyIncomplete: self.hideCompleted)
}
.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.previousActionCellViewModels, on: self)
.store(in: &cancellables)
}
///The loadBaseActions function takes the published actions list from the repository, and pulls a list of actions from the base date. (It hides completed actions by default, but this is impacted by the viewModel's "hideCompleted" parameter.
///
///- returns: Assigns a list of actions from the base date to the viewModel's baseDateActionCellViewModels property.
func loadBaseActions() {
self.actionRepository.actionsPublisher.map { actions in
actions.filter { action in
action.inDateRange(from: self.baseDate, to: self.baseDate) && action.showIfIncomplete(onlyIncomplete: self.hideCompleted)
}
.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.baseDateActionCellViewModels, on: self)
.store(in: &cancellables)
}
/// The loadWeekActions takes the published actions list for the current user from the repository, and pulls a list of actions either from remainder of the current week (if not the end of the week), or from next week, if it's the last day of the week.
///
///- returns: Assigns a list of actions from the rest of this week or the next week to the viewModel's baseDateWeekActionCellViewModels property.
func loadWeekTasks() {
let startDate: Date = self.baseDate.tomorrowDate()
print("Start date is \(startDate) and the end of that week is \(startDate.endOfWeekDate(weekStart: .sat))")
self.actionRepository.actionsPublisher.map { actions in
actions.filter { action in
action.inDateRange(from: startDate, to: startDate.endOfWeekDate(weekStart: .sat)) && action.showIfIncomplete(onlyIncomplete: self.hideCompleted)
}
.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.baseDateWeekActionCellViewModels, on: self)
.store(in: &cancellables)
}
/// The loadFutureActions function takes the published actions list for the current user from the repository, and pulls a list of actions from after the week tasks.
///
///- returns: Assigns a list of actions from the future (beyond this week or next, depending on whether the baseDate is the end of the week) to the futureActionCellViewModels property in the viewModel.
func loadFutureActions() {
let startAfter: Date = baseDate.tomorrowDate().endOfWeekDate(weekStart: .sat)
self.actionRepository.actionsPublisher.map { actions in
actions.filter { action in
action.afterDate(startAfter) && action.showIfIncomplete(onlyIncomplete: self.hideCompleted)
}
.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.futureActionCellViewModels, on: self)
.store(in: &cancellables)
}

Why Firebase Task in RxJava Completable emitter doesn't execute?

I'm developing a Firebase Android application which connect to a Firestore. The nomenclature is that the collection is "Assets". The example code had simple actions like addAsset and deleteAsset, those work fine. This is the data repository layer which actually converse with Firebase, the view model layer is above this.
class FirestoreAssetRepository(secondaryDB: FirebaseFirestore) : IAssetRepository {
companion object {
private const val TAG = "FirestoreAssetRepo"
private const val ASSET_COLLECTION = "Assets"
}
private var remoteDB: FirebaseFirestore
private var changeObservable: Observable<List<DocumentSnapshot>>
init {
remoteDB = secondaryDB
}
override fun addAsset(asset: Asset): Completable {
return Completable.create { emitter ->
remoteDB.collection(ASSET_COLLECTION)
.add(mapToAssetData(asset))
.addOnSuccessListener {
if (!emitter.isDisposed) {
emitter.onComplete()
}
}
.addOnFailureListener {
if (!emitter.isDisposed) {
emitter.onError(it)
}
}
}
}
override fun deleteAsset(assetId: String): Completable {
return Completable.create { emitter ->
remoteDB.collection(ASSET_COLLECTION)
.document(assetId)
.delete()
.addOnSuccessListener {
if (!emitter.isDisposed) {
emitter.onComplete()
}
}
.addOnFailureListener {
if (!emitter.isDisposed) {
emitter.onError(it)
}
}
}
}
I'm adding an action to the repository which would modify a specific document.
override fun lockUnlockAsset(assetId: String): Completable {
Log.d(TAG, "lockUnlockAsset")
return Completable.create { emitter ->
remoteDB.collection(ASSET_COLLECTION)
.document(assetId)
.get()
.addOnSuccessListener {
Log.d(TAG, "Unlocking")
val remoteAsset = mapDocumentToRemoteAsset(it)
it.reference.update(getUnlockLocation())
if (!emitter.isDisposed) {
emitter.onComplete()
}
}
.addOnFailureListener {
Log.d(TAG, "Could not find asset to unlock")
if (!emitter.isDisposed) {
emitter.onError(it)
}
}
}
}
The execution reaches Log.d(TAG, "lockUnlockAsset") but never gets to Log.d(TAG, "Unlocking"). If I place a break point at that second logging command it is the usual red dot in the beginning, but when the call comes into the function the icon changes to a grey "don't enter" icon and when I hover over it Android Studio tells me that "No executable found at ...". So something is definitely wrong there.
I'm new to Kotlin and RxJava2. How can I get this to work?
Update: to answer Pavel's question: these functions are called from the ViewModel layer:
fun deleteAsset(assetId: String) {
repository.deleteAsset(assetId)
.subscribeOn(Schedulers.io())
.subscribe(
{},
{
it.printStackTrace()
})
.addTo(disposable)
}
fun addAsset(assetTitle: String) {
repository.addAsset(Asset("${System.currentTimeMillis()}", assetTitle))
.subscribeOn(Schedulers.io())
.subscribe(
{},
{
it.printStackTrace()
})
.addTo(disposable)
}
fun lockUnlockAsset(assetId: String) {
repository.lockUnlockAsset(assetId)
}
I was experimenting with combinations of .subscribeOn(Schedulers.io()).observe at the repository level. Maybe it's the .addTo(disposable) which got it working, I'm not sure what I was missing. Now it's working, I wait for Pavel for his answer.
I experimented with combinations of .subscribeOn(...) and observeOn(..) + .observe(...) at the data repository level, but I should have just followed the pattern in the view model (view model calls the functions of the data repository): it's a chained subscribeOn + subscribe + addTo(disposable):
fun lockUnlockAsset(assetId: String) {
repository.lockUnlockAsset(assetId)
.subscribeOn(Schedulers.io())
.subscribe(
{},
{
it.printStackTrace()
})
.addTo(disposable)
}
Thanks for Pavel for pointing this out.

Firebase 'OR' condition on single field

In my application the main entity is threads, I mean sneakers, jackets, t-shorts and so on.
This is firebase db:
I have logic witch fetch threads by 'threadTypes'. In my app there're 3 types - outwear, footwear and accessory.
That is code:
extension GoodsViewController {
func fetchThreads(completion: #escaping (Swift.Void) -> Swift.Void) {
self.ref
.child("threads")
.observeSingleEvent(of: .value, with: { (snapshot) in
for rest in snapshot.children.allObjects as! [FIRDataSnapshot] {
guard let restDict = rest.value as? [String: Any] else { continue }
let thread = Thread()
thread.setValuesForKeys(restDict)
if Search.searchFilters.stuffTypes.isEmpty {
self.threads.append(thread)
}
else {
if let threadType = thread.threadType {
if Search.searchFilters.stuffTypes.contains(threadType) {
self.threads.append(thread)
}
}
}
}
completion()
})
}
}
self.threads - is variable which is used as table view datasource.
Search.searchFilters.stuffTypes - array which contains types for search.
As you see I fetch all threads and then check if current thread type contains in Search.searchFilters.stuffTypes array.
My question is - is it possible to perform this checking before I fetch all threads?
In C# it should be something like that -
threads.Where(t => Search.searchFilters.stuffTypes.Contains(t.threadType)

Wait for 2 callbacks before instantiating an object

I would like to download from firebase:
data issued from a group profile (Firebase realtime DB)
including...
data issued from the group admin profile (Firebase realtime DB)
a group profile image (Firebase Storage)
Then I can instantiate a group object with its data and its image
First approach, I used 3 nested closures that allowed me to get data, and then to get the image.
It did work, but it was quite long to get sequentially all that stuffs from firebase.
So I tried to use GCD in order to push my 2 latest Firebase queries (user data + group image) at the same time (rather than one after the other), and to wait for the last callback to start instantiating my group.
Is it a correct approach ?
If yes, I find some difficulties to implement it...
My issue : returnedUser and returnedGroupImage are always nil
Here is my bunch of code :
static func getGroup(_ groupID:String, completionBlock: #escaping (_ group: Group?) -> ()) {
dataRef.child("data").child("groups").child(groupID).observe(.value, with: { (snapshot) in
if let snapshotValue = snapshot.value {
guard let name = (snapshotValue as AnyObject).object(forKey: "name") as? String else
{
completionBlock(nil)
return
}
guard let adminID = (snapshotValue as AnyObject).object(forKey: "adminID") as? String else
{
completionBlock(nil)
return
}
let queue = DispatchQueue(label: "asyncQueue", attributes: .concurrent, target: .main)
let dispatch_group = DispatchGroup()
var returnedUser: User?
var returnedGroupImage: UIImage?
queue.async (group: dispatch_group) {
FireBaseHelper.getUser(adminID, completionBlock: { (user) in
if user != nil {
returnedUser = user
}
})
}
queue.async (group: dispatch_group) {
FireBaseHelper.getGroupImage(groupID, completionBlock: { (image) in
if image != nil {
returnedGroupImage = image
}
})
}
dispatch_group.notify(queue: DispatchQueue.main) {
// Single callback that is supposed to be executed after all tasks are complete.
if (returnedUser == nil) || (returnedGroupImage == nil) {
// always true !
return
}
let returnedGroup = Group(knownID: (snapshotValue as AnyObject).key, named: name, createdByUser: currentUser!)
returnedGroup.groupImage = returnedGroupImage
completionBlock(returnedGroup)
}
}
})
}
Thanks for your help !
I believe that the way you are using DispatchGroups are not correct.
let dispatch_group = DispatchGroup()
var returnedUser: User?
var returnedGroupImage: UIImage?
dispatch_group.enter()
FireBaseHelper.getUser(adminID, completionBlock: { (user) in
if user != nil {
returnedUser = user
}
dispatch_group.leave()
})
dispatch_group.enter()
FireBaseHelper.getGroupImage(groupID, completionBlock: { (image) in
if image != nil {
returnedGroupImage = image
}
dispatch_group.leave()
})
dispatch_group.notify(queue: DispatchQueue.main) {
// Single callback that is supposed to be executed after all tasks are complete.
if (returnedUser == nil) || (returnedGroupImage == nil) {
// always true !
return
}
let returnedGroup = Group(knownID: (snapshotValue as AnyObject).key, named: name, createdByUser: currentUser!)
returnedGroup.groupImage = returnedGroupImage
completionBlock(returnedGroup)
}

Resources