#Publish counters not in sync inside timer SwiftUI - ios

I'm working on an app where I'm using a timer to count down time left in a workout and also count up for total time. My counters are out of sync, looks like it's less than a second off. I'm wondering if it has something to do with #Publish, maybe one fires before the other. Any idea what's happening and how to fix it?
class TimeManager: ObservableObject {
#Published var totalTime: Double = 0.0
#Published var timeRemaining: Double = 180.0
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0) {
self.timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] timer in
guard let self = self else { return }
self.timeRemaining -= 0.1
self.totalTime += 0.1
}
}
}
then my view
#ObservedObject var timeManager = TimeManager()
...
var body: some View {
VStack {
let time = timeManager.timeRemaining
let minutes = Int(time) / 60 % 60
let seconds = Int(time) % 60
ZStack {
Progress()
Text(String(format:"%02i:%02i", minutes, seconds))
.font(.system(size: 60))
}
let total = timeManager.totalTime
let totalMins = Int(total) / 60 % 60
let totalSecs = Int(total) % 60
Text(String(format:"%02i:%02i", totalMins, totalSecs))
.font(.system(size: 40))
}
}

Your time values are in sync. The reason for the behaviour you are seeing is the Double / Int conversions and the rounding applied while display the Texts. Try this line:
Text("\(timeManager.timeRemaining + timeManager.totalTime)")
and you will see this allways adding up to 180.
You could try Int values in your Viewmodel decrementing/incrementing by 1 and a DateComponentsFormatter to format the values in your View.
let componentsFormatter = DateComponentsFormatter()
Text("\(componentsFormatter.string(from: Double(timeManager.timeRemaining)) ?? "NAN")")
.font(.system(size: 60))
You would of course need to tweek the formatter to display the time the way you want it to be. But I agree with Paulw11. This seems like a bad design. It would be better to have a single source of truth as a Date and go from there.

Maybe calculate your 2nd value based on the first when you decrement the time. For example like this:
remaining = (180 - total) >= 0 ? (180 - total) : 0

Related

How do I get the progress bar to work in my iOS app?

I made a timer in Swift using the following code:
import UIKit
class ViewController: UIViewController {
// #IBOutlet var ProgressBar: UIProgressView!
#IBOutlet var label: UILabel!
var hours: Int = 0
var mins: Int = 0
var secs: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
//ProgressBar.progress = 0.0
}
#IBAction func didTapAddButton() {
// var progress: Float = 0.0
//ProgressBar.progress = progress
let vc = storyboard?.instantiateViewController(identifier:"date_picker") as! DateViewController
vc.title = "New Event"
vc.completionHandler = { [weak self] name, date in
DispatchQueue.main.async {
self?.didCreateEvent(name: name, targetDate: date)
}
}
navigationController?.pushViewController(vc, animated: true)
}
private func didCreateEvent(name: String, targetDate: Date){
self.title = name
let difference = floor(targetDate.timeIntervalSince(Date()))
if difference > 0.0 {
let computedHours: Int = Int(difference) / 3600
let remainder: Int = Int(difference) - (computedHours * 3600)
let minutes: Int = remainder / 60
let seconds: Int = Int(difference) - (computedHours * 3600) - (minutes * 60)
print("\(computedHours) \(minutes) \(seconds)")
hours = computedHours
mins = minutes
secs = seconds
updateLabel()
startTimer()
}
else{
print("negative interval")
}
}
private func startTimer() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { _ in
if self.secs > 0 {
self.secs = self.secs - 1
}
else if self.mins > 0 && self.secs == 0 {
self.mins = self.mins - 1
self.secs = 59
}
else if self.hours > 0 && self.mins == 0 && self.secs == 0 {
self.hours = self.hours - 1
self.mins = 59
self.secs = 59
}
self.updateLabel()
})
}
private func updateLabel() {
label.text = "\(hours):\(mins):\(secs)"
}
}
I am trying to make a progress bar somewhat show how much time is left. I want the bar to be full when there is no time left. How should I do this?
I tried searching how to do this but Google wasn't helpful.
So the first thing you want to do is bring all your values for hour, minutes, and seconds into a singular combined value, represented as something like totalSeconds or whatever you want.
var hours: Int = XXX
var minutes: Int = XXX
var seconds: Int = XXX
var totalSeconds: Int = 0
totalSeconds = getTotalSecondsFrom(hours, minutes, seconds)
func getTotalSecondsFrom(hours: Int, minutes: Int, seconds: Int) -> Int {
let hoursToSec = (hours * 60) * 60
let minutesToSec = minutes * 60
return hoursToSec + minutesToSec + seconds
}
Once you have all of your values brought down into a singular value now you have a representation for calculating a progress bar's maximum limit. Remember that a progress bar can only have values betweeen 0.0 and 1.0 which is perfect for a percentage calculation. In the interest of explaining in a way that will make sense for learning, let's look at this from a different perspective. How can we get a percentage from a total maximum, then map it to a value in the range of 0.0 -> 1.0?
func getCompletedPercent(currentSeconds: Int, totalSeconds: Int) -> Float {
return Float(currentSeconds) / Float(totalSeconds)
}
Why does this work? Well, let's assume our total seconds are 5000 and current is 500. Doing the math, 500 / 5000 = 0.1 which is 1/10th or 10% of 5000, which is exactly what we're looking for. With this knowledge in hand all you need to do at this point is set your progress bars maximum to 1.0 and then every second, in your timer, update it's current progress using the functional examples getTotalSecondsFrom(...) and getCompletedPercent(...). Of course your version may vary slightly, but the concepts are the same.

