I store a value called month hours in my application that keeps track of the hours a person has used the apps and displays it in a line of text. The text if part of a stack in Swift UI, but I can't figure out how to make the text update once the information has been queried from I've tried quite a few ways of making this work from structs to classes to using #State.
This is just the latest thing I tried that didn't work if anyone can help that would be greatly appreciated.
let db = Firestore.firestore()
class Month {
var monthHours = "0"
func getMonthHours() {
db.addSnapshotListener(. //Im removing the actual query part to keep that private but the print statement below confirms the query is not the issue.
{ (docSnapShot, err) in
if let e = err {
print("There was an error retrieving the monthly hours:\n\(e.localizedDescription)")
} else {
let data = docSnapShot?.data()
if let h = data?[K.FStore.monthHoursField] as? Double {
self.monthHours = String(h.rounded())
print("These are the hours:\n\(self.monthHours)")
}
}
})
}
func getMonth() -> String {
let date = Date()
let formatter = DateFormatter()
formatter.dateFormat = "MMMM yyyy"
let result = formatter.string(from: date)
return result
}
init() {
getMonthHours()
}
}
struct ChartView : View {
#State private var month = Month()
//Struct variables
var body : some View {
ZStack {
Color(UIColor(named: K.BrandColors.grey)!).edgesIgnoringSafeArea(.all)
VStack {
Text("HOURS THIS MONTH \(month.monthHours)")
.font(.system(size: 18))
.fontWeight(.heavy)
}
}
}
This outlines one possible approach. The crux is to deal with the asynchronous function "getMonthHours". You need to wait till it is finished its fetching before you can use the results.
class Month {
var monthHours = "0"
// async fetch the month hours from Firestore, ... error handling todo
static func getMonthHours(handler: #escaping (String) -> Void) {
db.addSnapshotListener{ (docSnapShot, err) in
if let e = err {
print("There was an error retrieving the monthly hours:\n\(e.localizedDescription)")
return handler("") // should return some error here .... todo
} else {
if let data = docSnapShot?.data(),
let h = data?[K.FStore.monthHoursField] as? Double {
// return the result
return handler(String(h.rounded()))
} else {
return handler("") // should return some error here .... todo
}
}
}
}
func getMonth() -> String {
let date = Date()
let formatter = DateFormatter()
formatter.dateFormat = "MMMM yyyy"
let result = formatter.string(from: date)
return result
}
init() { }
}
struct ChartView : View {
#State private var monthHours = ""
var body : some View {
ZStack {
Color(UIColor(named: K.BrandColors.grey)!).edgesIgnoringSafeArea(.all)
VStack {
Text("HOURS THIS MONTH \(monthHours)")
.font(.system(size: 18))
.fontWeight(.heavy)
}
}.onAppear(perform: loadData)
}
}
func loadData() {
// when the fetching is done it will update the view
Month.getMonthHours() { hours in
self.monthHours = hours
}
}
Related
I want to record long term, how many times a specific ItemView has been displayed in my TabView below. Each time a user swipes on the tab, I want to update var timesViewed by 1. However, timesViewed doesn't seem to update and I am really stuck as to why now.
I removed some view modifiers to simplify the code below.
struct Item: Identifiable, Hashable, Codable {
var id = UUID()
var title: String
var detail: String
var repeatOn: String
var timesViewed = 0
mutating func viewedThisItem() {
timesViewed += 1
}
}
struct ItemSessionView: View {
var itemViewModel: ItemListVM
#State var count = 0
#State var currentIndex = 0
var body: some View {
let today = getTodaysDate().uppercased()
var tempList = itemViewModel.list.filter({ return $0.repeatOn == today})
ZStack {
GeometryReader { proxy in
TabView(selection: $currentIndex) {
ForEach(tempList) { item in
Group {
if today == item.repeatOn {
ItemDetailView(item: item)
}
}
}
}
.onChange(of: currentIndex) { value in
tempList[currentIndex].viewedThisItem()
}
}
}
}
func getTodaysDate() -> String {
let today = Date.now
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
formatter.dateFormat = "E"
let todaysDate = formatter.string(from: today)
return todaysDate
}
}
Structs are value type, you modify the (copied) item in the filtered array but not the original item in the itemViewModel object.
A possible solution is to get the item in the itemViewModel object by id and modify that directly.
.onChange(of: currentIndex) { value in
let id = tempList[value].id
let index = itemViewModel.list.firstIndex{$0.id == id}!
itemViewModel.list[index].viewedThisItem()
}
Force unwrapping is safe because the item does exist.
i maked core data and i fetch all data to List.
all working (add ,delete)
but! if the app inactive (back to background) and i open again to delete a row it crashes with error:
"Thread 1: "An NSManagedObjectContext cannot delete objects in other contexts."
Video problem: https://streamable.com/olqm7y
struct HistoryView: View {
#State private var history: [HistoryList] = [HistoryList]()
let coreDM: CoreDataManager
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "MM-dd-yyyy HH:mm"
return formatter
}
private func populateHistory(){
history = coreDM.getAllHistory()
}
var body: some View {
NavigationView{
VStack {
if !history.isEmpty {
List {
ForEach(history, id: \.self) { historyList in
HStack {
Text(dateFormatter.string(from: historyList.dateFlash ?? Date(timeIntervalSinceReferenceDate: 0)))
Text("\(historyList.timerFlash)s")
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity, alignment: .trailing)
}
}.onDelete(perform: { indexset in
indexset.forEach { index in
let history = history[index]
coreDM.deleteHistory(history: history)
populateHistory()
}
})
}.refreshable {
populateHistory()
print("## Refresh History List")
}
} else {
Text("History Flashlight is Empty")
}
}
.onAppear {
populateHistory()
print("OnAppear")
}
}.navigationTitle("History Flashlight")
.navigationBarTitleDisplayMode(.inline)
}
}
struct HistoryView_Previews: PreviewProvider {
static var previews: some View {
HistoryView(coreDM: CoreDataManager())
}
}
CoreDataManager:
import CoreData
class CoreDataManager {
let persistentContainer: NSPersistentContainer
init(){
persistentContainer = NSPersistentContainer(name: "DataModel")
persistentContainer.loadPersistentStores { (description , error) in
if let error = error {
fatalError("Core Data Store failed \(error.localizedDescription)")
}
}
}
func saveHistory(timeFlash: Int, dateFlash: Date) {
let history = HistoryList(context: persistentContainer.viewContext)
history.timerFlash = Int16(timeFlash)
history.dateFlash = dateFlash
do {
try persistentContainer.viewContext.save()
} catch {
print("failed to save \(error)")
}
}
func getAllHistory() -> [HistoryList] {
let fetchRequest: NSFetchRequest<HistoryList> = HistoryList.fetchRequest()
do {
return try persistentContainer.viewContext.fetch(fetchRequest)
} catch {
return []
}
}
func deleteHistory(history: HistoryList) {
persistentContainer.viewContext.delete(history)
do {
try persistentContainer.viewContext.save()
} catch {
persistentContainer.viewContext.rollback()
print("Failed to save context \(error)")
}
}
}
public extension NSManagedObject {
convenience init(context: NSManagedObjectContext) {
let name = String(describing: type(of: self))
let entity = NSEntityDescription.entity(forEntityName: name, in: context)!
self.init(entity: entity, insertInto: context)
}
}
why?
I looked at the stackoverflow site but didn't find a solution
I'm trying to update data in my viewModel here is my viewModel;
import SwiftUI
import CoreLocation
final class LocationViewViewModel: ObservableObject {
static let previewWeather: Response = load("Weather.json")
let weatherManager = WeatherManager()
let locationManager = LocationManager.shared
#Published var weather: Response
init(weather: Response) { // Remove async
DispatchQueue.main.async { // Here, you enter in an async environment
let data = await fetchData() // Read the data and pass it to a constant
DispatchQueue.main.async { // Get on the main thread
self.weather = data // Here, change the state of you app
}
}
}
func fetchData() async -> Response {
guard let weather = try? await weatherManager.getWeather(latitude: weatherManager.latitude!, longitude: weatherManager.latitude!) else { fatalError("Network Error.") }
return weather
}
var city: String {
return locationManager.getCityName()
}
var date: String {
return dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(weather.current.dt)))
}
var weatherIcon: String {
if weather.current.weather.count > 0 {
return weather.current.weather[0].icon
}
return "sun.max"
}
var temperature: String {
return getTempFor(temp: weather.current.temp)
}
var condition: String {
if weather.current.weather.count > 0 {
return weather.current.weather[0].main
}
return ""
}
var windSpeed: String {
return String(format: "%0.1f", weather.current.wind_speed)
}
var humidity: String {
return String(format: "%d%%", weather.current.humidity)
}
var rainChances: String {
return String(format: "%0.0f%%", weather.current.dew_point)
}
var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}()
var dayFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "EEE"
return formatter
}()
var timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "hh a"
return formatter
}()
func getTimeFor(time: Int) -> String {
return timeFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(time)))
}
func getTempFor(temp: Double) -> String {
return String(format: "%0.1f", temp)
}
func getDayFor(day: Int) -> String {
return dayFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(day)))
}
}
Also i fetched that data for my previous view in my weather manager so im using the same function in my viewModel.
My weatherManager;
final class WeatherManager {
var longitude = LocationManager.shared.location?.coordinate.longitude
var latitude = LocationManager.shared.location?.coordinate.latitude
var units: String = "metric"
func getWeather(latitude: CLLocationDegrees, longitude: CLLocationDegrees) async throws -> Response {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/onecall?lat=\(latitude)&lon=\(longitude)&units=\(units)&exclude=hourly,minutely&appid=\(API.API_KEY)") else { fatalError("Invalid Url.")}
let urlRequest = URLRequest(url: url)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { fatalError("Error while fetching data") }
let decodedData = try JSONDecoder().decode(Response.self, from: data)
return decodedData
}
}
But I stuck with compile errors about initializing my weather Also tried to make my weather model optional but in the end i get the fatal error which says Fatal error: Unexpectedly found nil while unwrapping an Optional value
What is the correct way of doing this if you are using fetched data in many views & viewModels
Your init() is trying to run asynchronously and it's updating a #Published property. Even if you manage to avoid compile errors, you cannot update a property that will change the state of your views (#Published) unless you are on the main thread.
What I propose:
#Published var weather = Response() // Initialise this property in some way, the dummy values will be used by the app until you complete fetching the data
init(weather: Response) { // Remove async
Task { // Here, you enter in an async environment
let data = await fetchData() // Read the data and pass it to a constant
DispatchQueue.main.async { // Get on the main thread
self.weather = data // Here, change the state of you app
}
}
}
I hope this works, but it would be better if after "But I stuck with compile errors..." you showed what kind of errors you find. I tried to use my best guess with the solution above.
We don't use view model objects in SwiftUI. Your object is doing unnecessary things that SwiftUI does for us automatically like formatting strings (so labels auto update automatically when region settings change) and managing asynchronous tasks (tasks are started when view appears and when ever data changes and also cancelled if data changes before previous request ends or the view disappears). Try re-architecting it to use SwiftUI data Views correctly, e.g.
struct WeatherView: View {
let location: Location
#State var weather: Weather?
var body: some View {
Form {
Text(weather.date, format: .dateTime) // new simpler formatting
Text(weather.date, formatter: dateFormatter) // label is auto updated when locale changes
Text(weather?.date == nil ? "No date" : "\(weather.date!, format: .dateTime)") // optional handling
}
.task(id: location) { newLocation // tasks auto cancelled and restarted when location changes
weather = await WeatherManager.shared.getWeather(location: newLocation)
}
}
How can I check if item 1 date is equal to item 2 date inside the foreach. Can I check the same dates inside the predicate like below without using 2 foreach loops?
struct TimedView: View {
static let taskDateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()
#State var releaseDate = Date()
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest var reminder: FetchedResults<CDReminder>
init(){
var calendar = Calendar.current
calendar.timeZone = NSTimeZone.local
let dateFrom = calendar.startOfDay(for: Date()) // eg. 2016-10-10 00:00:00
let dateTo = calendar.date(byAdding: .day, value: 1, to: dateFrom)
let predicate = NSPredicate(format : "date >= %# AND date == %#", dateFrom as CVarArg)
self._reminder = FetchRequest(
entity: CDReminder.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \CDReminder.date, ascending: false)],
predicate: predicate)
}
var body: some View {
NavigationView {
VStack{
HStack{
Text("Zamanlanmis")
.font(.system(size: 40, weight: .bold, design: .rounded))
.foregroundColor(.red)
Spacer()
}
.padding(.leading)
List{
ForEach(reminder, id: \.self) { item in
DatedReminderCell(reminder: item, isSelected: false, onComplete: {})
.foregroundColor(.black)
}
}
}
}
}
Now my output is coming like this
Date objects are only equal if they represent the exact same instant, down to the fraction of a second.
If you want to see if a date falls on a give month/day/year, you will need to use a Calendar object to generate beginning_of_day and end_of_day dates and see if each date fall between them.
This thread includes several examples of that. Most of the code is in Objective-C but it should be pretty easy to translate the approach to Swift, if not the exact code.
To solve this complex problem I created a helper file called ScheduledViewHelper. Inside this helper, I have created a new array that has two elements(reminder and date). Inside this helper first I fetched all the reminders from core data then I compared all their date data and when I find a new date I append the date element inside the array. Finally, I sorted those dates and return [ReminderList] to be able to get this array from my ScheduledView.
My ScheduledViewHelper is
public struct ReminderList: Hashable {
let date: String
let reminder: [CDReminder]
}
open class ScheduledViewHelper: NSObject {
public static let shared = ScheduledViewHelper()
private let viewContext = PersistenceController.shared.container.viewContext
private override init() {
super.init()
}
private func getReminders() -> [CDReminder] {
let request: NSFetchRequest<CDReminder> = CDReminder.fetchRequest()
do {
let items = try viewContext.fetch(request) as [CDReminder]
return items
} catch {
print("Error occured: ", error)
}
return []
}
public func getScheduledData() -> [ReminderList] {
let calendar = Calendar.current
var list: [ReminderList] = []
var dateList: [String] = []
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy"
let reminders = getReminders()
reminders.forEach { (reminder) in
let date = dateFormatter.string(from: reminder.date!)
if !dateList.contains(date) && date >= dateFormatter.string(from: Date()) {
dateList.append(date)
}
}
dateList.forEach { (date) in
let dateFrom = calendar.startOfDay(for: dateFormatter.date(from: date)!) // eg. 2016-10-10 00:00:00
let dateTo = calendar.date(byAdding: .day, value: 1, to: dateFrom)
let predicate = NSPredicate(format: "date <= %# AND date >= %#", dateTo! as NSDate, dateFrom as NSDate)
let request: NSFetchRequest<CDReminder> = CDReminder.fetchRequest()
request.predicate = predicate
do {
let items = try viewContext.fetch(request) as [CDReminder]
let newItem = ReminderList(date: date, reminder: items)
list.append(newItem)
} catch {
print("Error occured: ", error)
}
}
return list.sorted {
$0.date < $1.date
}
}
private func saveContext() {
do {
try viewContext.save()
} catch {
print("Error occured: ", error)
}
}
}
I added to my ScheduledView those lines:
created a new var before init() statement
#State var remindersList : [ReminderList] = []
just under NavigationView I initialized remindersList with onAppear
.onAppear {
remindersList = ScheduledViewHelper.shared.getScheduledData()
}
Reorganized my List like this
List{
ForEach(remindersList, id: .self) { item in
VStack(alignment: .leading) {
Text(item.date)
ForEach(item.reminder, id: .self) { reminder in
DatedReminderCell(reminder: reminder, isSelected: false, onComplete: {})
}
}
.foregroundColor(.black)
}
}
I have a test project where I get the total number of falls for a user for each day over the course of the week. The initialResultsHandler works perfectly every time, however the statisticsUpdateHandler doesn't always fire off. If you start the app, then go to the health app and insert falls manually, switch back to the test app you should see the total for today update. In reality this works for about the first 3-6 times. After that the statisticsUpdateHandler doesn't get called anymore.
What's also odd is that if you delete data and then go back to the test app, or add data from a time earlier than now, the statisticsUpdateHandler gets called. This leads me to think that it has something to do with the statisticsUpdateHandler end date.
Apples documentation is pretty clear however I’m afraid they might be leaving something out.
If this property is set to nil, the statistics collection query will automatically stop as soon as it has finished calculating the initial results. If this property is not nil, the query behaves similarly to the observer query. It continues to run, monitoring the HealthKit store. If any new, matching samples are saved to the store—or if any of the existing matching samples are deleted from the store—the query executes the update handler on a background queue.
Is there any reason that statisticsUpdateHandler might not be called? I have included a test project below.
struct Falls: Identifiable{
let id = UUID()
let date: Date
let value: Int
var formattedDate: String{
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate("MM/dd/yyyy")
return formatter.string(from: date)
}
}
struct ContentView: View {
#StateObject var manager = HealthKitManager()
var body: some View {
NavigationView{
List{
Text("Updates: \(manager.updates)")
ForEach(manager.falls){ falls in
HStack{
Text(falls.value.description)
Text(falls.formattedDate)
}
}
}
.overlay(
ProgressView()
.scaleEffect(1.5)
.opacity(manager.isLoading ? 1 : 0)
)
.navigationTitle("Falls")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class HealthKitManager: ObservableObject{
let healthStore = HKHealthStore()
let fallType = HKQuantityType.quantityType(forIdentifier: .numberOfTimesFallen)!
#Published var isLoading = false
#Published var falls = [Falls]()
#Published var updates = 0
init() {
let healthKitTypesToRead: Set<HKSampleType> = [fallType]
healthStore.requestAuthorization(toShare: nil, read: healthKitTypesToRead) { (success, error) in
if let error = error{
print("Error: \(error)")
} else if success{
self.startQuery()
}
}
}
func startQuery(){
let now = Date()
let cal = Calendar.current
let sevenDaysAgo = cal.date(byAdding: .day, value: -7, to: now)!
let startDate = cal.startOfDay(for: sevenDaysAgo)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: now, options: [.strictStartDate, .strictEndDate])
var interval = DateComponents()
interval.day = 1
// start from midnight
let anchorDate = cal.startOfDay(for: now)
let query = HKStatisticsCollectionQuery(
quantityType: fallType,
quantitySamplePredicate: predicate,
options: .cumulativeSum,
anchorDate: anchorDate,
intervalComponents: interval
)
query.initialResultsHandler = { query, collection, error in
guard let collection = collection else {
print("No collection")
DispatchQueue.main.async{
self.isLoading = false
}
return
}
collection.enumerateStatistics(from: startDate, to: Date()){ (result, stop) in
guard let sumQuantity = result.sumQuantity() else {
return
}
let totalFallsForADay = Int(sumQuantity.doubleValue(for: .count()))
let falls = Falls(date: result.startDate, value: totalFallsForADay)
print(falls.value, falls.formattedDate)
DispatchQueue.main.async{
self.falls.insert(falls, at: 0)
}
}
print("initialResultsHandler done")
DispatchQueue.main.async{
self.isLoading = false
}
}
query.statisticsUpdateHandler = { query, statistics, collection, error in
print("In statisticsUpdateHandler...")
guard let collection = collection else {
print("No collection")
DispatchQueue.main.async{
self.isLoading = false
}
return
}
DispatchQueue.main.async{
self.isLoading = true
self.updates += 1
self.falls.removeAll(keepingCapacity: true)
}
collection.enumerateStatistics(from: startDate, to: Date()){ (result, stop) in
guard let sumQuantity = result.sumQuantity() else {
return
}
let totalFallsForADay = Int(sumQuantity.doubleValue(for: .count()))
let falls = Falls(date: result.startDate, value: totalFallsForADay)
print(falls.value, falls.formattedDate)
print("\n\n")
DispatchQueue.main.async{
self.falls.insert(falls, at: 0)
}
}
print("statisticsUpdateHandler done")
DispatchQueue.main.async{
self.isLoading = false
}
}
isLoading = true
healthStore.execute(query)
}
}
I was so focused on the statisticsUpdateHandler and the start and end time that I didn't pay attention to the query itself. It turns out that the predicate was the issue. By giving it an end date, it was never looking for samples outside the the initial predicate end date.
Changing the predicate to this solved the issue:
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: nil, options: [.strictStartDate])