In SwiftUI I'm noting to use a Timer that:
Try 1 - This doesn't work as get "Use of unresolved identifier 'self'"
var timer2: Timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
self.angle = self.angle + .degrees(1)
}
Try 2 - Works, but have to put in an "_ = self.timer" to start it later
var timer: Timer {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) {_ in
self.angle = self.angle + .degrees(1)
}
}
// then after need to use " .onAppear(perform: {_ = self.timer}) "
Is there a way to get my Try1 working? That is where in a SwiftUI file I can create the timer up front? Or actually where in SwiftUI would one normally start and stop the timer? i.e. where are lifecycle methods
Whole file:
import SwiftUI
struct ContentView : View {
#State var angle: Angle = .degrees(55)
// var timer2: Timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
// self.angle = self.angle + .degrees(1)
// }
var timer: Timer {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) {_ in
self.angle = self.angle + .degrees(1)
}
}
private func buttonAction() {
print("test")
self.angle = self.angle + .degrees(5)
}
var body: some View {
VStack{
Text("Start")
ZStack {
Circle()
.fill(Color.blue)
.frame(
width: .init(integerLiteral: 100),
height: .init(integerLiteral: 100)
)
Rectangle()
.fill(Color.green)
.frame(width: 20, height: 100)
// .rotationEffect(Angle(degrees: 25.0))
.rotationEffect(self.angle)
}
Button(action: self.buttonAction) {
Text("CLICK HERE")
}
Text("End")
}
.onAppear(perform: {_ = self.timer})
}
}
It isn't clear to me that you need a timer for your example, but since there is a great deal of misinformation out there about how to include a Timer in a SwiftUI app, I'll demonstrate.
The key is to put the timer elsewhere and publish each time it fires. We can easily do this by adding a class that holds the timer as a bindable object to our environment (note that you will need to import Combine):
class TimerHolder : BindableObject {
var timer : Timer!
let didChange = PassthroughSubject<TimerHolder,Never>()
var count = 0 {
didSet {
self.didChange.send(self)
}
}
func start() {
self.timer?.invalidate()
self.count = 0
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
[weak self] _ in
guard let self = self else { return }
self.count += 1
}
}
}
We will need to pass an instance of this class down through the environment, so we modify the scene delegate:
window.rootViewController =
UIHostingController(rootView: ContentView())
becomes
window.rootViewController =
UIHostingController(rootView: ContentView()
.environmentObject(TimerHolder()))
Finally, lets put up some UI that starts the timer and displays the count, to prove that it is working:
struct ContentView : View {
#EnvironmentObject var timerHolder : TimerHolder
var body: some View {
VStack {
Button("Start Timer") { self.timerHolder.start() }
Text(String(self.timerHolder.count))
}
}
}
EDIT Update for those who have not been following the plot: BindableObject has migrated into ObservableObject, and there is no need any more for manual signalling. So:
class TimerHolder : ObservableObject {
var timer : Timer!
#Published var count = 0
// ... and the rest is as before ...
Based on matt's answer above, I think there is a less complicated version possible.
class TimerHolder : ObservableObject {
var timer : Timer!
#Published var count = 0
func start() {
self.timer?.invalidate()
self.count = 0
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
_ in
self.count += 1
print(self.count)
}
}
}
With : ObservableObjectand #Publish var count
and in the View
#ObservedObject var durationTimer = TimerHolder()
...
Text("\(durationTimer.count) Seconds").onAppear {
self.durationTimer.start()
}
you can observe any change to the count variable and update the Text in the view accordingly.
As mentioned in other comments, make sure though, that the timer is not restarted unexpectedly due to an action recreating the view.
Related
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()
}
}
My goal is to create a view in SwiftUI that starts with 0. When you press the view, a timer should start counting upwards, and tapping again stops the timer. Finally, when you tap again to start the timer, the timer should begin at 0.
Here is my current code:
import SwiftUI
struct TimerView: View {
#State var isTimerRunning = false
#State private var endTime = Date()
#State private var startTime = Date()
let timer = Timer.publish(every: 0.001, on: .main, in: .common).autoconnect()
var tap: some Gesture {
TapGesture(count: 1)
.onEnded({
isTimerRunning.toggle()
})
}
var body: some View {
Text("\(endTime.timeIntervalSince1970 - startTime.timeIntervalSince1970)")
.font(.largeTitle)
.gesture(tap)
.onReceive(timer) { input in
startTime = isTimerRunning ? startTime : Date()
endTime = isTimerRunning ? input : endTime
}
}
}
This code causes the timer to start instantly and never stop, even when I tap on it. The timer also goes backward (into negative numbers) rather than forward.
Can someone please help me understand what I am doing wrong? Also, I would like to know if this is a good overall strategy for a timer (using Timer.publish).
Thank you!
Here is a fixed version. Take a look at the changes I made.
.onReceive now updates a timerString if the timer is running. The timeString is the interval between now (ie. Date()) and the startTime.
Tapping on the timer sets the startTime if it isn't running.
struct TimerView: View {
#State var isTimerRunning = false
#State private var startTime = Date()
#State private var timerString = "0.00"
let timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
var body: some View {
Text(self.timerString)
.font(Font.system(.largeTitle, design: .monospaced))
.onReceive(timer) { _ in
if self.isTimerRunning {
timerString = String(format: "%.2f", (Date().timeIntervalSince( self.startTime)))
}
}
.onTapGesture {
if !isTimerRunning {
timerString = "0.00"
startTime = Date()
}
isTimerRunning.toggle()
}
}
}
The above version, while simple, bugs me that the Timer is publishing all the time. We only need the Timer publishing when the timer is running.
Here is a version that starts and stops the Timer:
struct TimerView: View {
#State var isTimerRunning = false
#State private var startTime = Date()
#State private var timerString = "0.00"
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text(self.timerString)
.font(Font.system(.largeTitle, design: .monospaced))
.onReceive(timer) { _ in
if self.isTimerRunning {
timerString = String(format: "%.2f", (Date().timeIntervalSince( self.startTime)))
}
}
.onTapGesture {
if isTimerRunning {
// stop UI updates
self.stopTimer()
} else {
timerString = "0.00"
startTime = Date()
// start UI updates
self.startTimer()
}
isTimerRunning.toggle()
}
.onAppear() {
// no need for UI updates at startup
self.stopTimer()
}
}
func stopTimer() {
self.timer.upstream.connect().cancel()
}
func startTimer() {
self.timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
}
}
Stop-watch Timer
The following approach allows you create a start/stop/reset SwiftUI Timer using #Published and #ObservedObject property wrappers, along with the ObservableObject protocol.
Here's the ContentView structure:
import SwiftUI
struct ContentView: View {
#ObservedObject var stopWatch = Stop_Watch()
var body: some View {
let minutes = String(format: "%02d", stopWatch.counter / 60)
let seconds = String(format: "%02d", stopWatch.counter % 60)
let union = minutes + " : " + seconds
ZStack {
Color.black.ignoresSafeArea()
VStack {
Spacer()
HStack {
Button("Start") { self.stopWatch.start() }
.foregroundColor(.purple)
Button("Stop") { self.stopWatch.stop() }
.foregroundColor(.orange)
Button("Reset") { self.stopWatch.reset() }
.foregroundColor(.yellow)
}
Spacer()
Text("\(union)")
.foregroundColor(.teal)
.font(.custom("", size: 90))
Spacer()
}
}
}
}
...and Stop_Watch class:
class Stop_Watch: ObservableObject {
#Published var counter: Int = 0
var timer = Timer()
func start() {
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0,
repeats: true) { _ in
self.counter += 1
}
}
func stop() {
self.timer.invalidate()
}
func reset() {
self.counter = 0
self.timer.invalidate()
}
}
Updated for Swift 5.7 and iOS 16 to display a timer that counts up seconds and minutes like a simple stopwatch. Using DateComponentsFormatter to format the minutes and seconds.
struct StopWatchView: View {
#State var isRunning = false
#State private var startTime = Date()
#State private var display = "00:00"
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text(display)
.font(.system(size: 20, weight: isRunning ? .bold : .light, design: .monospaced))
.foregroundColor(.accentColor)
.onReceive(timer) { _ in
if isRunning {
let duration = Date().timeIntervalSince(startTime)
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .pad
display = formatter.string(from: duration) ?? ""
}
}
.onTapGesture {
if isRunning {
stop()
} else {
display = "00:00"
startTime = Date()
start()
}
isRunning.toggle()
}
.onAppear {
stop()
}
}
func stop() {
timer.upstream.connect().cancel()
}
func start() {
timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
}
}
struct StopWatchView_Previews: PreviewProvider {
static var previews: some View {
StopWatchView()
}
}
I have a view representing a timer. It looks like this...
Video with animation...
Video without animation...
As you can see, it has a play/pause button, text for the name, and text for the remaining time. Most importantly, the background represents the percentage of the timer that has elapsed.
When run in a preview, the animation runs smoothly. When run anywhere else, the background jumps to each interval point with no animation.
Here is the background shape…
struct Meter: Shape, Animatable {
init(widthPercentage: CGFloat) {
self.widthPercentage = max(0, widthPercentage)
}
var widthPercentage: CGFloat
var animatableData: CGFloat {
get { widthPercentage }
set { self.widthPercentage = newValue }
}
func path(in rect: CGRect) -> Path {
// create the shape…
}
}
Here is the timer view....
struct TimerRow: View {
#ObservedObject var timer: ReusableTimer
var body: some View {
ZStack(alignment: .leading) {
Meter(widthPercentage: CGFloat(timer.progress))
.foregroundColor(.gray)
.opacity(0.20)
.animation(.linear(duration: 0.01))
HStack {
// Other views...
}
}
}
}
Here is the preview...
struct TimerRow_Previews: PreviewProvider {
static var previews: some View {
TimerRow(timer: ReusableTimer(name: "Timer1",
duration: 10,
timeInterval: 0.01))
.previewLayout(.fixed(width: 400, height: 50))
}
}
Notice that the animation duration in TimerRow is set to match the interval of the timer in the preview.
Here is only relevant parts of ReusableTimer...
class ReusableTimer: ObservableObject, Identifiable {
let id: Int
let name: String
#Published private(set) var counter: TimeInterval = 0
var isRunning: Bool { timer != nil }
private var timer: Timer?
let duration: TimeInterval
private let timeInterval: TimeInterval
func start() {
// ...do some other stuff
timer = Timer(timeInterval: timeInterval, repeats: true) { [weak self] _ in
guard let this = self else { return }
this.counter += this.timeInterval // increment the counter
if this.counter >= this.duration { this.removeTimer() }
}
RunLoop.current.add(timer!, forMode: RunLoop.Mode.default)
}
func pause() {
removeTimer()
}
private func removeTimer() {
// Remove the timer
}
}
90% of the time, I ask the wrong question. My duration gave the appearance that there was no animation taking place, but there was... It was just really fast. So I corrected it to .animation(.linear(duration: 1)).
Why did it look perfect in the preview with the incorrect value? That's still a mystery, but it now works.
In my view model, if I update an NSManagedObject property, then the view won't update anymore. I have attached the code and the view model.
I added a comment in front of the line that breaks the view update.
class StudySessionCounterViewModel: ObservableObject {
fileprivate var studySession: StudySession
init(_ studySession: StudySession) {
self.studySession = studySession
}
#Published var elapsedTime = 14 * 60
#Published var circleProgress: Double = 0
var timer: Timer?
var formattedTime: String {
get {
let timeToFormat = (Int(studySession.studyTime) * 60) - elapsedTime
let minutes = timeToFormat / 60 % 60
let seconds = timeToFormat % 60
return String(format:"%02i:%02i", minutes, seconds)
}
}
func startTimer() {
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.timerTicked), userInfo: nil, repeats: true)
studySession.isActive = true //Adding this stops my view from updating
}
#objc func timerTicked() {
elapsedTime += 1
circleProgress = (Double(elapsedTime) / Double(studySession.studyTime * 60))
}
func stop() {
timer?.invalidate()
}
}
This is the view that uses that view model. When adding that line, the text that represents the formatted time no longer changes and the progress circle's progress remain the same.
If I remove the line, everything updates and work as expected.
struct StudySessionCounterView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var studySessionCounterVM: StudySessionCounterViewModel
var studySession: StudySession
init(_ studySession: StudySession) {
studySessionCounterVM = StudySessionCounterViewModel(studySession)
self.studySession = studySession
}
#State var showAlert = false
#State var isCounting = false
var body: some View {
VStack {
ZStack {
Text(studySessionCounterVM.formattedTime)
.font(.largeTitle)
ProgressRingView(size: .large, progress: $studySessionCounterVM.circleProgress)
}
Spacer()
if isCounting {
Button(action: {
self.studySessionCounterVM.stop()
self.isCounting = false
}) {
Image(systemName: "stop.circle")
.resizable()
.frame(width: 64, height: 64, alignment: .center)
.foregroundColor(.orange)
}
} else {
Button(action: {
self.studySessionCounterVM.startTimer()
self.isCounting = true
}) {
Image(systemName: "play.circle")
.resizable()
.frame(width: 64, height: 64, alignment: .center)
.foregroundColor(.orange)
}
}
}.padding()
.navigationBarTitle("Matematica", displayMode: .inline)
}
}
UPDATE: Found out that each time the NSManagedObject changes a property, the view model gets reinitialised. Still no solution, unfortunately
I am quite new to SwiftUI. I have a following "Counter" view that counts up every second. I want to "reset" the counter when the colour is changed:
struct MyCounter : View {
let color: Color
#State private var count = 0
init(color:Color) {
self.color = color
_count = State(initialValue: 0)
}
var body: some View {
Text("\(count)").foregroundColor(color)
.onAppear(){
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.count = self.count + 1 }
}
}
}
Here is my main view that uses counter:
struct ContentView: View {
#State var black = true
var body: some View {
VStack {
MyCounter(color: black ? Color.black : Color.yellow)
Button(action:{self.black.toggle()}) { Text("Toggle") }
}
}
}
When i click "Toggle" button, i see MyCounter constructor being called, but #State counter persists and never resets. So my question is how do I reset this #State value? Please note that I do not wish to use counter as #Binding and manage that in the parent view, but rather MyCounter be a self-contained widget. (this is a simplified example. the real widget I am creating is a sprite animator that performs sprite animations, and when I swap the image, i want the animator to start from frame 0). Thanks!
There are two way you can solve this issue. One is to use a binding, like E.Coms explained, which is the easiest way to solve your problem.
Alternatively, you could try using an ObservableObject as a view model for your timer. This is the more flexible solution. The timer can be passed around and it could also be injected as an environment object if you so desire.
class TimerModel: ObservableObject {
// The #Published property wrapper ensures that objectWillChange signals are automatically emitted.
#Published var count: Int = 0
init() {}
func start() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.count = self.count + 1 }
}
func reset() {
count = 0
}
}
Your timer view then becomes
struct MyCounter : View {
let color: Color
#ObservedObject var timer: TimerModel
init(color: Color, timer: TimerModel) {
self.color = color
self.timer = timer
}
var body: some View {
Text("\(timer.count)").foregroundColor(color)
.onAppear(){
self.timer.start()
}
}
}
Your content view becomes
struct ContentView: View {
#State var black = true
#ObservedObject var timer = TimerModel()
var body: some View {
VStack {
MyCounter(color: black ? Color.black : Color.yellow, timer: self.timer)
Button(action: {
self.black.toggle()
self.timer.reset()
}) {
Text("Toggle")
}
}
}
}
The advantage of using an observable object is that you can then keep track of your timer better. You could add a stop() method to your model, which invalidates the timer and you can call it in a onDisappear block of your view.
One thing that you have to be careful about this approach is that when you're using the timer in a standalone fashion, where you create it in a view builder closure with MyCounter(color: ..., timer: TimerModel()), every time the view is rerendered, the timer model is replaced, so you have to make sure to keep the model around somehow.
You need a binding var:
struct MyCounter : View {
let color: Color
#Binding var count: Int
var body: some View {
Text("\(count)").foregroundColor(color)
.onAppear(){
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.count = self.count + 1 }
}
}
}
struct ContentView: View {
#State var black = true
#State var count : Int = 0
var body: some View {
VStack {
MyCounter(color: black ? Color.black : Color.yellow , count: $count)
Button(action:{self.black.toggle()
self.count = 0
}) { Text("Toggle") }
}
}
}
Also you can just add one State Value innerColor to help you if you don't like binding.
struct MyCounter : View {
let color: Color
#State private var count: Int = 0
#State private var innerColor: Color?
init(color: Color) {
self.color = color
}
var body: some View {
return Text("\(self.count)")
.onAppear(){
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.count = self.count + 1 }
}.foregroundColor(color).onReceive(Just(color), perform: { color in
if self.innerColor != self.color {
self.count = 0
self.innerColor = color}
})
}
}