SwiftUI Timer restart after canceling - timer

I'm still trying out timers and this works as well. The timer is exited after a certain time. But how can I restart it now? I'm grateful for any help. I suspect with .autoconnect (). But I don't really get it.
import SwiftUI
struct ContentView: View {
#State private var timerHold = false
#State var waitingTimerCount = 0
let waiting = ["A","B","C",]
let waitingTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Button(action: {
self.timerHold = true
}) { Text("Button") }
if timerHold == true {
Text(verbatim: waiting[waitingTimerCount])
.onReceive(waitingTimer) { timer in
self.waitingTimerCount = (self.waitingTimerCount + 1) % self.waiting.count
if self.waitingTimerCount == 3 {
self.timerHold = false
self.waitingTimerCount = 0
self.waitingTimer.upstream.connect().cancel()
}
}
}
}
}
}
//??
// self.waitingTimer.upstream.autoconnect()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

auto-connecting timer in playground
import PlaygroundSupport
import Combine
import Foundation
PlaygroundPage.current.needsIndefiniteExecution = true
var cancellable: AnyCancellable?
// start automatically
cancellable = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.sink {
print($0)
}
prints
...
2020-02-24 02:22:20 +0000
2020-02-24 02:22:21 +0000
2020-02-24 02:22:22 +0000
2020-02-24 02:22:23 +0000
2020-02-24 02:22:24 +0000
2020-02-24 02:22:25 +0000
2020-02-24 02:22:26 +0000
...
manually start / stop timer
let timerPublisher = Timer.publish(every: 1.0, on: RunLoop.main, in: .default)
cancellable = timerPublisher
.sink {
print($0)
}
/// ...
// start publishing time
let cancellableTimerPublisher = timerPublisher.connect()
// stop publishing time
cancellableTimerPublisher.cancel()

Related

Swift Struct in Array of Structs not updating to new values

