Why do views using #ObservedObject disappear when reinitialised? - ios

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

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.

Related

Why is m view not updating using ObservedObject and HealthKit?

In the Home Screen of my app, I have a Capsule() that contains the user's steps. This data is obtained via HealthKit. The data is correct however when it changes in the health app, it should change in my app but this is not happening. How do I get 'steps' variable to listen to HealthKit, there must be an error in my code.
Here is the code for my Home view:
import SwiftUI
import HealthKit
struct Home: View {
#ObservedObject var healthStore = HealthStore()
#State private var steps: [Step] = [Step]()
init() {
healthStore = HealthStore()
}
private func updateUIFromStatistics(_ statisticsCollection: HKStatisticsCollection) {
let startDate = Date()
let endDate = Date()
statisticsCollection.enumerateStatistics(from: startDate, to: endDate) { (statistics, stop) in
let count = statistics.sumQuantity()?.doubleValue(for: .count())
let step = Step(count: Int(count ?? 0), date: statistics.startDate)
steps.append(step)
}
}
var body: some View {
NavigationView {
ScrollView {
ZStack {
Color("BackgroundColour")
.ignoresSafeArea()
VStack {
let totalSteps = steps.reduce(0) { $0 + $1.count }
ForEach($steps, id: \.id) { step in
Button(action: {
// Perform button action here
print("Step Capsule Tapped...")
}) {
HStack {
Image("footsteps")
Text("\(totalSteps)")
}
}
} // ForEach End
} // VStack End
}//ZStack End
.edgesIgnoringSafeArea(.all)
} // ScrollView End
.background(Color("BackgroundColour"))
.onLoad {
healthStore.requestAuthorization { success in
if success {
healthStore.calculateSteps { statisticsCollection in
if let statisticsCollection = statisticsCollection {
// update the UI
updateUIFromStatistics(statisticsCollection)
}
}
}
}
} // .onLoad End
.onAppear(perform: {
let defaults = UserDefaults.standard
let keyString: String? = defaults.string(forKey: "key") ?? ""
print("User's Key:\(keyString ?? "")")
}) // .onAppear End
} // NavigationView End
}
}
Here is the code for the HealthStore:
import Foundation
import HealthKit
import SwiftUI
import Combine
class HealthStore: ObservableObject {
#Published var healthStore: HKHealthStore?
#Published var query: HKStatisticsCollectionQuery?
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]
)
query = HKStatisticsCollectionQuery(
quantityType: stepType,
quantitySamplePredicate: compoundPredicate,
options: .cumulativeSum,
anchorDate: anchorDate,
intervalComponents: daily)
query!.initialResultsHandler = { query, statisticsCollection, error in
completion(statisticsCollection)
}
if let healthStore = 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)
}
}
}
extension Date {
static func mondayAt12AM() -> Date {
return Calendar(identifier: .iso8601).date(from: Calendar(identifier: .iso8601).dateComponents([.yearForWeekOfYear, .weekOfYear], from: Date()))!
}
}
Here is the code for Step:
import Foundation
struct Step: Identifiable {
let id = UUID()
let count: Int
let date: Date
}
Here is the code for the .onLoad method that is used on the Home view:
import SwiftUI
struct ViewDidLoadModifier: ViewModifier {
#State private var didLoad = false
private let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content.onAppear {
if didLoad == false {
didLoad = true
action?()
}
}
}
}
extension View {
func onLoad(perform action: (() -> Void)? = nil) -> some View {
modifier(ViewDidLoadModifier(perform: action))
}
}
Any ideas?
I think I know what's missing here.
In your class HealthStore, inside the method func calculateSteps(completion: #escaping (HKStatisticsCollection?)-> Void), you have set the initialResultsHandler property of your HKStatisticsCollectionQuery. However, you've not set the statisticsUpdateHandler property.
To continue to listen to updates from HealthKit, after initialResultsHandler completes execution, you need to also set statisticsUpdateHandler.
As-per the statisticsUpdateHandler documentation, in-case statisticsUpdateHandler is nil, which is the case above, the HKStatisticsCollectionQuery will:
"automatically stop as soon as it has finished calculating the initial results"
which seems to be in-line with what you are observing.
Full disclosure, I've never worked with HealthKit, so please pardon me if this doesn't help. I've gone through the HealthKit documentation after looking at your code and writing here what jumps out at me.
If it does help you though, kindly consider marking it as the answer.

How do I pass CoreData into UICalendarview

I'm trying to use my CoreData entity 'Item' to show decorations on a calendar (using UIcalendarview in iOS16). I have my view called CalView calling the CalendarHelper struct (where the UICalendarView is coded and setup). It is showing decorations on every day currently, but I want to only show decorations on the days where there is a CoreData entry, and then enable navigation when you click on those days to the detail view. The problem is that I can't figure out how to get the CoreData entity into the CalendarHelper and UICalendarView to use it. It's currently giving me an error stating that I can't pass a FetchedResult in to an expected ObservedResult. But when I change them all to FetchedResult on that function, it gives me an error saying it's expecting a type of Item.
NOTE: I posted this over here as well but I think I messed up that post by deleting the original when I updated it. https://stackoverflow.com/questions/75138925/pass-coredata-into-uicalendarview-to-show-decorations-and-click-on-date-for-deta
CALVIEW:
struct CalView: View {
var body: some View {
ZStack {
VStack {
HStack {
CalendarHelper(interval: DateInterval(start: .distantPast, end: .distantFuture))
}
}
}
}
}
CALENDARHELPER:
struct CalendarHelper: UIViewRepresentable {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: SleepSort.default.descriptors,
animation: .default)
private var items: FetchedResults<Item>
let interval: DateInterval
func makeUIView(context: Context) -> UICalendarView {
let calView = UICalendarView()
calView.delegate = context.coordinator
calView.calendar = Calendar(identifier: .gregorian)
calView.availableDateRange = interval
calView.fontDesign = .rounded
let dateSelection = UICalendarSelectionSingleDate(delegate: context.coordinator)
calView.selectionBehavior = dateSelection
calView.wantsDateDecorations = true
return calView
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self, items: items)
}
func updateUIView(_ uiView: UICalendarView, context: Context) {
}
class Coordinator: NSObject, UICalendarViewDelegate, UICalendarSelectionSingleDateDelegate {
var parent: CalendarHelper
#ObservedObject var items: Item
init(parent: CalendarHelper, items: ObservedObject<Item>) {
self.parent = parent
self._items = items
}
#MainActor
func calendarView(_ calendarView: UICalendarView, decorationFor dateComponents: DateComponents) -> UICalendarView.Decoration? {
let font = UIFont.systemFont(ofSize: 10)
let configuration = UIImage.SymbolConfiguration(font: font)
let image = UIImage(systemName: "star.fill", withConfiguration: configuration)?.withRenderingMode(.alwaysOriginal)
return .image(image)
}
func dateSelection(_ selection: UICalendarSelectionSingleDate,
didSelectDate dateComponents: DateComponents?) {
}
func dateSelection(_ selection: UICalendarSelectionSingleDate,
canSelectDate dateComponents: DateComponents?) -> Bool {
return true
}
}
}
I tested for a while to pass in FetchResults to the coordinator (as SwiftUI #FetchRequest works on a view only), and did not find a solution, but the recommendation to use a standard NSFetchRequest in or from the coordinator. The decorationFor method is called once for each calendar day in the displayed month - this makes the change lagging if that makes 30 NSFetchrequests - so we need to fetch once per month displayed and store the result.
Here is my my calendarView decorationFor method (simplified):
// --------------------------------------------------------------------------
//helper function to compare two dateComponents on day granularity
extension DateComponents {
func sameDate(source: DateComponents) -> Bool {
if let lhs = self.date,
let rhs = source.date,
Calendar.current.startOfDay(for: lhs) == Calendar.current.startOfDay(for: rhs) {
return true
} else {
return false
}
}
}
// -------------------------------------------------------
#MainActor func calendarView(_ calendarView: UICalendarView, decorationFor dateComponents: DateComponents) -> UICalendarView.Decoration? {
//in the coordinator, define:
// private var currentVisibleRange = DateComponents()
// private var result : [YourClass] = []
//check if the requested dateComponents are within the currentVisibleRange
if !currentVisibleRange.sameDate(source: calendarView.visibleDateComponents) {
//here the requested date is not in the currentVisibleRange, so we have to do a new fetch
let myFetch = makeMyFetchRequest(dateComponents) //whatever fetch request you need, based on NSFetchRequest
do {
result = try context.fetch(myFetch)
//result now contains an Array of NSManagedObjects, might need to be sorted, dependent on the underlying data
} catch {
print("Failed to fetch: \(error)")
}
//now we have to store the new range to avoid doing it again for the same range
currentVisibleRange = calendarView.visibleDateComponents
return .customView {
//return any UIView, e.g. a UILabel, or use UIHostingController if you want to return a Swift view
//the view must be a decoration for dateComponents.day
makeMyUILabel(forDay: dateComponents.day)
}
}//calendarView
This works well also on large datasets, one fetch per month displayed is sustainable and shows good performance

UserDefaults implementation always has null in extension

Im trying so save persisting data to my Userdefault storage so I can use it inside my extension.
Question
How do I implement this so I can update my view(update value of toggle) when another target is run, in my case an extension. I created the same app group. For my userdefaults
App is structured like this first my UserDefaults implementation
extension UserDefaults {
static let group = UserDefaults(suiteName: "group.com.carlpalsson.superapp")
func save<T: Codable>(_ object: T, forKey key: String) {
let encoder = JSONEncoder()
if let encodedObject = try? encoder.encode(object) {
UserDefaults.group?.set(encodedObject, forKey: key)
UserDefaults.standard.synchronize()
}
}
func getObject<T: Codable>(forKey key: String) -> T? {
if let object = UserDefaults.group?.object(forKey: key) as? Data {
let decoder = JSONDecoder()
if let decodedObject = try? decoder.decode(T.self, from: object) {
return decodedObject
}
}
return nil
}
}
class UserSettings : ObservableObject {
let test = FamilyActivitySelection()
#Published var discouragedAppsCategoryTokens : Set<ActivityCategoryToken> {
didSet {
UserDefaults.group?.save(discouragedAppsCategoryTokens, forKey:"DiscourageAppsCategoryTokens")
}
}
init() {
self.discouragedAppsCategoryTokens =
(UserDefaults.group?.getObject(forKey: "DiscourageAppsCategoryTokens")) ?? appcategorytokens
}
static var shared: UserSettings {
return _userSettings
}
}
In my extension
class MyDeviceActivityMonitor: DeviceActivityMonitor {
let store = ManagedSettingsStore()
let userSettings = UserSettings.shared
let korven = UserSettings()
override func intervalDidStart(for activity: DeviceActivityName) {
do{
//Im trying to my values here but it´s always null
var family = userSettings.DiscouragedAppsFamilyActivitySelection
var familys: FamilyActivitySelection? = UserDefaults.group?.getObject(forKey: "DiscouragedAppsFamilyActivitySelection")
var iii = korven.DiscouragedAppsFamilyActivitySelection
}
}
Inside my #main
#StateObject var userSettings = UserSettings.shared
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(model)
.environmentObject(store)
.environmentObject(userSettings)
}
}
And in my view
struct ContentView: View {
#EnvironmentObject var userSettings : UserSettings
VStack {
Button("Select Apps to Discourage") {
isDiscouragedPresented = true
}
.familyActivityPicker(isPresented: $isDiscouragedPresented, selection: $userSettings.DiscouragedAppsFamilyActivitySelection)
.onChange(of: userSettings.DiscouragedAppsFamilyActivitySelection) { newSelection in
UserSettings.shared.DiscouragedAppsFamilyActivitySelection = newSelection
MyModel.shared.startDiscourageApps()
// MySchedule.setSchedule()
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(MyModel())
.environmentObject(UserSettings())
}
}
If I understand you correctly you want to update your UI in your extension when the user defaults value changes in your app. This certainly is possible, but requires some more code on your side.
In your UserSettings class you currently read the user defaults in the initializer and then leave it as is. To get your UI to update you also need to update your property in there when the actual user defaults change. To do this you need to observe the user defaults. The easy way would be using the Notification Center with the NSUserDefaultsDidChangeNotification. In your case this won’t work as this notification only is sent for changes made in the same process.
Changes from a different process can be observed using Key-Value-Observing (KVO) though. Unfortunately this seems to be impossible using the nice Swift KeyPath API. Instead you have to do that using the Objective-C version. To do this you would make your UserSettings class inherit NSObject and implement observeValue(forKeyPath:of:change:context:). In there you can read the new data from the user defaults. Then you can add your object as an observer on the user defaults using addObserver(_:forKeyPath:options:context:). Options can stay empty and context can be nil.

View not being updated when ViewModel gets new data

I have an app that will make an API call once a day to retrieve new data.
When the app launches it will check if new data is available, save new data to core data, call core data to show data in a view.
Issue
After saving new data to core data my view will not display the newly save data, but instead display the default dummy data from my model. The new data will be displayed after relaunching the app.
Question
How can I display the newly save data in the view without having to relaunch the app? I'm using MVVM pattern. Below is my code with links to my code.
UVIndexNowModel
class UVIndexNowModel: ObservableObject
{
#Published var uvIndex: Int
#Published var dateTime: Date
init(uvIndex: Int = 0, dateTime: Date = Date())
{
self.uvIndex = uvIndex
self.dateTime = dateTime
}
}
UVIndexNowViewModel
class UVIndexNowViewModel: NSObject, ObservableObject
{
private var isFirstAppearance = true
private let moc = PersistentStore.shared.context
private let nowController: NSFetchedResultsController<UVHour>
#Published var data: UVIndexNowModel = UVIndexNowModel()
#Published var coreDateError: Bool = false
override init()
{
nowController = NSFetchedResultsController(fetchRequest: UVHour.uvIndexNowRequest,
managedObjectContext: moc,
sectionNameKeyPath: nil, cacheName: nil)
super.init()
nowController.delegate = self
do {
try nowController.performFetch()
let results = nowController.fetchedObjects ?? []
setUVIndex(from: results)
} catch {
print("failed to fetch items!")
}
}
func setUVIndex(from hours: [UVHour])
{
let date = Date()
let formatter = DateFormatter()
formatter.timeZone = .current
formatter.dateFormat = "MMM/d/yyyy hh a"
let todaystr = formatter.string(from: date)
print("UVIndexNowVM.setUVIndex() size of UVHour array: \(hours.count)")
for i in hours
{
let tempDateStr = formatter.string(from: i.wrappedDateTime)
if todaystr == tempDateStr
{
print("UV Now VM Date matches! \(tempDateStr)")
self.data.uvIndex = i.wrappedUVIndex
self.data.dateTime = i.wrappedDateTime
break
}
}
}
}
extension UVIndexNowViewModel: NSFetchedResultsControllerDelegate
{
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
{
print("UVIndexNowViewModel controllerDidChangeContent was called. New Stuff in DB")
guard let results = controller.fetchedObjects as? [UVHour] else { return }
setUVIndex(from: results)
}
}
UVIndexNowView
struct UVIndexNowView: View {
#ObservedObject var vm = UVIndexNowViewModel()
var body: some View {
VStack {
Text("\(vm.data.dateTime)")
Text("UV Index: \(vm.data.uvIndex)")
}
}
}
As your UVIndexNowModel is-a ObservableObject you need to observe it in separated view, like
struct UVIndexNowView: View {
#ObservedObject var vm = UVIndexNowViewModel()
var body: some View {
UVIndexNowDataView(data: vm.data) // << here !!
}
}
and
struct UVIndexNowDataView: View {
#ObservedObject var data: UVIndexNowModel
var body: some View {
VStack {
Text("\(data.dateTime)")
Text("UV Index: \(data.uvIndex)")
}
}
}

Escaping closure captures mutating 'self' parameter, Firebase

I have the following code, How can i accomplish this without changing struct into class. Escaping closure captures mutating 'self' parameter,
struct RegisterView:View {
var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
self.names = name //here is the error
}){(error) in
print("Error: \(error)")
}
init(){
LoadPerson()
}a
var body:some View{
//ui code
}
}
Firebasemanager.swift
struct FirebaseManager {
func fetchPerson(
success: #escaping (Person) -> (),
failure: #escaping (String) -> ()
) {
Database.database().reference().child("Person")
.observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
success(Person(dictionary: dictionary))
}
}) { (error) in
failure(error.localizedDescription)
}
}
}
SwiftUI view can be created (recreated) / copied many times during rendering cycle, so View.init is not appropriate place to load some external data. Use instead dedicated view model class and load explicitly only when needed.
Like
class RegisterViewModel: ObservableObject {
#Published var names = [String]()
func loadPerson() {
// probably it also worth checking if person has already loaded
// guard names.isEmpty else { return }
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
struct RegisterView: View {
// in SwiftUI 1.0 it is better to inject view model from outside
// to avoid possible recreation of vm just on parent view refresh
#ObservedObject var vm: RegisterViewModel
// #StateObject var vm = RegisterViewModel() // << only SwiftUI 2.0
var body:some View{
Some_Sub_View()
.onAppear {
self.vm.loadPerson()
}
}
}
Make the names property #State variable.
struct RegisterView: View {
#State var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success: { person in
guard let name = person.name else { return }
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
//...
}

Resources