In a Live Activity or in a Dynamic Island is easy to create a timer. For instance:
Text(Date(timeIntervalSinceNow: 60), style: .timer)
will create a 60 seconds timer counting from when the code was first run.
What I could not do is create a repeating timer, where when it gets to 00:00 it would reset back to the initial value (60 seconds in the example) ad infinitum.
Use onReceive to measure the value every second and set it back to 60 seconds when it becomes 0.
import SwiftUI
struct ContentView: View {
#State var timeRemaining = 60
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("\(timeRemaining)")
.onReceive(timer) { _ in
if self.timeRemaining > 0 {
self.timeRemaining -= 1
} else {
timeRemaining = 60
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Related
Can I add working Progress View to new Live Activities?
I'm trying to initiate timer with this code, but it doesn't work, view just sleep:
#State private var progress = 100.0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
ProgressView(value: progress, total: 100)
.onReceive(timer, perform: { _ in
if progress > 100 {
progress -= 100
}
})
ProgressView(timerInterval: <#T##ClosedRange<Date>#>, label: <#T##() -> _#>, currentValueLabel: <#T##() -> _#>)
With timeInterval it is works, but I need to customize
I would like to pass a timer from ContentView to SecondView, but I don't know how to manage it because I never used it before.
Can someone figure this out for me?
ContentView
struct ContentView: View {
#State private var timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()
#State private var timeRemaining = 10
var body: some View {
NavigationView {
VStack {
Text("\(timeRemaining)")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
NavigationLink {
SecondView(timer: ???) // <-- What should i pass here?
} label: {
Text("Change View")
}
}
}
}
}
SecondView
struct SecondView: View {
#Binding var timer: ??? // <-- What type?
#State private var timeRemaining = 5
var body: some View {
Text("Hello")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView(timer: ???) // <-- Same thing here in SecondView preview
}
}
With this timer declaration you are in the Combine world. Combine is the reactive framework from Apple.
First you would need to import it:
import Combine
I have commented the code but Combine is a far field and it probably would be best to read the documentation about it, read some tutorials and try some things out.
documentation
struct ContentView: View {
// The typ here is Publishers.Autoconnect<Timer.TimerPublisher>
// But we can erase it and the result will be a Publisher that emits a date and never throws an error: AnyPublisher<Date,Never>
#State private var timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common)
.autoconnect()
.eraseToAnyPublisher()
#State private var timeRemaining = 10
var body: some View {
NavigationView {
VStack {
Text("\(timeRemaining)")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
NavigationLink {
// pass the publisher on
SecondView(timer: timer)
} label: {
Text("Change View")
}
}
}
}
}
struct SecondView: View {
//You donĀ“t need binding here as this view never manipulates this publisher
var timer: AnyPublisher<Date,Never>
#State private var timeRemaining = 5
var body: some View {
Text("Hello")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
print(timeRemaining)
}
}
}
}
struct SecondView_Previews: PreviewProvider {
// Creating a static private var should work here !not tested!
#State static private var timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common)
.autoconnect()
.eraseToAnyPublisher()
static var previews: some View {
SecondView(timer: timer)
}
}
You could simply inject the timer publisher, as suggested above, but there may be an even simpler solution:
FirstView is already updating with every tick of the timer. You could simply pass a timeRemaning binding to your second view and then it too would just update with every tick of the timer (because timeRemaining changes on each tick). You can then observe and react to changes of timeRemaining using .onChange(of:):
struct SecondView: View {
#Binding var timeRemaining: TimeInterval
var body: some View {
Text("Hello")
.onChange(of: timeRemaining) {
if $0 < 0 {
timeRemaining = -1
}
}
}
}
You don't need to pass a binding, Since you are not mutating timer of contentview from the second view. You can just pass the reference to the timer publisher and then subscribe to it using .onReceive().
import Combine // here
struct ContentView: View {
let timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect().eraseToAnyPublisher() //<= Here
#State private var timeRemaining = 10
var body: some View {
NavigationView {
VStack {
Text("\(timeRemaining)")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
NavigationLink {
SecondView(timer: timer)
} label: {
Text("Change View")
}
}
}
}
}
struct SecondView: View {
let timer: AnyPublisher<Date, Never> // Here
#State private var timeRemaining = 5
var body: some View {
VStack {
Text("Hello")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
Text("time remaining \(timeRemaining)")
}
}
}
I want to animate a text that starts by default from 0, to a variable.
For example, for x = 80, I want my text to display all the numbers between 0 and 80 very fast, until it hits 80.
I found examples with progress indicators, but I cannot apply the methods to this.
Do you have any ideas for doing this?
Thanks, Diocrasis.
Here I've created a little function called runCounter which takes a binding to the counter variable, a start value, the end value, and the speed. When called, it sets the bound variable to the start value, and then starts a Timer which runs every speed seconds and increments the counter until it reaches end at which point it invalidates the timer.
This standalone example shows two counters running at different speeds, both of which start when they first appear using .onAppear().
struct ContentView: View {
#State private var counter1 = 0
#State private var counter2 = 0
var body: some View {
VStack {
Text("\(self.counter1)")
.onAppear {
self.runCounter(counter: self.$counter1, start: 0, end: 80, speed: 0.05)
}
Text("\(self.counter2)")
.onAppear {
self.runCounter(counter: self.$counter2, start: 0, end: 10, speed: 0.5)
}
}
}
func runCounter(counter: Binding<Int>, start: Int, end: Int, speed: Double) {
counter.wrappedValue = start
Timer.scheduledTimer(withTimeInterval: speed, repeats: true) { timer in
counter.wrappedValue += 1
if counter.wrappedValue == end {
timer.invalidate()
}
}
}
}
You can use a Timer.Publisher to trigger incrementing of your counter at regular intervals.
To stop incrementing once you reach your desired count, whenever your Timer fires, you can check if count has reached end, if not, increment it, otherwise remove the subscription and hence stop incrementing.
class Counter: ObservableObject {
#Published var count = 0
let end: Int
private var timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
private var subscriptions = Set<AnyCancellable>()
init(end: Int) {
self.end = end
}
func start() {
timer.sink { [weak self] _ in
guard let self = self else { return }
if self.count <=self.end {
self.count += 1
} else {
self.subscriptions.removeAll()
}
}.store(in: &subscriptions)
}
}
struct AnimatedText: View {
#ObservedObject var counter: Counter
var body: some View {
Text("\(counter.count)")
.onAppear() {
self.counter.start()
}
}
}
struct AnimatedText_Previews: PreviewProvider {
static var previews: some View {
AnimatedText(counter: Counter(end: 80))
}
}
Adding to the answer by #vacawama.
func runCounter(counter: Binding<Int>, start: Int, end: Int, speed: Double) {
let maxSteps = 20
counter.wrappedValue = start
let steps = min(abs(end), maxSteps)
var increment = 1
if steps == maxSteps {increment = end/maxSteps}
Timer.scheduledTimer(withTimeInterval: speed, repeats: true) { timer in
counter.wrappedValue += increment
if counter.wrappedValue >= end {
counter.wrappedValue = end
timer.invalidate()
}
}
}
I am trying to achieve a navigation to another view when timer hits a specific time. For example I want to navigate to another view after 5 minutes. In swift i can easily achieve this but I am new to SwiftUI and little help will be highly appreciated.
My code:
struct TwitterWebView: View {
#State var timerTime : Float
#State var minute: Float = 0.0
#State private var showLinkTarget = false
let timer = Timer.publish(every: 60.0, on: .main, in: .common).autoconnect()
var body: some View {
WebView(url: "https://twitter.com/")
.navigationBarTitle("")
.navigationBarHidden(true)
.onReceive(timer) { _ in
if self.minute == self.timerTime {
print("Timer completed navigate to Break view")
NavigationLink(destination: BreakView()) {
Text("Test")
}
self.timer.upstream.connect().cancel()
} else {
self.minute += 1.0
}
}
}
}
Here is possible approach (of course assuming that TwitterWebView has NavigationView somewhere in parents)
struct TwitterWebView: View {
#State var timerTime : Float
#State var minute: Float = 0.0
#State private var showLinkTarget = false
let timer = Timer.publish(every: 60.0, on: .main, in: .common).autoconnect()
#State private var shouldNavigate = false
var body: some View {
WebView(url: "https://twitter.com/")
.navigationBarTitle("")
.navigationBarHidden(true)
.background(
NavigationLink(destination: BreakView(),
isActive: $shouldNavigate) { EmptyView() }
)
.onReceive(timer) { _ in
if self.minute == self.timerTime {
print("Timer completed navigate to Break view")
self.timer.upstream.connect().cancel()
self.shouldNavigate = true // << here
} else {
self.minute += 1.0
}
}
}
}
In my top level view, I have declared a timer like so:
Struct ContentView: View {
#State var timer = Timer.publish(every: 1, on: .main, in:
.common).autoconnect()
var body: some View {
ZStack {
if self.timerMode == .warmup {
WarmupView(
timer: $timer
)
if self.timerMode == .work {
WorkView(
timer: $timer
)
}
}
}
In a child view, I want to be able to access and update this timer, which will serve as the single source of truth.
Struct WarmupView: View {
#Binding var timer: Publishers.Autoconnect<Timer.TimerPublisher>
#Binding var timeRemaining: Int
var body: some View {
VStack {
Text("\(self.timeRemaining)").font(.system(size: 160))
.onReceive(self.timer) { _ in
if self.timeRemaining > 0 {
self.timeRemaining -= 1
}
}
}
}
}
The timer is publishing to the warmup view without issues, but when timerMode is updated to .work (which has nearly identical code) and the view changes, the timer stops publishing.
Simple as changing the type of your #Binding var timer in your WarmupView to Publishers.Autoconnect<Timer.TimerPublisher>. The .autoconnect() wraps the timer publisher in another publisher, which changes the type.
Here's a simplified version of your code:
struct ContentView: View {
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State var remaining = 100
var body: some View {
Text("\(remaining)")
.font(.system(size: 160))
.onReceive(timer) { _ in
if self.remaining > 0 {
self.remaining -= 1
}
}
}
}