This is my data structure
struct SPPWorkout: Codable {
static let setKey = "Sets"
static let exerciseID = "id"
var id: Double? = 0.0
var duration: String?
var calories: Int?
var date: String?
var exercises: [ExerciseSet]
[...]
}
struct ExerciseSet: Codable {
let id: String
let name: String
var reps: Int
var weight: Double
[...]
}
extension ExerciseSet: Equatable {
static func ==(lhs: ExerciseSet, rhs: ExerciseSet) -> Bool {
return lhs.id == rhs.id
}
}
and in a SwiftUI view I'm trying to modify an ExerciseSet from user input
#State private var sppWorkout: SPPWorkout!
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
guard let editingIndex = editingIndex else { return }
sppWorkout.exercises[editingIndex].reps = Int(reps) ?? 0
sppWorkout.exercises[editingIndex].weight = Double(weight) ?? 0.0
self.editingIndex = nil
})
}
The issue is here
sppWorkout.exercises[editingIndex].reps = Int(reps) ?? 0
sppWorkout.exercises[editingIndex].weight = Double(weight) ??
and I've tried in all ways to update it, both from the view and with a func in SPPWorkout. I've also tried to replace the object at index
var newSet = ExerciseSet(id: [...], newValues)
self.exercises[editingIndex] = newSet
but in no way it wants to update. I'm sure that somewhere it creates a copy that it edits but I have no idea why and how to set the new values.
Edit: if I try to delete something, it's fine
sppWorkout.exercises.removeAll(where: { $0 == sppWorkout.exercises[index]})
Edit 2:
It passes the guard statement and it does not change the values in the array.
Edit 3:
At the suggestion below from Jared, I've copied the existing array into a new one, set the new values then tried to assign the new one over to the original one but still, it does not overwrite.
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
print(sppWorkout.exercises)
guard let editingIndex = editingIndex else { return }
var copyOfTheArray = sppWorkout.exercises
copyOfTheArray[editingIndex].reps = Int(reps) ?? 0
copyOfTheArray[editingIndex].weight = Double(weight) ?? 0.0
//Copy of the array is updated correctly, it has the new values
sppWorkout.exercises = copyOfTheArray
//Original array doesn't get overwritten. It still has old values
self.editingIndex = nil
Edit 4: I've managed to make progress by extracting the model into a view model and updating the values there. Now the values get updated in sppWorkout, but even though I call objectWillChange.send(), the UI Update doesn't trigger.
full code:
class WorkoutDetailsViewModel: ObservableObject {
var workoutID: String!
#Published var sppWorkout: SPPWorkout!
func setupData(with workoutID: String) {
sppWorkout = FileIOManager.readWorkout(with: workoutID)
}
func update(_ index: Int, newReps: Int, newWeight: Double) {
let oldOne = sppWorkout.exercises[index]
let update = ExerciseSet(id: oldOne.id, name: oldOne.name, reps: newReps, weight: newWeight)
sppWorkout.exercises[index] = update
self.objectWillChange.send()
}
}
struct WorkoutDetailsView: View {
var workoutID: String!
#StateObject private var viewModel = WorkoutDetailsViewModel()
var workout: HKWorkout
var dateFormatter: DateFormatter
#State private var offset = 0
#State private var isShowingOverlay = false
#State private var editingIndex: Int?
#EnvironmentObject var settingsManager: SettingsManager
#Environment(\.dismiss) private var dismiss
var body: some View {
if viewModel.sppWorkout != nil {
VStack {
ListWorkoutItem(workout: workout, dateFormatter: dateFormatter)
.padding([.leading, .trailing], 10.0)
List(viewModel.sppWorkout.exercises, id: \.id) { exercise in
let index = viewModel.sppWorkout.exercises.firstIndex(of: exercise) ?? 0
DetailListSetItem(exerciseSet: viewModel.sppWorkout.exercises[index], set: index + 1)
.environmentObject(settingsManager)
.swipeActions {
Button(role: .destructive, action: {
viewModel.sppWorkout.exercises.removeAll(where: { $0 == viewModel.sppWorkout.exercises[index]})
} ) {
Label("Delete", systemImage: "trash")
}
Button(role: .none, action: {
isShowingOverlay = true
editingIndex = index
} ) {
Label("Edit", systemImage: "pencil")
}.tint(.blue)
}
}
.padding([.leading, .trailing], -30)
//iOS 16 .scrollContentBackground(.hidden)
}
.overlay(alignment: .bottom, content: {
editOverlay
.animation(.easeInOut (duration: 0.5), value: isShowingOverlay)
})
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action : {
do {
try FileIOManager.write(viewModel.sppWorkout, toDocumentNamed: "\(viewModel.sppWorkout.id ?? 0).json")
} catch {
Debugger.log(error: error.localizedDescription)
}
dismiss()
}){
Image(systemName: "arrow.left")
})
} else {
Text("No workout details found")
.italic()
.fontWeight(.bold)
.font(.system(size: 35))
.onAppear(perform: {
viewModel.setupData(with: workoutID)
})
}
}
#ViewBuilder private var editOverlay: some View {
if isShowingOverlay {
ZStack {
Button {
isShowingOverlay = false
} label: {
Color.clear
}
.edgesIgnoringSafeArea(.all)
VStack{
Spacer()
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
guard let editingIndex = editingIndex else { return }
print(viewModel.sppWorkout.exercises)
print("dupa aia:\n")
viewModel.update(editingIndex, newReps: Int(reps) ?? 0, newWeight: Double(weight) ?? 0.0)
print(viewModel.sppWorkout.exercises)
self.editingIndex = nil
})
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color("popupBackground"),
lineWidth: 3)
)
}
}
}
}
}
So I got a very good explanation on reddit on what causes the problem. Thank you u/neddy-seagoon if you are reading this.
The explanation
. I believe that updating an array will not trigger a state update. The only thing that will, with an array, is if the count changes. So
sppWorkout.exercises[index].reps = newReps
will not cause a trigger. This is not changing viewModel.sppWorkout.exercises.indices
So all I had to to was modify my List from
List(viewModel.sppWorkout.exercises, id: \.id)
to
List(viewModel.sppWorkout.exercises, id: \.hashValue)
as this triggers the list update because the hashValue does change when updating the properties of the entries in the list.
For the line
List(viewModel.sppWorkout.exercises, id: \.id) { exercise in
Replace with
List(viewModel.sppWorkout.exercises, id: \.self) { exercise in

Swift - Cannot append Object to Array

I have created a New View in my App, but the Identifiable Object won't append to the Array.
I really don't know why its not appending...
Here is the Code:
struct FirstSettingsIdentifiables: Identifiable {
var id: UUID = UUID()
var name: String
var icon: String
}
struct SettingsView: View {
#EnvironmentObject var settingItems: ContentModel
#State var firstArr: [FirstSettingsIdentifiables] = []
init() {
createFirstList()
print("Settings successfully initialized.")
}
var body: some View {
return VStack {
Text("Einstellungen")
.font(.title)
NavigationView {
//Mitteilungen Liste
List(firstArr) { x in
// ForEach(firstArr) { x in
// VStack {
// Image(systemName: x.icon)
Text("Das ist ein test")
// }
// }
}.navigationBarTitle("Mitteilungen")
}
}
}
func createFirstList() {
let aText = "Mitteilungen"
let aIcon = "info.circle.fill"
let aObject = FirstSettingsIdentifiables(name: aText, icon: aIcon)
firstArr.append(aObject)
print(firstArr.count)
}
}
The problem is probably in the createFirstList() Section. In this function, the Object aObject is full of data(This is working fine), but then the Object won't append to my firstArr. The count is always 0.
What am I doing wrong here?
You are changing the value of firstArr too early. Instead of calling createFirstList() in the init, remove that and instead add the following code onto the view body:
VStack {
/* ... */
}
.onAppear(perform: createFirstList)
Alternatively, you could do the following:
init() {
_firstArr = State(initialValue: getFirstList())
print(firstArr.count)
print("Settings successfully initialized.")
}
/* ... */
func getFirstList() -> [FirstSettingsIdentifiables] {
let aText = "Mitteilungen"
let aIcon = "info.circle.fill"
let aObject = FirstSettingsIdentifiables(name: aText, icon: aIcon)
return [aObject]
}

Set up a repeating 24:00:00 countdown timer which stops and resets at 00:00:00

Right now im getting a countdown timer which actually has the form "24:00:00". The problem is that the timer keeps running after being at "00:00:00" and starts counting down negative numbers. What i want is the timer to stop at 0 and repeat at 24 hours. My problem is that the time is of the Data.type Date(). so i don't know how to explain with an if statement that the textlabel should change if the countdown timer == 0. I hope you understand the problem
struct ContentView: View {
// Timer
func countDownString(from referenceDate: Date, until nowDate: Date) -> String
{
let components = calender.dateComponents([.hour, .minute, .second], from: nowDate, to: referenceDate)
return String(format: "%02dh:%02dm:%02ds",
components.hour ?? 00,
components.minute ?? 00,
components.second ?? 00)
}
#State var nowDate: Date = Date()
let referenceDate: Date = Date().addingTimeInterval(20)
let calender = Calendar(identifier: .gregorian)
let formatter = DateFormatter()
var timer: Timer {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) {_ in
self.nowDate = Date()
}
}
var body: some View {
Text(countDownString(from: referenceDate, until: nowDate)).font(.largeTitle).onAppear(perform: {
_ = self.timer
})
}
}

Updating Time Only No Date Information

struct CurrentDateView : View {
#State var now = Date()
let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
var body: some View {
Text("\(now)")
.onReceive(timer) {
self.now = Date()
}
}
}
This code is perfect, but I need the the Date format removed. I just need the HH:MM:SS showing
You can use a DateFormatter with Swift 5's string interpolation.
struct ContentView: View { #State var now = Date()
static let timeFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "h:mm:ss a zz"
return formatter
}()
let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
var body: some View {
Text("\(now, formatter: Self.timeFormat)")
.onReceive(timer) {_ in
self.now = Date()
}
}
}

