I am trying to load HealthKit data and display a total distance and last workout date in a SwiftUI view (for a Widget). I am getting the data in the print statement but it's not getting updated in the HTWidgetView below:
class WidgetViewModel: ObservableObject {
#Published var workoutDate: Date = Date()
#Published var totalDistance: Double = 0.0
func getWorkoutDataForWidget() {
WorkoutManager.loadWorkouts { (workouts, error) in
DispatchQueue.main.async {
guard let unwrappedWorkouts = workouts else { return }
if let first = unwrappedWorkouts.first {
self.workoutDate = first.startDate
}
let distancesFromWorkouts = unwrappedWorkouts.compactMap {$0.totalDistance?.doubleValue(for: .foot())}
let total = distancesFromWorkouts.sum()
self.totalDistance = total
print("TOtal Distance = \(total)")
}
}
}
}
extension Sequence where Element: AdditiveArithmetic {
func sum() -> Element { reduce(.zero, +) }
}
struct HTWidgetView: View {
#ObservedObject var viewModel: WidgetViewModel
var body: some View {
VStack {
Text("Last Workout = \(viewModel.workoutDate)")
Text("Total Distance")
Text("\(viewModel.totalDistance)")
}
.onAppear {
viewModel.getWorkoutDataForWidget()
}
}
}
Related
This is Model and View Model. I am using UserDefaults for saving data.
import Foundation
struct Item: Identifiable, Codable {
var id = UUID()
var name: String
var int: Int
var date = Date()
}
class ItemViewModel: ObservableObject {
#Published var ItemList = [Item] ()
init() {
load()
}
func load() {
guard let data = UserDefaults.standard.data(forKey: "ItemList"),
let savedItems = try? JSONDecoder().decode([Item].self, from: data) else { ItemList = []; return }
ItemList = savedItems
}
func save() {
do {
let data = try JSONEncoder().encode(ItemList)
UserDefaults.standard.set(data, forKey: "ItemList")
} catch {
print(error)
}
}
}
and this is the view. I am tryng too add new item and sort them by date. After that adding numbers on totalNumber. I tried .sorted() in ForEach but its not work for sort by date. and I try to create a func for adding numbers and that func is not work thoo.
import SwiftUI
struct ContentView: View {
#State private var name = ""
#State private var int = 0
#AppStorage("TOTAL_NUMBER") var totalNumber = 0
#StateObject var VM = ItemViewModel()
var body: some View {
VStack(spacing: 30) {
VStack(alignment: .leading) {
HStack {
Text("Name:")
TextField("Type Here...", text: $name)
}
HStack {
Text("Number:")
TextField("Type Here...", value: $int, formatter: NumberFormatter())
}
Button {
addItem()
VM.save()
name = ""
int = 0
} label: {
Text ("ADD PERSON")
}
}
.padding()
VStack(alignment: .leading) {
List(VM.ItemList) { Item in
Text(Item.name)
Text("\(Item.int)")
Text("\(Item.date, format: .dateTime.day().month().year())")
}
Text("\(totalNumber)")
.padding()
}
}
}
func addItem() {
VM.ItemList.append(Item(name: name, int: int))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
First of all please name variables always with starting lowercase letter for example
#Published var itemList = [Item] ()
#StateObject var vm = ItemViewModel()
To sort the items by date in the view model replace
itemList = savedItems
with
itemList = savedItems.sorted{ $0.date < $1.date }
To show the sum of all int properties of the today items add a #Published var totalNumber in the view model and a method to calculate the value. Call this method in load and save
class ItemViewModel: ObservableObject {
#Published var itemList = [Item] ()
#Published var totalNumber = 0
init() {
load()
}
func load() {
guard let data = UserDefaults.standard.data(forKey: "ItemList"),
let savedItems = try? JSONDecoder().decode([Item].self, from: data) else { itemList = []; return }
itemList = savedItems.sorted{ $0.date < $1.date }
calculateTotalNumber()
}
func save() {
do {
let data = try JSONEncoder().encode(itemList)
UserDefaults.standard.set(data, forKey: "ItemList")
calculateTotalNumber()
} catch {
print(error)
}
}
func calculateTotalNumber() {
let todayItems = itemList.filter{ Calendar.current.isDateInToday($0.date) }
totalNumber = todayItems.map(\.int).reduce(0, +)
}
}
In the view delete the #AppStorage line because the value is calculated on demand and replace
Text("\(totalNumber)")
with
Text("\(vm.totalNumber)")
I have:
class Exercise: ObservableObject {
#Published var currentSet: Int = 1
func start() { somehow changing currentSet }
}
class ExerciseProgram: ObservableObject {
#Published var currentExercise: Exercise? = nil
func start() { ...self.currentExercise = self.exercises[exerciseIndex + 1]... }
}
struct Neck: View {
#ObservedObject var program: ExerciseProgram = ExerciseProgram(exercises: neckExercises)
var body: some View {
Text(\(self.program.currentExercise!.currentSet))
}
}
The problem is that my View is updated only when the currentExercise of the ExerciseProgram changes, and the currentExercise itself has a currentSet property, and when it changes, my view is not updated. In principle, I understand the logic of why we work exactly as it works: I specified that the view should be updated when currentExercise changes, but I did not say that the view should be updated when the properties of the currentExercise entity change. And so I don't understand how to do it. And I can't change Exercise as struct
You just have to observe the object at the appropriate level.
Each #Published only triggers a refresh if the object as a whole has changed.
In you example the array will change if you replace the array or add/remove objects.
import SwiftUI
struct ExerciseProgramView: View {
//At this level you will see the entire program
#StateObject var program: ExerciseProgram = ExerciseProgram()
var body: some View {
VStack{
if program.currentExercise != nil{
ExerciseView(exercise: program.currentExercise!)
}else{
Text("Ready?")
}
Spacer()
HStack{
if program.currentExercise == nil{
Button("start program", action: {
program.start()
})
}else{
Button("stop", action: {
program.stop()
})
Button("next", action: {
program.next()
})
}
}
}
}
}
struct ExerciseView: View {
//At this level you will see the changes for the exercise
#ObservedObject var exercise: Exercise
var body: some View {
VStack{
Text("\(exercise.name)")
Text("\(exercise.currentSet)")
if exercise.timer == nil{
Button("start exercise", action: {
exercise.start()
})
}else{
Button("stop exercise", action: {
exercise.stop()
})
}
}.onDisappear(perform: {
exercise.stop()
})
}
}
struct ExerciseProgramView_Previews: PreviewProvider {
static var previews: some View {
ExerciseProgramView()
}
}
class Exercise: ObservableObject, Identifiable {
let id: UUID = UUID()
let name: String
#Published var currentSet: Int = 1
var timer : Timer?
init(name: String){
self.name = name
}
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in
self.currentSet += 1
if self.currentSet >= 10{
timer.invalidate()
self.timer = nil
}
})
}
func stop(){
timer?.invalidate()
timer = nil
}
}
class ExerciseProgram: ObservableObject {
#Published var currentExercise: Exercise? = nil
#Published var exercises: [Exercise] = [Exercise(name: "neck"), Exercise(name: "arm"), Exercise(name: "leg"), Exercise(name: "abs")]
#Published var exerciseIndex: Int = 0
func start() {
self.currentExercise = self.exercises[exerciseIndex]
}
func next(){
if exerciseIndex < exercises.count{
self.exerciseIndex += 1
}else{
self.exerciseIndex = 0
}
start()
}
func stop(){
exerciseIndex = 0
currentExercise = nil
}
}
Also, notice how the ObservableObjects have been initialized.
#StateObject is used when the object has to be initialized in a View
#ObservedObject is used to pass an ObservableObject to a child View but the object was created inside a class, specifically class ExerciseProgram.
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
This question is similar to this unanswered question from the Apple Developer Forums, but with a slightly different scenario:
I have a view with a #FetchRequest property of type FetchedResults<Metric>
Within the view, I display the list of those objects
I can tap on one item to select it, storing that selection in a #State var selection: Metric? = nil property.
Here's the properties I defined for my #FocusedValue:
struct FocusedMetricValue: FocusedValueKey {
typealias Value = Metric?
}
extension FocusedValues {
var metricValue: FocusedMetricValue.Value? {
get { self[FocusedMetricValue.self] }
set { self[FocusedMetricValue.self] = newValue }
}
}
Here's how I set the focusedValue from my list view:
.focusedValue(\.metricValue, selection)
And here's how I'm using the #FocusedValue on my Commands struct:
struct MacOSCommands: Commands {
#FocusedValue(\.metricValue) var metric
var body: some Commands {
CommandMenu("Metric") {
Button("Test") {
print(metric??.name ?? "-")
}
.disabled(metric == nil)
}
}
}
The code builds successfully, but when I run the app and select a Metric from the list, the app freezes. If I pause the program execution in Xcode, this is the stack trace I get:
So, how can I make #FocusedValue work in this scenario, with an optional object from a list?
I ran into the same issue. Below is a View extension and ViewModifier that present a version of focusedValue which accepts an Binding to an optional. Not sure why this wasn't included in the framework as it corresponds more naturally to a selection situation in which there can be none...
extension View{
func focusedValue<T>(_ keypath: WritableKeyPath<FocusedValues, Binding<T>?>, selection: Binding<T?>) -> some View{
self.modifier(FocusModifier(keypath: keypath, optionalBinding: selection))
}
}
struct FocusModifier<T>: ViewModifier{
var keypath: WritableKeyPath<FocusedValues, Binding<T>?>
var optionalBinding: Binding<T?>
func body(content: Content) -> some View{
Group{
if optionalBinding.wrappedValue == nil{
content
}
else if let binding = Binding(optionalBinding){
content
.focusedValue(keypath, binding)
}
else{
content
}
}
}
}
In your car usage would look like:
.focusedValue(\.metricValue, selection: $selection)
I have also found that the placement of this statement is finicky. I can only make things work when I place this on the NavigationView itself as opposed to one of its descendants (eg List).
// 1 CoreData optional managedObject in #State var selection
#State var selection: Metric?
// 2 modifiers on View who own the list with the selection
.focusedValue(\.metricValue, $selection)
.focusedValue(\.deleteMetricAction) {
if let metric = selection {
print("Delete \(metric.name ?? "unknown metric")?")
}
}
// 3
extension FocusedValues {
private struct SelectedMetricKey: FocusedValueKey {
typealias Value = Binding<Metric?>
}
private struct DeleteMetricActionKey: FocusedValueKey {
typealias Value = () -> Void
}
var metricValue: Binding<Metric?>? {
get { self[SelectedMetricKey.self] }
set { self[SelectedMetricKey.self] = newValue}
}
var deleteMetricAction: (() -> Void)? {
get { self[DeleteMetricActionKey.self] }
set { self[DeleteMetricActionKey.self] = newValue }
}
}
// 4 Use in macOS Monterey MenuCommands
struct MetricsCommands: Commands {
#FocusedValue(\.metricValue) var selectedMetric
#FocusedValue(\.deleteMetricAction) var deleteMetricAction
var body: some Commands {
CommandMenu("Type") {
Button { deleteMetricAction?() } label: { Text("Delete \(selectedMetric.name ?? "unknown")") }.disabled(selectedMetric?.wrappedValue == nil || deleteMetricAction == nil )
}
}
}
// 5 In macOS #main App struct
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
}
.commands {
SidebarCommands()
MetricsCommands()
}
}
Use of #FocusedValues in Apple WWDC21 example
Use of FocusedValues in SwiftUI with Majid page
Use of #FocusedSceneValue with example of action value passed, in Apple documentation
For SwiftUI 3 macOS Table who support multiple selections
// 1 Properties
#Environment(\.managedObjectContext) var context
var type: Type
var fetchRequest: FetchRequest<Propriete>
var proprietes: FetchedResults<Propriete> { fetchRequest.wrappedValue }
#State private var selectedProprietes = Set<Propriete.ID>()
// 2 Init from Type managedObject who own Propriete managedObjects
// #FecthRequest required to have updates in Table (when delete for example)
init(type: Type) {
self.type = type
fetchRequest = FetchRequest<Propriete>(entity: Propriete.entity(),
sortDescriptors: [ NSSortDescriptor(key: "nom",
ascending: true,
selector: #selector(NSString.localizedCaseInsensitiveCompare(_:))) ],
predicate: NSPredicate(format: "type == %#", type))
}
// 3 Table view
VStack {
Table(proprietes, selection: $selectedProprietes) {
TableColumn("Propriétés :", value: \.wrappedNom)
}
.tableStyle(BorderedTableStyle())
.focusedSceneValue(\.selectedProprietes, $selectedProprietes)
.focusedSceneValue(\.deleteProprietesAction) {
deleteProprietes(selectedProprietes)
}
}
// 4 FocusedValues
extension FocusedValues {
private struct FocusedProprietesSelectionKey: FocusedValueKey {
typealias Value = Binding<Set<Propriete.ID>>
}
var selectedProprietes: Binding<Set<Propriete.ID>>? {
get { self[FocusedProprietesSelectionKey.self] }
set { self[FocusedProprietesSelectionKey.self] = newValue }
}
}
// 5 Delete (for example) in Table View
private func deleteProprietes(_ proprietesToDelete: Set<Propriete.ID>) {
var arrayToDelete = [Propriete]()
for (index, propriete) in proprietes.enumerated() {
if proprietesToDelete.contains(propriete.id) {
let propriete = proprietes[index]
arrayToDelete.append(propriete)
}
}
if arrayToDelete.count > 0 {
print("array to delete: \(arrayToDelete)")
for item in arrayToDelete {
context.delete(item)
print("\(item.wrappedNom) deleted!")
}
try? context.save()
}
}
How to manage selection in Table
I have a parent view DetailView and I want to query for heart rates during a workout. After I get back the heart rates I want to pass them to a child View. The code below, the heart rates aren't getting passed to HeartRateRecoveryCard ?
import SwiftUI
import HealthKit
struct DetailView: View {
#State var heartRateSamples: [HKQuantitySample] = []
var body: some View {
HeartRateRecoveryCard(heartRateSamples: heartRateSamples)
.onAppear {
getHeartRatesFromWorkout()
}
}
private func getHeartRatesFromWorkout() {
guard let workout = SelectedWorkoutSingleton.sharedInstance.selectedWorkout else { return }
print("workout = \(workout.startDate)")
WorkoutManager.getHeartRateSamplesFrom(workout: workout) { (samples, error) in
guard let unwrappedSamples = samples else {
print("no HR samples")
return }
if let unwrappedError = error {
print("error attempting to get heart rates = \(unwrappedError)")
}
print("we have \(unwrappedSamples.count) heart rates")
DispatchQueue.main.async {
heartRateSamples = unwrappedSamples
}
}
}
}
struct HeartRateRecoveryCard: View {
//Don't download HR samples for this view the parent view should download HR samples and pass them into it's children views
#State var heartRateSamples: [HKQuantitySample]
#State var HRRAnchor = 0
#State var HRRTwoMinutesLater = 0
#State var heartRateRecoveryValue = 0
var body: some View {
VStack(alignment: .leading) {
Text("\(heartRateRecoveryValue) bpm")
Text("\(HRRAnchor) bpm - \(heartRateRecoveryValue) bpm")
SwiftUILineChart(chartLabelName: "Heart Rate Recovery", entries: convertHeartRatesToLineChartDataAndSetFormatter(heartRates: heartRateSamples).0, xAxisFormatter: convertHeartRatesToLineChartDataAndSetFormatter(heartRates: heartRateSamples).1)
.frame(width: 350, height: 180, alignment: .center)
.onAppear {
print("heart rate count = \(heartRateSamples.count)")
if let unwrappedHRRObject = HeartRateRecoveryManager.calculateHeartRateRecoveryForHRRGraph(heartRateSamples: heartRateSamples) {
HRRAnchor = unwrappedHRRObject.anchorHR
HRRTwoMinutesLater = unwrappedHRRObject.twoMinLaterHR
heartRateRecoveryValue = unwrappedHRRObject.hrr
}
}
}
}
So, I have the following View Model where I fetch the data:
import Foundation
import SwiftUI
import Combine
import Alamofire
class AllStatsViewModel: ObservableObject {
#Published var isLoading: Bool = true
#Published var stats = [CountryStats]()
func fetchGlobalStats() {
let request = AF.request("https://projectcovid.deadpool.wtf/all")
request.responseDecodable(of: AllCountryStats.self) { (response) in
guard let globalStats = response.value else { return }
DispatchQueue.main.async {
self.stats = globalStats.data
}
self.isLoading = false
}
}
}
And this is the view:
struct CardView: View {
#ObservedObject var allStatsVM = AllStatsViewModel()
var body: some View {
VStack {
Text(self.allStatsVM.stats[0].country)
}
.onAppear {
self.allStatsVM.fetchGlobalStats()
}
}
}
I'd like to access only the first element of the data, but the problem I face is that when the view loads, the data is not loaded, so I get an index out of range error at
Text(self.allStatsVM.stats[0].country)
Is there a way, I can access the first element?
try this:
struct CardView: View {
#ObservedObject var allStatsVM = AllStatsViewModel()
var body: some View {
VStack {
if self.allStatsVM.stats.count > 0 {
Text(self.allStatsVM.stats[0].country)
} else {
Text ("data loading")
}
}
.onAppear {
self.allStatsVM.fetchGlobalStats()
}
}
}