0

Im a total noob to Swift and SwiftUI and im trying to build a project for myself where i can track my workouts and learn myself some Swift. The problem im hitting is that i have a view which shows all my workout sessions and are formatted by a setting i have in UserDefaults. The user can change this setting to 'metric' or 'imperial' When changing this the view should update to represent those changes.

The data i have is:

extension WorkoutSession {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<WorkoutSession> {
        return NSFetchRequest<WorkoutSession>(entityName: "WorkoutSession")
    }

    @NSManaged public var created_at: Date?
    @NSManaged public var id: UUID?
    @NSManaged public var reps: Int16
    @NSManaged public var sets: Int16
    @NSManaged public var updated_at: Date?
    @NSManaged public var weight: Double
    @NSManaged public var height: Double
    @NSManaged public var notes: String?
    @NSManaged public var type: Workout?

    override public func awakeFromInsert() {
        super.awakeFromInsert()
        setPrimitiveValue(UUID(), forKey: "id")
        setPrimitiveValue(Date(), forKey: "updated_at")
    }
    
    override public func willSave() {
        super.willSave()
        if let updated_at = updated_at {
            if updated_at.timeIntervalSince(Date()) > 10.0 {
                self.updated_at = Date()
            }

        } else {
            self.updated_at = Date()
        }
    }
    
    var formattedWeight: String {
        var measurement = Measurement(value: self.weight, unit: UserDefaultsWrapper().savedUnitWeight)
        let numberFormatter = NumberFormatter()
        let measurementFormatter = MeasurementFormatter()
        measurement.convert(to:UserDefaultsWrapper().getWeightUnit)
        numberFormatter.maximumFractionDigits = 2
        measurementFormatter.unitOptions = .providedUnit
        measurementFormatter.numberFormatter = numberFormatter
        return measurementFormatter.string(from: measurement)
    }
    
    
    // No matter the user defined weight we always save the values in Kilogram so we can convert to anything afterwards
    func convertWeightFromUserDefinedUnitToKilogram(weight: Double) -> Double {
        let measurement = Measurement(value: weight, unit: UserDefaultsWrapper().getWeightUnit)
        return measurement.converted(to: UserDefaultsWrapper().savedUnitWeight).value
    }
    
}

extension WorkoutSession : Identifiable {

}

I also made myself a helper for userDefaults

import SwiftUI
import Foundation

enum UserDefaultsKeys: String {
    case measurementUnit = "measurementUnit"
}

enum measurementUnit: String, CaseIterable {
    case metric = "metric"
    case imperial = "Imperial"
}


struct UserDefaultsWrapper {
    let defaults = UserDefaults.standard
    
    var savedUnitWeight = UnitMass.kilograms
    var savedUnitHeight = UnitLength.meters
    
    @AppStorage(UserDefaultsKeys.measurementUnit.rawValue) var selectedUnit: String = measurementUnit.metric.rawValue
    
    var getMeasurementUnit: measurementUnit {
        get {
            let locale = Locale.current
            let systemMeasurementUnit = locale.usesMetricSystem ? measurementUnit.metric : measurementUnit.imperial
            return measurementUnit(rawValue: selectedUnit) ?? systemMeasurementUnit
        }
        set(unit) {
            defaults.set(unit.rawValue, forKey: UserDefaultsKeys.measurementUnit.rawValue)
        }
    }

    var getWeightUnit: UnitMass {
        switch getMeasurementUnit {
            case measurementUnit.imperial:
                return UnitMass.pounds
            case measurementUnit.metric:
                fallthrough
            default:
                return UnitMass.kilograms
        }
    }
    
    var getHeightUnit: UnitLength {
        switch getMeasurementUnit {
            case measurementUnit.imperial:
                return UnitLength.feet
            case measurementUnit.metric:
                fallthrough
            default:
                return UnitLength.meters
        }
    }


}

And this is the view

import SwiftUI

struct WorkoutDetailView: View {
    
    @Environment(\.managedObjectContext) var moc
    
    @State private var showingAddWorkoutView = false
    
    @ObservedObject var workout: Workout
    
    var body: some View {
        List {
            ForEach(workout.sessionsArray, id: \.id) { session in
                Text("\(session.formattedWeight)")
            }
            .onDelete(
                perform: { offsets in
                    self.removeItems(at: offsets, from: workout)
                }
            )
        }
        .listStyle(InsetGroupedListStyle())
        .navigationTitle(workout.name ?? "Unknown Workout")
        .navigationBarItems(
            leading: EditButton(),
            trailing:
                Button(action: {
                    self.showingAddWorkoutView = true
                }) {
                    Image(systemName: "plus")
                }
        )
        .sheet(isPresented: $showingAddWorkoutView) {
            AddWorkoutSession(workout: workout)
                .environment(\.managedObjectContext, moc)
        }
    }
    
    func removeItems(at offsets: IndexSet, from workout: Workout) {
        for offset in offsets {
            let sessionToDelete = workout.sessionsArray[offset]
            workout.removeFromSessions(sessionToDelete)
            moc.delete(sessionToDelete)
        }
        if moc.hasChanges{
            try? moc.save()
        }
    }
}

The problem is that when i change the unit from imperial to metric and i go back to the session view the view is not updated, but when i go back to the workout view and open the session view again the changes are there.

Please let me know if you need more code.

andre de waard
  • 134
  • 2
  • 6
  • 22
  • Just use AppStorage, like in https://stackoverflow.com/a/62716736/12299030 – Asperi Mar 08 '21 at 12:57
  • Thanks for your reply, Adding AppStorage to my UserDefaults Helper still doesnt update my view. Is what im trying to do even possible? or should i make `formattedWeight` an func which accepts a parameter of a massUnit and return the formatted string? – andre de waard Mar 08 '21 at 15:33
  • 1
    Why not just `Text(session.formattedWeight)`? How are you changing the preference in relation to the session view; you say "when I go back to the session view" how do you "go back"? Are you presenting a new view or going backwards to an existing view? – Paulw11 Mar 08 '21 at 20:11
  • Yes i could that also but that doesnt change anything. My navigating back i mean that i have a tabbar inside the ContentView, one where you see the settings and one with the workouts, and via that workouts overview you can see the sessions of that workout. so if im on the workout detail page to see the sessions, go to the settings page to change the units and will go back to the sessions via the tab bar the units aren't updating. I Fixed this now but including `@ObservedObject var userDefaultsWrapper = UserDefaultsWrapper()` inside that view. which i dont use but it updates the view. – andre de waard Mar 08 '21 at 20:37
  • 1
    Yes, the text was just a suggestion to simplify your code. When you switch between tabs the views aren't refreshed unless SwiftUI knows that it needs to do so. Computed properties can't be subscribed the way a binding or published or observable object can – Paulw11 Mar 08 '21 at 21:08

0 Answers0