How to update text using timer in SwiftUI - swift

I have text in view now i want to update that text using alert on every second.
Here is code i have done.
struct CountDownView : View {
var body: some View {
VStack{
Text("Update text with timer").lineLimit(nil).padding(20)
}.navigationBarTitle(Text("WWDC"), displayMode:.automatic)
}
}

Using Combine:
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()
}
}
}

i have managed to update text using alert.
i have declared date as State so whenever date is changed using alert text will also get updated.
struct CurrentDateView : View {
#State var newDate = Date()
let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
var body: some View {
Text("\(newDate)")
.onReceive(timer) {
self.newDate = Date()
}
}
}

the original sample was at:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-a-timer-with-swiftui
I do use this approach to update from one of my calls periodically...
see also at:
https://developer.apple.com/documentation/combine/replacing-foundation-timers-with-timer-publishers
struct ContentView: View {
#State var msg = ""
var body: some View {
let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
Text(msg)
.onReceive(timer) { input in
msg = MyManager.shared.ifo ?? "not yet received"
}
}
}
I do I haver to call network in other ways, but here I simply call periodically some of my managers.
To stop timer (as in link from hackingSwift..) You can use:
self.timer.upstream.connect().cancel()

Related

Timer publisher init timer after button click

In all tutorials and in official documentation I only see initialization of timer straight up when the view loads.
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
and later on
.onReceive(timer) {....}
but how should I init timer only on button click and assign it to the unassigned / not connected timer.
Later on I will need to cancel and reconnect, but that is not the issue here. Issue here is connecting only after button click.
I tried to init it like this
#State private var timer: Cancellable?
....
timer = Timer.publish(every: 1, on: .main, in: .common).connect()
But I can not call onReceive on timer inited like this, because first:
Protocol 'Cancellable' as a type cannot conform to 'Publisher'
and second
Argument type 'Cancellable?' does not conform to expected type 'Cancellable'
Just put the timer in a child view and control its visibility with a bool. When the TimerView is removed the state is destroyed and the timer stops.
struct ContentView: View {
#State var started = false
var body: some View {
VStack {
Button(started ? "Stop" : "Start") {
started.toggle()
}
if started {
TimerView()
}
}
}
}
struct TimerView: View {
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
...
Here is demo of possible approach - timer publisher is created with button (same autoconnected), but subscriber should be registered conditionally, because timer in such case is an optional.
Tested with Xcode 13.4 / iOS 15.5
struct ContentView: View {
#State private var timer: Publishers.Autoconnect<Timer.TimerPublisher>? // << here !!
#State private var value = 10 // just demo
var body: some View {
Button {
if timer != nil {
timer = nil // << reset !!
value = 10
} else {
timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() // << create !!
}
} label: {
Text("Toggle")
}
if timer != nil { // << verify !!
Text("Counter: \(value)")
.font(.largeTitle)
.onReceive(timer!) { _ in // << observe !!
value = value == 0 ? 10 : value - 1
}
}
}
}

Save a value at closure and restore the value at reopening

I am currently learning swiftui and have the following problem:
My code contains a timer that counts up when the app is opened. This works fine so far. Now I want the previous time to be saved when the app is closed and when it is reopened, the value is loaded and counted up from there.
Is there a simple way to implement this?
Here my code:
struct TimeView: View {
#State private var timeTracker = 0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
HStack{
Text("\(timeTracker) s")
}
.onReceive(timer) { time in
self.timeTracker += 1
}
}
}
struct TimeView_Previews: PreviewProvider {
static var previews: some View {
TimeView()
}
}
struct TimeView: View {
#State private var timeTracker = 0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#AppStorage("TIME") var time: Int = 0
var body: some View {
HStack{
Text("\(timeTracker) s")
}
.onReceive(timer) { _ in timeTracker += 1 }
.onAppear(perform: { timeTracker = time })
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in time = timeTracker }
}
}
//OR
struct TimeView: View {
#AppStorage("TIME") var timeTracker: Int = 0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
HStack{
Text("\(timeTracker) s")
}
.onReceive(timer) { _ in timeTracker += 1 }
}
}

How to set up multiple combine timer publishers?

Here I have a simple SwiftUI project which has two timer publishers which are set to fire every 1 second and 2 seconds respectively.
The expected behavior is that the first label would update every second and the second label would update every 2 seconds. However what actually happens is only the first label updates every second and the second label remains at 0 indefinitely.
I know it's possible to make multiple timers using Timer.scheduledTimer(withTimeInterval:) by simply making new variables of those timer instances but it doesn't seem to work the same way with these publishers.
How can I make both timers work?
import SwiftUI
struct ContentView: View {
#State private var counter1 = 0
#State private var counter2 = 0
let timer1 = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
let timer2 = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
var body: some View {
VStack(spacing: 30) {
Text(String(counter1))
.frame(width: 50, height: 20)
Text(String(counter2))
.frame(width: 50, height: 20)
}
.padding()
.onReceive(timer1) {_ in
counter1 += 1
}
.onReceive(timer2) {_ in
counter2 += 2
}
}
}
In SwiftUI your view is a transitory struct. Ie the system remakes the views all the time and throws away the old ones. The View, therefore should not be holding the references to the timers (you will be making new timers and dropping old ones all the time). Make a viewModel and store the values there. In general don't store things on the view that are not in a property wrapper or passed in through the initializer.
final class ViewModel: ObservableObject {
#Published private(set) var first = 0
#Published private(set) var second = 0
private var subscriptions: Set<AnyCancellable> = []
func start() {
Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.scan(0) { accumulated, _ in accumulated + 1 }
.assign(to: \.first, on: self)
.store(in: &subscriptions)
Timer.publish(every: 2, on: .main, in: .common)
.autoconnect()
.scan(0) { accumulated, _ in accumulated + 1 }
.assign(to: \.second, on: self)
.store(in: &subscriptions)
}
func stop() {
subscriptions.removeAll()
}
}
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
Text(String(describing: viewModel.first))
Text(String(describing: viewModel.second))
}
.onAppear { viewModel.start() }
.onDisappear { viewModel.stop() }
}
}

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 - How to Make A Start/Stop Timer

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()
}
}