Add ProgressView to Live Activity - ios

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

Related

Create a repeating timer for a Live Activity

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

SwiftUI doesn't refresh until button action finishes

I want the view to be updated every time #State list gets updated.
But when I use Button to trigger a consecutive update, the list gets updated only after all the consecutive updates are finished.
This code updates the list 10 seconds after the button is pressed.
What should I do to make this code update every second?
#State var i_list: [Int] = []
var body: some View {
VStack {
Button(action: {
for i in 0..<10 {
i_list.append(i)
do {
sleep(1)
}
}
}) {
Text(button_text)
}
List (i_list, id: \.self){ i in
Text(String(i))
}
}
}
Solved:
I solved this problem by updating the list in a different thread.
let globalQueue = DispatchQueue.global()
globalQueue.async {
for i in 0..<10 {
DispatchQueue.main.async {
i_list.append(i)
}
do {
sleep(1)
}
}
}
If you want to see your view updates immediately, instead of using a for loop to update your array, use a publisher. Here I use a timer and add a onReceive(timer) on the List
struct ContentView : View {
#State var i_list: [Int] = []
#State var currentNumber = 0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
List (i_list, id: \.self) { i in
Text(String(i))
}.onReceive(timer) { _ in
guard currentNumber < 10 else {
return
}
i_list.append(currentNumber)
currentNumber += 1
}
}
}
}
Edit: changed currentNumber to #State

SwiftUI - Animating count text from 0 to x

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

How to navigate another view in onReceive timer closure SwiftUI iOS

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

Passing a timer to a child view in SwiftUI

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

Resources