SwiftUI: EnvironmentObject loads only the first time - class

in my current project I work with an EnvironmentObject but I have a very strange bug, maybe someone can help.
First the code:
Model:
class Daten: ObservableObject{
#Published var intervallInsgesamt: Double = UserDefaults.standard.double(forKey: Keys.intervallInsgesamt){
didSet{
UserDefaults.standard.set(self.intervallInsgesamt, forKey: Keys.intervallInsgesamt)
}
}
#Published var pauseInsgesamt: Double = UserDefaults.standard.double(forKey: Keys.pauseInsgesamt ){
didSet{
UserDefaults.standard.set(self.pauseInsgesamt, forKey: Keys.pauseInsgesamt)
}
}
...
}
First View:
#EnvironmentObject var daten: Daten
var body: some View {
NavigationView{
GeometryReader { geometry in
VStack{
ScrollView(showsIndicators: false){
...
}
//IMPORTANT START
NavigationLink(destination: TrainingView().environmentObject(daten)){ //!!!
Text("Start")
.styleButton()
}
//IMPORTANT END
}
}
}
}
Second View (TraingsView):
struct TrainingView: View {
#EnvironmentObject var daten: Daten
#State private var isRunning = false
#State private var to: CGFloat = 0
#State private var insgesamt = 0.0
#State private var minuten = 0
#State private var sekunden = 0
#State private var durchgang = 1
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State private var zähl = 0
#State private var color = Color.red
#State private var übung = 0
#State private var momentaneÜbung = ""
#State private var nächsteÜbung = ""
#State private var teilerTo = 0
var body: some View {
ZStack{
VStack{
...
}
HStack(spacing: 50){
Button(action:{
isRunning = true
Sounds.playSounds(soundfile: "sound.wav")
}) {
Text("Start")
}
Button(action:{
isRunning = false
Sounds.playSounds(soundfile: "sound.wav")
}) {
Text("Stop")
}
}
}
}
.onReceive(timer) { _ in
if isRunning{
if insgesamt > 0{
print("1. Durchgang: \(durchgang), Übung: \(übung), momenaten: \(momentaneÜbung), nächste: \(nächsteÜbung)")
insgesamt -= 1.0
minuten = Int(insgesamt/60)
sekunden = Int(insgesamt.truncatingRemainder(dividingBy: 60))
zähl += 1
withAnimation(.default) {
to = CGFloat(Double(zähl) / Double(teilerTo))
}
}else{
... stuff that should run when insgesamt <= 0 but is not important for my problem
}
}
}
.onAppear {
teilerTo = Int(daten.intervallInsgesamt)
print("Teiler: \(teilerTo)")
insgesamt = daten.intervallInsgesamt
minuten = Int(insgesamt/60)
sekunden = Int(insgesamt.truncatingRemainder(dividingBy: 60))
if !daten.übungen.isEmpty{
momentaneÜbung = daten.übungen[übung].name
if übung+1 < daten.übungen.count{
nächsteÜbung = "Pause"
}else{
nächsteÜbung = "Nächster Durchgang"
}
}
}
}
}
NOW THE PROBLEM:
When I start the app, set the different times, go to the TrainingsView, everything works fine. Then I go back to the first View(change the times or not, it doesn't matter) and go back to the TraingsView. Now nothing works! It doesn't find the EnvironmentObject anymore. I don't know why because the first time it did. Does somebody know why?
(An other problem I is, that print() doesn't work. I can't see the things that should print out. I don't know if that is important...)
Thanks in advance!

In your root view, you should declare your object as an observable object:
#ObservableObject var daten = Daten()
then you should use a .environmentObject() modifier on top of your first View:
// pass the daten object that was declared as observable object here
.environmentObject(daten)
from now on, on other descendant views, you can get your model by declaring it as an environment object:
#EnvironmentObject var daten: Daten
// gives you the same old daten model

Related

Conditional view modifier sometimes doesn't want to update

As I am working on a study app, I'm tring to build a set of cards, that a user can swipe each individual card, in this case a view, in a foreach loop and when flipped through all of them, it resets the cards to normal stack. The program works but sometimes the stack of cards doesn't reset. Each individual card updates a variable in a viewModel which my conditional view modifier looks at, to reset the stack of cards using offset and when condition is satisfied, the card view updates, while using ".onChange" to look for the change in the viewModel to then update the variable back to original state.
I've printed each variable at each step of the way and every variable updates and I can only assume that the way I'm updating my view, using conditional view modifier, may not be the correct way to go about. Any suggestions will be appreciated.
Here is my code:
The view that houses the card views with the conditional view modifier
extension View {
#ViewBuilder func `resetCards`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition == true {
transform(self).offset(x: 0, y: 0)
} else {
self
}
}
}
struct StudyListView: View {
#ObservedObject var currentStudySet: HomeViewModel
#ObservedObject var studyCards: StudyListViewModel = StudyListViewModel()
#State var studyItem: StudyModel
#State var index: Int
var body: some View {
ForEach(currentStudySet.allSets[index].studyItem.reversed()) { item in
StudyCardItemView(currentCard: studyCards, card: item, count: currentStudySet.allSets[index].studyItem.count)
.resetCards(studyCards.isDone) { view in
view
}
.onChange(of: studyCards.isDone, perform: { _ in
studyCards.isDone = false
})
}
}
}
StudyCardItemView
struct StudyCardItemView: View {
#StateObject var currentCard: StudyListViewModel
#State var card: StudyItemModel
#State var count: Int
#State var offset = CGSize.zero
var body: some View {
VStack{
VStack{
ZStack(alignment: .center){
Text("\(card.itemTitle)")
}
}
}
.frame(width: 350, height: 200)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
.padding(5)
.rotationEffect(.degrees(Double(offset.width / 5)))
.offset(x: offset.width * 5, y: 0)
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded{ _ in
if abs(offset.width) > 100 {
currentCard.cardsSortedThrough += 1
if (currentCard.cardsSortedThrough == count) {
currentCard.isDone = true
currentCard.cardsSortedThrough = 0
}
} else {
offset = .zero
}
}
)
}
}
HomeViewModel
class HomeViewModel: ObservableObject {
#Published var studySet: StudyModel = StudyModel()
#Published var allSets: [StudyModel] = [StudyModel()]
}
I initialize allSets with one StudyModel() to see it in the preview
StudyListViewModel
class StudyListViewModel: ObservableObject {
#Published var cardsSortedThrough: Int = 0
#Published var isDone: Bool = false
}
StudyModel
import SwiftUI
struct StudyModel: Hashable{
var title: String = ""
var days = ["One day", "Two days", "Three days", "Four days", "Five days", "Six days", "Seven days"]
var studyGoals = "One day"
var studyItem: [StudyItemModel] = []
}
Lastly, StudyItemModel
struct StudyItemModel: Hashable, Identifiable{
let id = UUID()
var itemTitle: String = ""
var itemDescription: String = ""
}
Once again, any help would be appreciated, thanks in advance!
I just found a fix and I put .onChange at the end for StudyCardItemView. Basically, the onChange helps the view scan for a change in currentCard.isDone variable every time it was called in the foreach loop and updates offset individuality. This made my conditional view modifier obsolete and just use the onChange to check for the condition.
I still used onChange outside the view with the foreach loop, just to set currentCard.isDone variable false because the variable will be set after all array elements are iterator through.
The updated code:
StudyCardItemView
struct StudyCardItemView: View {
#StateObject var currentCard: StudyListViewModel
#State var card: StudyItemModel
#State var count: Int
#State var offset = CGSize.zero
var body: some View {
VStack{
VStack{
ZStack(alignment: .center){
Text("\(card.itemTitle)")
}
}
}
.frame(width: 350, height: 200)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
.padding(5)
.rotationEffect(.degrees(Double(offset.width / 5)))
.offset(x: offset.width * 5, y: 0)
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded{ _ in
if abs(offset.width) > 100 {
currentCard.cardsSortedThrough += 1
if (currentCard.cardsSortedThrough == count) {
currentCard.isDone = true
currentCard.cardsSortedThrough = 0
}
} else {
offset = .zero
}
}
)
.onChange(of: currentCard.isDone, perform: {_ in
offset = .zero
})
}
}
StudyListView
struct StudyListView: View {
#ObservedObject var currentStudySet: HomeViewModel
#ObservedObject var studyCards: StudyListViewModel = StudyListViewModel()
#State var studyItem: StudyModel
#State var index: Int
var body: some View {
ForEach(currentStudySet.allSets[index].studyItem.reversed()) { item in
StudyCardItemView(currentCard: studyCards, card: item, count:
currentStudySet.allSets[index].studyItem.count)
.onChange(of: studyCards.isDone, perform: { _ in
studyCards.isDone = false
})
}
}
}
Hope this helps anyone in the future!