Countdown timer SwiftUI

How to make a countdown timer daily at a specific time. When I open the application again, the timer is reset and the countdown starts again, I'm trying to figure out how to make the timer start again after its time has elapsed..
For example, so that this timer starts over every day at 6 pm
struct TimerView: View {
//MARK: - PROPERTIES
#State var timeRemaining = 24*60*60
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
//MARK: - BODY
var body: some View {
Text("\(timeString(time: timeRemaining))")
.font(.system(size: 60))
.frame(height: 80.0)
.frame(minWidth: 0, maxWidth: .infinity)
.foregroundColor(.white)
.background(Color.black)
.onReceive(timer){ _ in
if self.timeRemaining > 0 {
self.timeRemaining -= 1
}else{
self.timer.upstream.connect().cancel()
}
}
}
//Convert the time into 24hr (24:00:00) format
func timeString(time: Int) -> String {
let hours = Int(time) / 3600
let minutes = Int(time) / 60 % 60
let seconds = Int(time) % 60
return String(format:"%02i:%02i:%02i", hours, minutes, seconds)
}
}
the timer is reset and the countdown starts again
You could try to play with UserDefaults to store variables in the device's memory.
Here is the Documentation : https://developer.apple.com/documentation/foundation/userdefaults
Take a look at TimelineView and AppStorage e.g.
#AppStorage("StartDate") var startDate: Date
...
TimelineView(.periodic(from: startDate, by: 1)) { context in
AnalogTimerView(date: context.date)
}

SwiftUI State updates in Xcode 11 Playground

I have not been able to find anything through the standard Google search, but is there any reason why the ContentView is not updating through the ObservableObject? Feel like I am missing something but I am not quite sure what.
import SwiftUI
import PlaygroundSupport
let start = Date()
let seconds = 10.0 * 60.0
func timeRemaining(minutes: Int, seconds: Int) -> String {
return "\(minutes) minutes \(seconds) seconds"
}
class ViewData : ObservableObject {
#Published var timeRemaining: String = "Loading..."
}
// View
struct ContentView: View {
#ObservedObject var viewData: ViewData = ViewData()
var body: some View {
VStack {
Text(viewData.timeRemaining)
}
}
}
let contentView = ContentView()
let viewData = contentView.viewData
let hosting = UIHostingController(rootView: contentView)
// Timer
let timer = DispatchSource.makeTimerSource()
timer.schedule(deadline: .now(), repeating: .seconds(1))
timer.setEventHandler {
let diff = -start.timeIntervalSinceNow
let remaining = seconds - diff
let mins = Int(remaining / 60.0)
let secs = Int(remaining) % 60
let timeRemaning = timeRemaining(minutes: mins, seconds: secs)
viewData.timeRemaining = timeRemaning
print(timeRemaning)
}
timer.resume()
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
timer.cancel()
PlaygroundPage.current.finishExecution()
}
PlaygroundPage.current.setLiveView(contentView)
PlaygroundPage.current.needsIndefiniteExecution = true
The reason is that GCD based timer works on own queue, so here is the fix - view model have to be updated on main, UI, queue as below
DispatchQueue.main.async {
viewData.timeRemaining = timeRemaning
}
The main utility of GCD timers over standard timers is that they can run on a background queue. Like Asperi said, you can dispatch the updates to the main queue if your GCD timer isn’t using the main queue, itself.
But, you might as well just schedule your GCD timer on the main queue from the get go, and then you don’t have to manually dispatch to the main queue at all:
let timer = DispatchSource.makeTimerSource(queue: .main)
But, if you’re going to run this on the main thread, you could just use a Timer, or, in SwiftUI projects, you might prefer Combine’s TimerPublisher:
import Combine
...
var timer: AnyCancellable? = nil
timer = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { _ in
let remaining = start.addingTimeInterval(seconds).timeIntervalSince(Date())
guard remaining >= 0 else {
viewData.timeRemaining = "done!"
timer?.cancel()
return
}
let mins = Int(remaining / 60.0)
let secs = Int(remaining) % 60
viewData.timeRemaining = timeRemaining(minutes: mins, seconds: secs)
}
When you incorporate your timer within your SwiftUI code (rather than a global like here), it’s nice to stay within the Combine Publisher paradigm.
I also think it’s probably cleaner to cancel the timer when it expires in the timer handler, rather than doing a separate asyncAfter.
Unrelated, but you might consider using DateComponentsFormatter in your timeRemaining function, e.g.:
let formatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
formatter.unitsStyle = .full
return formatter
}()
func timeRemaining(_ timeInterval: TimeInterval) -> String {
formatter.string(from: timeInterval) ?? "Error"
}
Then,
It gets you out of the business of calculating minutes and seconds yourself;
The string will be localized; and
It will ensure grammatically correct wording; e.g. when there are 61 seconds left and the existing routine will report a grammatically incorrect “1 minutes, 1 seconds”.
DateComponentsFormatter gets you out of the weeds of handling these sorts of edge cases, where you want singular instead of plural or languages other than English.