how to stop print time and timer function using a button

I'm working on a countdown timer that allows the user to select a future trip departure date & time, hit a start button to start the countdown, then hit a reset button to invalidate the timer and allow the user to select a different departure date and time, then start again.
everything works pretty well, except for the reset button. the timer invalidates briefly, but then continues the countdown, and the days, minutes, hours, seconds labels continue to update.
I thought that invalidating the timer would freeze values, or I could set all days, minutes, hours, seconds labels to "", and then start button would begin a new countdown. no such luck. right after selecting a new departure date/time, countdown begins, even if start button isn't pressed.
I'm looking for a way to stop the printTime and runTimer functions. thanks for the help.
here's the code:
import UIKit
class ViewController: UIViewController {
var resetBtnStatus: Bool = false
var timer = Timer()
let userCalendar = Calendar.current
let requestedComponent: Set<Calendar.Component> = [.day,.hour,.minute,.second]
var departureDateTime: Date?
#IBOutlet weak var departureDateTimePicker: UIDatePicker!
#IBAction func departureDateTimePicker(_ sender: Any) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yy H:mm:ss"
departureDateTimePicker.addTarget(self, action: #selector(handler), for: UIControlEvents.valueChanged)
}
#IBOutlet weak var daysLabel: UILabel!
#IBOutlet weak var hoursLabel: UILabel!
#IBOutlet weak var minutesLabel: UILabel!
#IBOutlet weak var secondsLabel: UILabel!
func handler(sender: UIDatePicker) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yy H:mm:ss"
let departureDateTimeString = dateFormatter.string(from: departureDateTimePicker.date)
departureDateTime = dateFormatter.date(from: departureDateTimeString)
}
#IBAction func resetBtnPressed(_ sender: Any) {
if resetBtn.isSelected == true {
resetBtnStatus = true
}
departureDateTimePicker.isHidden = false
startBtn.isHidden = false
// let dateFormatter = DateFormatter()
// dateFormatter.dateFormat = "MM/dd/yy H:mm:ss"
// let startTime = Date()
// let endTime = startTime
// let timeDifference = userCalendar.dateComponents(requestedComponent, from: startTime, to: endTime)
// daysLabel.text = "\(timeDifference.day!) Days"
// hoursLabel.text = "\(timeDifference.hour!) Hours"
// minutesLabel.text = "\(timeDifference.minute!) Minutes"
// secondsLabel.text = "\(timeDifference.second!) Seconds"
}
// #IBAction func resetBtnPressed(_ sender: Any) {
// if resetBtn.isSelected == true {
// resetBtnStatus = true
// }
// departureDateTimePicker.isHidden = false
// startBtn.isHidden = false
// // let dateFormatter = DateFormatter()
// // dateFormatter.dateFormat = "MM/dd/yy H:mm:ss"
// // let startTime = Date()
// // let endTime = startTime
// // let timeDifference = userCalendar.dateComponents(requestedComponent, from: startTime, to: endTime)
// // daysLabel.text = "\(timeDifference.day!) Days"
// // hoursLabel.text = "\(timeDifference.hour!) Hours"
// // minutesLabel.text = "\(timeDifference.minute!) Minutes"
// // secondsLabel.text = "\(timeDifference.second!) Seconds"
// }
func printTime() {
if resetBtnStatus == false {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yy H:mm:ss"
let startTime = Date()
let endTime = departureDateTime
let timeDifference = userCalendar.dateComponents(requestedComponent, from: startTime, to: endTime!)
let startTimeDouble: Double = startTime.timeIntervalSinceReferenceDate
var endTimeDouble: Double?
endTimeDouble = (endTime?.timeIntervalSinceReferenceDate)
daysLabel.text = "\(timeDifference.day!) Days"
hoursLabel.text = "\(timeDifference.hour!) Hours"
minutesLabel.text = "\(timeDifference.minute!) Minutes"
secondsLabel.text = "\(timeDifference.second!) Seconds"
if endTimeDouble! > startTimeDouble {
daysLabel.text = "\(timeDifference.day!) Days"
hoursLabel.text = "\(timeDifference.hour!) Hours"
minutesLabel.text = "\(timeDifference.minute!) Minutes"
secondsLabel.text = "\(timeDifference.second!) Seconds"
}
else {
timer.invalidate()
daysLabel.text = ""
hoursLabel.text = ""
minutesLabel.text = ""
secondsLabel.text = ""
}
} else {
timer.invalidate()
daysLabel.text = ""
hoursLabel.text = ""
minutesLabel.text = ""
secondsLabel.text = ""
}
}
func runTimer() {
if resetBtnStatus == false {
let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(printTime), userInfo: nil, repeats: true)
timer.fire()
}
if resetBtnStatus == true {
timer.invalidate()
}
}
#IBOutlet weak var startBtn: UIButton!
#IBAction func startBtnPressed(_ sender: Any) {
handler(sender: departureDateTimePicker)
printTime()
runTimer()
departureDateTimePicker.isHidden = true
startBtn.isHidden = true
}
#IBOutlet weak var resetBtn: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

Resources