Mxyb
Mxyb

Reputation: 415

Why do views using @ObservedObject disappear when reinitialised?

In this SwiftUI project, I had a profile tab that displayed the username of the user. When I tapped on this tab again (whilst already in the profile view), the name of the user disappeared.

I fixed this issue by changing this line of code:

@ObservedObject var ProfileModel = ProfileViewModel()

To this line of code:

@StateObject var ProfileModel = ProfileViewModel()

I found that changing @ObservedObject to @StateObject fixed the bug.

Therefore I wanted to apply the same fix to my home view (which was experiencing the same bug as the profile view), where I am displaying the User's step data using the HealthKit framework.

However this caused problems in the init().

Changing:

@ObservedObject var healthStore = HealthStore()

To:

@StateObject var healthStore = HealthStore()

Caused this error in my init(): Cannot assign to property: 'healthStore' is a get-only property.

Here is my init():

init() {
        healthStore = HealthStore()
    }

I need the healthStore to use @ObservedObject in order to listen for changes in the healthStore and update the view accordingly.

Here is HealthStore:

import Foundation
import HealthKit
import SwiftUI
import Combine


class HealthStore: ObservableObject {
    
    @Published var healthStore: HKHealthStore?
    @Published var query: HKStatisticsCollectionQuery?
    
    @Published var steps = [Step]()
    
    init() {
        if HKHealthStore.isHealthDataAvailable() {
            healthStore = HKHealthStore()
            
        }
    }
        
    func calculateSteps(completion: @escaping (HKStatisticsCollection?) -> Void) {
       let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!
       let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())
       let anchorDate = Date.mondayAt12AM()
       let daily = DateComponents(day: 1)
       let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate)
       let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates:
                                     [.init(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered), predicate]
                                 )
       DispatchQueue.main.async {
          self.query = HKStatisticsCollectionQuery(
             quantityType: stepType,
             quantitySamplePredicate: compoundPredicate,
             options: .cumulativeSum,
             anchorDate: anchorDate,
             intervalComponents: daily)
          self.query!.initialResultsHandler = { [weak self] query, statisticsCollection, error in
             guard let self = self else { return }
             DispatchQueue.main.async {
                completion(statisticsCollection)
             }
          }
          self.query!.statisticsUpdateHandler = { [weak self] query, statistics, collection, error in
             guard let self = self else { return }
             DispatchQueue.main.async {
                if let stepCount = statistics?.sumQuantity()?.doubleValue(for: HKUnit.count()) {
                   self.steps.append(Step(count: Int(stepCount), date: statistics!.startDate))
                }
             }
          }
          if let healthStore = self.healthStore, let query = self.query {
             healthStore.execute(query)
          }
       }
    }
    
    func requestAuthorization(completion: @escaping (Bool) -> Void) {
        
        let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!
        
        guard let healthStore = self.healthStore else { return completion(false) }
        
        healthStore.requestAuthorization(toShare: [], read: [stepType]) { (success, error) in
            completion(success)
        }
    }
}

How can I fix the issue of the view that contains step data from healthStore disappearing when the Home view is reinitialised (tapping the home tab when already in the home tab)?

Upvotes: 1

Views: 224

Answers (1)

Paulw11
Paulw11

Reputation: 114846

SwiftUI views are structs and therefore immutable. What actually happens when a SwiftUI view "updates", is that a new instance of the view is created and shown.

This means that when you declare your view model via @ObservableObject, you get a new instance of ProfileViewModel along with your new ProfileView.

@StateObject tells SwiftUI that you want a single instance of this object to be kept when this view is updated, so SwiftUI takes care of this for you. An implication of this is that you cannot assign new values to the property; It is "get only".

In your HomeView you need to remove the healthStore = HealthStore() from your init - You have already provided an instance of HealthStore and assigned it to healthStore when you declared @StateObject var healthStore = HealthStore()

@StateObject properties still refer to ObservableObjects, so your HealthStore doesn't need to change.

Upvotes: 3

Related Questions