AVPlayer seektotime with Pangesturerecognizer

I'm trying to use seektotime with Pangesture recognizer.But its not seeking as expected.
let totalTime = self.avPlayer.currentItem!.duration
print("time: \(CMTimeGetSeconds(totalTime))")
self.avPlayer.pause()
let touchDelta = swipeGesture.translationInView(self.view).x / CGFloat(CMTimeGetSeconds(totalTime))
let currentTime = CMTimeGetSeconds((avPlayer.currentItem?.currentTime())!) + Float64(touchDelta)
print(currentTime)
if currentTime >= 0 && currentTime <= CMTimeGetSeconds(totalTime) {
let newTime = CMTimeMakeWithSeconds(currentTime, Int32(NSEC_PER_SEC))
print(newTime)
self.avPlayer.seekToTime(newTime)
}
What I'm doing wrong in here ?
Think about what's happening in this line here:
let touchDelta = swipeGesture.translationInView(self.view).x / CGFloat(CMTimeGetSeconds(totalTime))
You're dividing pixels (the translation in just the x-axis) by time. This really isn't a "delta" or absolute difference. It's a ratio of sorts. But it's not a ratio that has any meaning. Then you're getting your new currentTime by just added this ratio to the previous currentTime, so you're adding pixels per seconds to pixels, which doesn't give a logical or useful number.
What we need to do is take the x-axis translation from the gesture and apply a scale (which is a ratio) to it in order to get a useful number of seconds to advance/rewind the AVPlayer. The x-axis translation is in pixels so we'll need a scale that describes seconds per pixels and multiple the two in order to get our number of seconds. The proper scale is the ratio between the total number of seconds in the video and the total number of pixels that the user can move through in the gesture. Multiplying pixels times (seconds divided by pixels) gives us a number in seconds. In pseudocode:
scale = totalSeconds / totalPixels
timeDelta = translation * scale
currentTime = oldTime + timeDelta
So I would rewrite your code like this:
let totalTime = self.avPlayer.currentItem!.duration
print("time: \(CMTimeGetSeconds(totalTime))")
self.avPlayer.pause()
// BEGIN NEW CODE
let touchDelta = swipeGesture.translationInView(self.view).x
let scale = CGFloat(CMTimeGetSeconds(totalTime)) / self.view.bounds.width
let timeDelta = touchDelta * scale
let currentTime = CMTimeGetSeconds((avPlayer.currentItem?.currentTime())!) + Float64(timeDelta)
// END NEW CODE
print(currentTime)
if currentTime >= 0 && currentTime <= CMTimeGetSeconds(totalTime) {
let newTime = CMTimeMakeWithSeconds(currentTime, Int32(NSEC_PER_SEC))
print(newTime)
self.avPlayer.seekToTime(newTime)
}
I have same issue, then i create the UISlider and set the action method is given below,
declare AVPlayer is var playerVal = AVPlayer()
#IBAction func sliderAction(sender: UISlider) {
playerVal.pause()
displayLink.invalidate()
let newTime:CMTime = CMTimeMakeWithSeconds(Double(self.getAudioDuration() as! NSNumber) * Double(sender.value), playerVal.currentTime().timescale)
playerVal.seekToTime(newTime)
updateTime()
playerVal.play()
deepLink()
}
And another method is,
func updateTime() {
let currentTime = Float(CMTimeGetSeconds(playerItem1.currentTime()))
let minutes = currentTime/60
let seconds = currentTime - minutes * 60
let maxTime = Float(self.getAudioDuration() as! NSNumber)
let maxminutes = maxTime / 60
let maxseconds = maxTime - maxminutes * 60
startValue.text = NSString(format: "%.2f:%.2f", minutes,seconds) as String
stopValue.text = NSString(format: "%.2f:%.2f", maxminutes,maxseconds) as String
}
I have used CADisplayLink and declare var displayLink = CADisplayLink(), its used continue(automatically) playing audios. code is
func deepLink() {
displayLink = CADisplayLink(target: self, selector: ("updateSliderProgress"))
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
}
func updateSliderProgress(){
let progress = Float(CMTimeGetSeconds(playerVal.currentTime())) / Float(self.getAudioDuration() as! NSNumber)
sliderView.setValue(Float(progress), animated: false)
}
if you see this above answer, you have get idea, hope its helpful