Timer within EnvironmentObject view model not updating the View

I have a view model, that has multiple child view models. I am fairly new to watchOS, SwiftUI and Combine - taking this opportunity to learn.
I have a watchUI where it has
Play Button (View) - SetTimerPlayPauseButton
Text to show Time (View) - TimerText
View Model - that has 1 WatchDayProgramViewModel - N: ExerciseTestClass - N: SetInformationTestClass. For each ExerciseSets, there is a watchTimer & watchTimerSubscription and I have managed to run the timer to update remaining rest time.
ContentView - that has been injected the ViewModel as EnvironmentObject
If I tap SetTimerPlayPauseButton to start the timer, timer is running, working and changing the remainingRestTime(property within the child view model SetInformationTestClass) correctly, but the updates/changes are not being "published" to the TimerText View.
I have done most, if not all, the recommendation in other SO answers, I even made all my WatchDayProgramViewModel and ExerciseTestClass,SetInformationTestClass properties #Published, but they are still not updating the View, when the view model properties are updated as shown in the Xcode debugger below.
Please review my code and give me some advice on how to improve it.
ContentView
struct ContentView: View {
#State var selectedTab = 0
#StateObject var watchDayProgramVM = WatchDayProgramViewModel()
var body: some View {
TabView(selection: $selectedTab) {
SetRestDetailView().id(2)
}
.environmentObject(watchDayProgramVM)
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(.page(backgroundDisplayMode: .automatic))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView(watchDayProgramVM: WatchDayProgramViewModel())
}
}
}
SetRestDetailView
import Foundation
import SwiftUI
import Combine
struct SetRestDetailView: View {
#EnvironmentObject var watchDayProgramVM: WatchDayProgramViewModel
var setCurrentHeartRate: Int = 120
#State var showingLog = false
var body: some View {
HStack {
let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
TimerText(elapsedRestTime: elapsedRestTime, totalRestTime: totalRestTime, rect: rect)
.border(Color.yellow)
}
HStack {
SetTimerPlayPauseButton(isSetTimerRunningFlag: false,
playImage: "play.fill",
pauseImage: "pause.fill",
bgColor: Color.clear,
fgColor: Color.white.opacity(0.5),
rect: rect) {
print("playtimer button tapped")
self.watchDayProgramVM.exerciseVMList[0].sets[2].startTimer()
let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
print("printing elapsedRestTime from SetRestDetailView \(elapsedRestTime)")
print("printing elapsedRestTime from SetRestDetailView \(totalRestTime)")
}
.border(Color.yellow)
}
}
}
TimerText
struct TimerText: View {
var elapsedRestTime: Int
var totalRestTime: Int
var rect: CGRect
var body: some View {
VStack {
Text(counterToMinutes())
.font(.system(size: 100, weight: .semibold, design: .rounded))
.kerning(0)
.fontWeight(.semibold)
.minimumScaleFactor(0.25)
.padding(-1)
}
}
func counterToMinutes() -> String {
let currentTime = totalRestTime - elapsedRestTime
let seconds = currentTime % 60
let minutes = Int(currentTime / 60)
if currentTime > 0 {
return String(format: "%02d:%02d", minutes, seconds)
}
else {
return ""
}
}
}
ViewModel
import Combine
final class WatchDayProgramViewModel: ObservableObject {
#Published var exerciseVMList: [ExerciseTestClass] = [
(static/hard-coded values for testing)
]
class ExerciseTestClass: ObservableObject {
init(exercise: String, sets: [SetInformationTestClass]) {
self.exercise = exercise
self.sets = sets
}
var exercise: String
#Published var sets: [SetInformationTestClass]
}
class SetInformationTestClass: ObservableObject {
init(totalRestTime: Int, elapsedRestTime: Int, remainingRestTime: Int, isTimerRunning: Bool) {
self.totalRestTime = totalRestTime
self.elapsedRestTime = elapsedRestTime
self.remainingRestTime = remainingRestTime
self.isTimerRunning = isTimerRunning
}
#Published var totalRestTime: Int
#Published var elapsedRestTime: Int
#Published var remainingRestTime: Int
#Published var isTimerRunning = false
#Published var watchTimer = Timer.publish(every: 1.0, on: .main, in: .default)
#Published var watchTimerSubscription: AnyCancellable? = nil
#Published private var startTime: Date? = nil
func startTimer() {
print("startTimer initiated")
self.watchTimerSubscription?.cancel()
if startTime == nil {
startTime = Date()
}
self.isTimerRunning = true
self.watchTimerSubscription = watchTimer
.autoconnect()
.sink(receiveValue: { [weak self] _ in
guard let self = self, let startTime = self.startTime else { return }
let now = Date()
let elapsedTime = now.timeIntervalSince(startTime)
self.remainingRestTime = self.totalRestTime - Int(elapsedTime)
self.elapsedRestTime = self.totalRestTime - self.remainingRestTime
guard self.remainingRestTime > 0 else {
self.pauseTimer()
return
}
self.objectWillChange.send()
print("printing elapsedRest Time \(self.elapsedRestTime) sec")
print("printing remaining Rest time\(self.remainingRestTime)sec ")
})
}
func pauseTimer() {
//stop timer and retain elapsed rest time
print("pauseTimer initiated")
self.watchTimerSubscription?.cancel()
self.watchTimerSubscription = nil
self.isTimerRunning = false
self.startTime = nil
}
Managed to resolve the issue with help of #lorem ipsum and his feedback. As per his comment, the problem lied with the fact that
it is more than likely not working because you are chaining ObservableObjects #Published will only detect a change when the object is changed as a whole now when variables change. One way to test is to wrap each SetInformationTestClass in an #ObservbleObject by using a subview that takes the object as a parameter.
After which, I managed to find similar SO answers on changes in nested view model (esp child), and made the child view model an ObservedObject. The changes in child view model got populated to the view. Please see the changed code below.
SetRestDetailView
import Foundation
import SwiftUI
import Combine
struct SetRestDetailView: View {
#EnvironmentObject var watchDayProgramVM: WatchDayProgramViewModel
var setCurrentHeartRate: Int = 120
#State var showingLog = false
var body: some View {
HStack {
let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
let setInformatationVM = self.watchDayProgramVM.exerciseVMList[0].sets[2]
TimerText(setInformationVM: setInformatationVM, rect: rect)
.border(Color.yellow)
}
HStack {
SetTimerPlayPauseButton(isSetTimerRunningFlag: false,
playImage: "play.fill",
pauseImage: "pause.fill",
bgColor: Color.clear,
fgColor: Color.white.opacity(0.5),
rect: rect) {
print("playtimer button tapped")
self.watchDayProgramVM.exerciseVMList[0].sets[2].startTimer()
let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
print("printing elapsedRestTime from SetRestDetailView \(elapsedRestTime)")
print("printing elapsedRestTime from SetRestDetailView \(totalRestTime)")
}
.border(Color.yellow)
}
}
}
TimerText
struct TimerText: View {
#ObservedObject var setInformationVM: SetInformationTestClass
// #State var elapsedRestTime: Int
// #State var totalRestTime: Int
var rect: CGRect
var body: some View {
VStack {
Text(counterToMinutes())
.font(.system(size: 100, weight: .semibold, design: .rounded))
.kerning(0)
.fontWeight(.semibold)
.minimumScaleFactor(0.25)
.padding(-1)
}
}
func counterToMinutes() -> String {
let currentTime = setInformationVM.totalRestTime - setInformationVM.elapsedRestTime
let seconds = currentTime % 60
let minutes = Int(currentTime / 60)
if currentTime > 0 {
return String(format: "%02d:%02d", minutes, seconds)
}
else {
return ""
}
}
}

SwiftUI Multiple Timer Management

I'm making the app needs to control several timer at once, and now facing 2 problems.
1: N timers keep running N times faster than expected.
2: Stop button not working for each timer.
2nd problem is the core problem that I'm running into the real project, and would like to understand how to manage the multiple timer instances when they are generated with loop structure.
I pasted the test code, and this code does not use TimelineView for a reason, understanding how the Timer works.
import SwiftUI
import Combine
import Foundation
struct ContentView: View {
#StateObject var timerData = TimerDataViewModel()
var body: some View {
ScrollView{
ForEach(0..<10) { _ in
CurrentDateView(timerData: timerData)
Button(action:{
timerData.stop()
}, label:{
Text("STOP THIS TIMER")
})
}
}
}
}
struct CurrentDateView: View {
#State private var currentDate = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#ObservedObject var timerData: TimerDataViewModel
var body: some View {
Text("\(Int(timerData.hoursElapsed), specifier: "%02d"):\(Int(timerData.minutesElapsed), specifier: "%02d"):\(Int(timerData.secondsElapsed), specifier: "%02d")")
.fontWeight(.bold)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onAppear(){
timerData.start()
}
}
}
import Foundation
import SwiftUI
class TimerDataViewModel: ObservableObject{
#Published var timer = Timer()
#Published var startTime : Double = 0.0
#Published var secondsOriginal = 0.0
#Published var secondsElapsed = 0.0
#Published var secondsElapsed_ = 0.0
#Published var minutesElapsed = 0.0
#Published var hoursElapsed = 0.0
enum stopWatchMode {
case running
case stopped
case paused
}
init(){
// start()
print("initialized")
}
func start(){
self.secondsOriginal = self.startTime
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ timer in
self.secondsOriginal += 1
self.secondsElapsed_ = Double(Int(self.secondsOriginal))
self.secondsElapsed = Double(Int(self.secondsOriginal)%60)
self.minutesElapsed = Double(Int(self.secondsOriginal)/60 % 60)
self.hoursElapsed = Double(Int(self.secondsOriginal)/3600 % 24)
}
}
func stop(){
self.timer.invalidate()
}
}

Why doesn't code in a body property of a View run each time an #State variable of its parent View changes?

I wish to run the function calculateBedtime() when the app first loads, and each time any of the #State variables of ContentView change, so that an updated bedtime is displayed constantly at the bottom of the app in the lowermost Section. However, the app acts as if variable bedtime just keeps its initial value all the time and never changes.
What I am expecting to happen is that when I change any #State variable, say using the DatePicker to change wakeUp, the body property is reinvoked, the first line of which is a call to calculateBedtime(), and so this function runs and updates bedtime as frequently as I want it to.
import SwiftUI
struct ContentView: View {
#State private var wakeUp = defaultWakeTime
#State private var bedtime = ""
#State private var sleepAmount = 8.0
#State private var coffeeAmount = 1
#State private var alertTitle = ""
#State private var alertMessage = ""
#State private var showingAlert = false
var body: some View {
bedtime = calculateBedtime()
return NavigationView
{
Form
{
Section(header: Text("When do you want to wake up?").font(.headline))
{
Text("When do you want to wake up?")
.font(.headline)
DatePicker("Please enter a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
.labelsHidden()
.datePickerStyle(WheelDatePickerStyle())
}
Section(header: Text("Desired amount of sleep")
.font(.headline))
{
Stepper(value: $sleepAmount, in: 4...12, step: 0.25)
{
Text("\(sleepAmount, specifier: "%g") hours")
}
}
Section(header: Text("Daily coffee intake")
.font(.headline))
{
Picker("\(coffeeAmount+1) cup(s)", selection: $coffeeAmount)
{
ForEach(1..<21)
{ num in
if num==1
{
Text("\(num) cup")
}
else
{
Text("\(num) cups")
}
}
}
.pickerStyle(MenuPickerStyle())
}
Section(header: Text("Your Ideal Bedtime")
.font(.headline))
{
Text("\(bedtime)")
.font(.largeTitle)
}
}
.navigationBarTitle("BetterRest")
}
/*.onAppear(perform: {
calculateBedtime()
})
.onChange(of: wakeUp, perform: { value in
calculateBedtime()
})
.onChange(of: sleepAmount, perform: { value in
calculateBedtime()
})
.onChange(of: coffeeAmount, perform: { value in
calculateBedtime()
})*/
}
static var defaultWakeTime: Date
{
var components = DateComponents()
components.hour = 7
components.minute = 0
return Calendar.current.date(from: components) ?? Date()
}
func calculateBedtime() -> String
{
let model = SleepCalculator()
let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUp)
let hour = (components.hour ?? 0) * 60 * 60
let minute = (components.minute ?? 0) * 60
var sleepTime = ContentView.defaultWakeTime
do
{
let prediction = try
model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
sleepTime = wakeUp - prediction.actualSleep
let formatter = DateFormatter()
formatter.timeStyle = .short
alertMessage = formatter.string(from: sleepTime)
alertTitle = "Your ideal bedtime is..."
} catch {
alertTitle = "Error"
alertMessage = "Sorry, there was a problem calculating your bedtime."
}
showingAlert = true
return alertMessage
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
What is the problem here? I am new to SwiftUI and feel that I must have a crucial misunderstanding of how the #State wrapper works. And what would be a good way to get the behavior I desire?
#State variables can only be mutated from within the body of your view and methods invoked by it; for anything else, you need to use ObservableObject which I think will solve your problem here.
You should only access a state property from inside the view’s body, or from methods called by it. For this reason, declare your state properties as private, to prevent clients of your view from accessing them. It is safe to mutate state properties from any thread.
More or less the scaffolding of the code below should achieve the results you want:
class SleepTimerViewModel: ObservableObject {
#Published public var bedTimeMessage: String?
}
struct ContentView: View {
#ObservedObject public var sleepTimerViewModel: SleepTimerViewModel
var body: some View {
Text(sleepTimerViewModel.bedTimeMessage)
}
public func updateBedTimeMessage() {
sleepTimerViewModel.bedTimeMessage = "Hello World"
}
}
I do think it's kind of annoying that Swift just don't care to let you know that you're updating a #State variable incorrectly. It just silently ignores the value you're trying to set, which is super annoying!

SwiftUI: Published string changes inside view model are not updating view

I have a timer inside my view model class which every second changes two #Published strings inside the view model. View model class is an Observable Object which Observed by the view but the changes to these string objects are not updating my view.
I have a very similar structure in many other views(Published variables inside a ObservableObject which is observed by view) and it always worked. I can't seem to find what am I doing wrong?
ViewModel
final class QWMeasurementViewModel: ObservableObject {
#Published var measurementCountDownDetails: String = ""
#Published var measurementCountDown: String = ""
private var timer: Timer?
private var scheduleTime = 0
func setTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
DispatchQueue.main.async {
self.scheduleTime += 1
if self.scheduleTime == 1 {
self.measurementCountDownDetails = "Get ready"
self.measurementCountDown = "1"
}
else if self.scheduleTime == 2 {
self.measurementCountDownDetails = "Relax your arm"
self.measurementCountDown = "2"
}
else if self.scheduleTime == 3 {
self.measurementCountDownDetails = "Breathe"
self.measurementCountDown = "3"
}
else if self.scheduleTime == 4 {
self.measurementCountDownDetails = ""
self.measurementCountDown = ""
timer.invalidate()
}
}
}
}
}
View
struct QWMeasurementView: View {
#ObservedObject var viewModel: QWMeasurementViewModel
var body: some View {
VStack {
Text(viewModel.measurementCountDownDetails)
.font(.body)
Text(viewModel.measurementCountDown)
.font(.title)
}
.onAppear {
viewModel.setTimer()
}
}
}
Edit
After investigation, this seems to be related to how it is being presented. Cause if it's a single view this code works but I am actually presenting this as a sheet. (Still cannot understand why would it make a difference..)
struct QWBPDStartButtonView: View {
#ObservedObject private var viewModel: QWBPDStartButtonViewModel
#State private var startButtonPressed: Bool = false
init(viewModel: QWBPDStartButtonViewModel) {
self.viewModel = viewModel
}
var body: some View {
Button(action: {
self.startButtonPressed = true
}) {
ZStack {
Circle()
.foregroundColor(Color("midGreen"))
Text("Start")
.font(.title)
}
}
.buttonStyle(PlainButtonStyle())
.sheet(isPresented: $startButtonPressed) {
QWMeasurementView(viewModel: QWMeasurementViewModel())
}
}
}
You’re passing in a brand new viewmodel to the sheet’s view.
Try passing in the instance from line 3