Format timer label to hours:minutes:seconds in Swift

I have an NSTimer which counts DOWN from 2 hours until 0.
Here are some of my code:
var timer = NSTimer()
let timeInterval:NSTimeInterval = 0.5
let timerEnd:NSTimeInterval = 0.0
var timeCount:NSTimeInterval = 7200.0 // seconds or 2 hours
// TimeString Function
func timeString(time:NSTimeInterval) -> String {
let minutes = Int(time) / 60
let seconds = time - Double(minutes) * 60
let secondsFraction = seconds - Double(Int(seconds))
return String(format:"%02i:%02i.%01i",minutes,Int(seconds),Int(secondsFraction * 10.0))
}
The Timer Label is:
TimerLabel.text = "Time: \(timeString(timeCount))"
HOWEVER, my timer label shows as:
Time: 200:59.0
How do I format my timer label to look like this:
Time: 01:59:59 // (which is hours:minutes:seconds)?
[Please note that I have no problems with my countdown timer, I only need to know how to CHANGE THE TIME FORMAT using the TimeString function.]
EDIT:
Someone mentioned that my question is a possible duplicate of this one: Swift - iOS - Dates and times in different format. HOWEVER, I am asking on how do I change the time format using the TimeString function that I gave above. I am not asking for another WAY on how to do it.
For instance:
let minutes = Int(time) / 60
gives me "200" minutes. etc.
Your calculations are all wrong.
let hours = Int(time) / 3600
let minutes = Int(time) / 60 % 60
let seconds = Int(time) % 60
return String(format:"%02i:%02i:%02i", hours, minutes, seconds)
#rmaddy's solution is accurate and answers the question. However, neither the question nor the solution take into account international users. I suggest using DateComponentsFormatter and let the framework handle the calculations and formatting. Doing so makes your code less error prone and more future proof.
I came across this blog post that provides a concise solution:
http://crunchybagel.com/formatting-a-duration-with-nsdatecomponentsformatter/
Pulled from that post, this is the code snippet that would replace the code you're currently using to make your calculations. Updated for Swift 3:
let duration: TimeInterval = 7200.0
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale
formatter.allowedUnits = [ .hour, .minute, .second ] // Units to display in the formatted string
formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
let formattedDuration = formatter.string(from: duration)
Swift5
var totalSecond = Int()
var timer:Timer?
call startTimer() based on requirement-
func startTimer(){
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(countdown), userInfo: nil, repeats: true)
}
#objc func countdown() {
var hours: Int
var minutes: Int
var seconds: Int
if totalSecond == 0 {
timer?.invalidate()
}
totalSecond = totalSecond - 1
hours = totalSecond / 3600
minutes = (totalSecond % 3600) / 60
seconds = (totalSecond % 3600) % 60
timeLabel.text = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
}
Done
The best way to implement a Timer in Swift (swift 4 works fine).
Declare the variable secs: Int and assign the value, in seconds, of the timer.
Then with the Timer () function, discount one second at a time and pass it to this function.
var secs = 0
var timer = Timer()
func startTimer(segs: Int) {
seg = segs
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerDiscount), userInfo: nil, repeats: true)
}
func timerDiscount() {
let hours = secs / 3600
let mins = secs / 60 % 60
let secs = secs % 60
let restTime = ((hours<10) ? "0" : "") + String(hours) + ":" + ((mins<10) ? "0" : "") + String(mins) + ":" + ((secs<10) ? "0" : "") + String(secs)
}
Declare the variables hours ,minutes and seconds and copy paste the below code it works fine.
if counter > 0 {
let hours = counter / 3600
let minutes = counter / 60
let seconds = counter % 60
counter = counter - 1
timerLbl.text = "\(hours):\(minutes):\(seconds)"
}

Resources