I need to use 2 timers at the same time: one for a session and one for exercices.
The one for exercices can be either a timer or a count down depending on the exercices.
I used this class for the 2 timers but I have a delay of 1s: I think it's a problem of thread. I tried to dispatch but not working.
Moreover the class is not accurate since refresh may take longer than specified, based on the available resources.
Do you have a solution to resolve my issue: having 2 accurate timers at same which can be timer or count down ?
Thank you for your help
public class Chronometer: NSObject {
//
// MARK: - Private Properties
private var startTime = NSTimeInterval(0)
private var accumulatedTime = NSTimeInterval(0)
private var elapsedSinceLastRefresh = NSTimeInterval(0)
private var timer = NSTimer()
//
// MARK: - Public Properties
public var elapsedTime: NSTimeInterval {
return elapsedSinceLastRefresh + accumulatedTime
}
/// The Time Interval to refresh the chronometer. The default is 1 second
public var refreshInterval = NSTimeInterval(1)
/// Determines a time limit for this Chronometer
public var timeLimit: NSTimeInterval?
/// Optional Block that gets called on each refresh of the specified time interval.
/// If this class needs to update UI, make sure that the UI class are made in the
/// main thread, or dispatched into it.
public var updateBlock: ((NSTimeInterval, NSTimeInterval?) -> ())?
/// Optional Block that gets called when the chronometer reach its limit time
public var completedBlock: (() -> ())?
//
// MARK: - Initializers
///
/// A convenience initializer that allow specifying the refresh interval
/// :param: refreshInterval The desired refresh interval
///
public convenience init(refreshInterval: NSTimeInterval) {
self.init()
self.refreshInterval = refreshInterval
}
///
/// A convenience initializer that allow specifying the refesh interval and update block
/// :param: refreshInterval The desired refresh interval
/// :param: updateBlock The update block to be called on each refresh
///
public convenience init(refreshInterval: NSTimeInterval, updateBlock: (NSTimeInterval, NSTimeInterval?) -> ()) {
self.init()
self.refreshInterval = refreshInterval
self.updateBlock = updateBlock
}
//
// MARK: - Internal Methods
///
/// Refresh the timer, calling the update block to notify the tracker about the new value.
///
func refreshTime() {
// Calculate the new time
var refreshTime = NSDate.timeIntervalSinceReferenceDate()
self.elapsedSinceLastRefresh = (refreshTime - startTime)
// Calculates the remaining time if applicable
var remainingTime: NSTimeInterval? = nil
if self.timeLimit != nil {
remainingTime = self.timeLimit! - elapsedTime
}
// If an update block is specified, then call it
self.updateBlock?(elapsedTime, remainingTime)
// If the chronometer is complete, then call the block and
if let limit = self.timeLimit {
if self.elapsedTime >= limit {
self.stop()
self.completedBlock?()
}
}
}
//
// MARK: - Public Methods
///
/// Set a time limit for this chronometer
///
public func setLimit(timeLimit: NSTimeInterval, withCompletionBlock completedBlock: () -> ()) {
self.timeLimit = timeLimit
self.completedBlock = completedBlock
}
///
/// Start the execution of the Cronometer.
/// Start will take place using accumulated time from the last session.
/// If the Cronometer is running the call will be ignored.
///
public func start() {
if !timer.valid {
// Create a new timer
timer = NSTimer.scheduledTimerWithTimeInterval(self.refreshInterval,
target: self,
selector: "refreshTime",
userInfo: nil,
repeats: true)
// Set the base date
startTime = NSDate.timeIntervalSinceReferenceDate()
}
}
///
/// Stops the execution of the Cronometer.
/// Keeps the accumulated value, in order to allow pausing of the chronometer, and
/// to keep "elapsedTime" property value available for the user to keep track.
///
public func stop() {
timer.invalidate()
accumulatedTime = elapsedTime
elapsedSinceLastRefresh = 0
}
///
/// Resets the Cronometer.
/// This method will stop chronometer if it's running. This is necessary since
/// the class is not thread safe.
///
public func reset() {
timer.invalidate()
elapsedSinceLastRefresh = 0
accumulatedTime = 0
}
}
In my UIViewController, in the viewDidLoad():
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) { [unowned self] in
self.chronometerWorkout = Chronometer(refreshInterval: NSTimeInterval(0.01)) {
(elapsedTime: NSTimeInterval, remainingTime: NSTimeInterval?) in
dispatch_async(dispatch_get_main_queue()) {
if let r = remainingTime {
self.counterView?.chronoWorkoutLabel.text = r.getFormattedInterval(miliseconds: false)
} else {
self.secondsChronoWorkout = elapsedTime
self.counterView?.chronoWorkoutLabel.text = elapsedTime.getFormattedInterval(miliseconds: false)
}
}
}
}
self.chronometerExo = Chronometer(refreshInterval: NSTimeInterval(0.01)) {
(elapsedTime: NSTimeInterval, remainingTime: NSTimeInterval?) in
if let r = remainingTime {
self.counterView?.chronoExoLabel.text = r.getFormattedInterval(miliseconds: false)
self.graphicView.yValues[self.selectedRow] = Double(remainingTime!)
self.counterView?.circularTimerProgressView.progress = CGFloat(remainingTime!)
} else {
self.secondsChronoExo = elapsedTime
self.counterView?.chronoExoLabel.text = elapsedTime.getFormattedInterval(miliseconds: false)
self.graphicView.yValues[self.selectedRow] = Double(elapsedTime)
self.counterView?.circularTimerProgressView.progress = CGFloat(elapsedTime)
}
}
UPDATED:
var startWorkOutDate:CFAbsoluteTime!
var startExoTime:CFAbsoluteTime!
var timeReference:Double!
func updateTimerLabels() {
let elapsedTimeWorkOut = CFAbsoluteTimeGetCurrent() - startWorkOutDate
self.counterView?.chronoWorkoutLabel.text = String(format: "%.2f",elapsedTimeWorkOut)
let startExoTime = self.startExoTime ?? 0.0
let elapsedTimeExo = CFAbsoluteTimeGetCurrent() - startExoTime
if timeReference != 0 {
if elapsedTimeExo <= timeReference {
let remainingTimeExo = timeReference - elapsedTimeExo
self.counterView?.chronoExoLabel.text = String(format: "%.2f",remainingTimeExo)
}
}
}
func startTimers(indexPath: NSIndexPath){
...
if self.startWorkoutChronometer == false {
startWorkOutDate = CFAbsoluteTimeGetCurrent()
self.startWorkoutChronometer = true
self.startExoChronometer = true
_ = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("updateTimerLabels"), userInfo: nil, repeats: true)
}
if self.startExoChronometer == true {
if timeReference != 0 {
if self.graphicView.yValues[selectedRow] == timeReference {
startExoTime = CFAbsoluteTimeGetCurrent()
} else {
timeReference = Double(Int(self.graphicView.yValues[selectedRow]))
}
}
}
}
Consider not using 1 NSTimer instance per timer. Use an NSTimer, or maybe even display link' only to trigger updates to the UI.
For your timers, just record the start time as an NSDate and the timer type. Provide a method to ask for the current time, or display value. The timer can then calculate that on the fly based on the current device time.
In this way you will have no slip or offset and you can update the UI as fast as you like.
Related
I want to animate a number. The animation I want to achieve is going from 0 increasing all the way up to the current number (at high speed). In this project, the number is the number of steps a user has taken. Is there a way this can be achieved?
LazyVStack{
ForEach(steps, id: \.id) { step in
//Here is the number I want to be animated
Text("\(step.count)")
.font(.custom(customFont, size: 50))
Text("Steps")
.font(.custom(customFont, size: 25))
.multilineTextAlignment(.center)
}
}
I believe I have a function along the right lines, I just need to apply it! Here is the function:
func addNumberWithRollingAnimation() {
withAnimation {
// Decide on the number of animation tasks
let animationDuration = 1000 // milliseconds
let tasks = min(abs(self.enteredNumber), 100)
let taskDuration = (animationDuration / tasks)
// add the remainder of our entered num from the steps
total += self.enteredNumber % tasks
// For each task
(0..<tasks).forEach { task in
// create the period of time when we want to update the number
// I chose to run the animation over a second
let updateTimeInterval = DispatchTimeInterval.milliseconds(task * taskDuration)
let deadline = DispatchTime.now() + updateTimeInterval
// tell dispatch queue to run task after the deadline
DispatchQueue.main.asyncAfter(deadline: deadline) {
// Add piece of the entire entered number to our total
self.total += Int(self.enteredNumber / tasks)
}
}
}
}
Here is a utility function called Timer.animateNumber() which takes a Binding<Int> to animate, a Binding<Bool> busy which indicates if the value is currently animating, and Int start value, an Int end value, and a Double duration in seconds.
To use it, you need to define an #State private var number: Int to animate, and #State private var busy: Bool to keep track of the animation's state. This can also be used to terminate the animation early by just setting busy to false. Pass in your start value, end value, and duration in seconds.
This demo shows two animated numbers. The first counts up from 1 to 10000 in 1 second. The second counts down from 20 to 0 in 20 seconds. The Stop All button can be used to stop both animations.
extension Timer {
static func animateNumber(number: Binding<Int>, busy: Binding<Bool>, start: Int, end: Int, duration: Double = 1.0) {
busy.wrappedValue = true
let startTime = Date()
Timer.scheduledTimer(withTimeInterval: 1/120, repeats: true) { timer in
let now = Date()
let interval = now.timeIntervalSince(startTime)
if !busy.wrappedValue {
timer.invalidate()
}
if interval >= duration {
number.wrappedValue = end
timer.invalidate()
busy.wrappedValue = false
} else {
number.wrappedValue = start + Int(Double(end - start)*(interval/duration))
}
}
}
}
struct ContentView: View {
#State private var number: Int = 0
#State private var number2: Int = 0
#State private var busy: Bool = false
#State private var busy2: Bool = false
var body: some View {
VStack(spacing: 20) {
Text(String(number))
Button("Go") {
if !busy {
Timer.animateNumber(number: $number, busy: $busy, start: 1, end: 10000, duration: 1)
}
}
Text(String(number2))
Button("Go") {
if !busy2 {
Timer.animateNumber(number: $number2, busy: $busy2, start: 20, end: 0, duration: 20)
}
}
Button("Stop All") {
busy = false
busy2 = false
}
}
}
}
I am wanting to have a stopwatch in my app that runs completely off the device's time. I have my code below which takes the time in which the start button is pressed, and then every second updates the secondsElapsed to be the difference between the startTime and current. I am getting stuck on implementing a pause function. If I just invalidate the update timer, then the timer will restart having pretty much carried on from where it left off. Any ideas on how this could be done?
class StopWatchManager: ObservableObject{
#Published var secondsElapsed = 0
var startTime: Date = Date()
var timer = Timer()
func startWatch(){
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ timer in
let current = Date()
let diffComponents = Calendar.current.dateComponents([.second], from: self.startTime, to: current)
let seconds = (diffComponents.second ?? 0)
self.secondsElapsed = seconds
}
}
func pauseWatch(){
timer.invalidate()
}
}
I display the stopwatch using this code below:
struct ContentView: View {
#ObservedObject var stopWatchManager = StopWatchManager()
var body: some View{
HStack{
Button("Start"){
stopWatchManager.startWatch()
}
Text("\(stopWatchManager.secondsElapsed)")
Button("Pause"){
stopWatchManager.pauseWatch()
}
}
}
}
Yes. Here is how to do it:
When pause is pressed, note the current time and compute the elapsed time for the timer. Invalidate the update timer.
When the timer is resumed, take the current time and subtract the elapsed time. Make that the startTime and restart the update timer.
Here's the updated code:
class StopWatchManager: ObservableObject{
#Published var secondsElapsed = 0
var startTime: Date = Date()
var elapsedTime = 0.0
var paused = false
var running = false
var timer = Timer()
func startWatch(){
guard !running else { return }
if paused {
startTime = Date().addingTimeInterval(-elapsedTime)
} else {
startTime = Date()
}
paused = false
running = true
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true){ timer in
let current = Date()
let diffComponents = Calendar.current.dateComponents([.second], from: self.startTime, to: current)
let seconds = (diffComponents.second ?? 0)
self.secondsElapsed = seconds
}
}
func pauseWatch(){
guard !paused else { return }
timer.invalidate()
elapsedTime = Date().timeIntervalSince(startTime)
paused = true
running = false
}
}
Things to note:
I changed the timer interval to 0.1 from 1 to avoid missing updates.
I added paused and running state variables to keep the buttons from being pressed more than once in a row.
I have built this app with the help of some friends. I don't really know how the code works.
Basically using an apple pencil it records data (time on tablet, speed of apple pencil, stroke counts etc). However as more time elapses and more drawing occurs, the timer gets out of sync with real time.
The purpose of this app is for dementia research, I get patients to draw on the tablet, and i collect information of that. I can't do the research if the timer stinks.
I have tried disabling all the timers, but the lag remains the same. I have a felling it has something to do with how strokes are being sampled. I just need a stroke count I don't need it to show strokes per min (which is what it is currently doing). I think the stroke counter might the cause???
this is the program:
https://drive.google.com/open?id=1lwzKwG7NLcX1qmE5yoxsdq5HICV2TNHm
class StrokeSegment {
var sampleBefore: StrokeSample?
var fromSample: StrokeSample!
var toSample: StrokeSample!
var sampleAfter: StrokeSample?
var fromSampleIndex: Int
var segmentUnitNormal: CGVector {
return segmentStrokeVector.normal!.normalized!
}
var fromSampleUnitNormal: CGVector {
return interpolatedNormalUnitVector(between: previousSegmentStrokeVector, and: segmentStrokeVector)
}
var toSampleUnitNormal: CGVector {
return interpolatedNormalUnitVector(between: segmentStrokeVector, and: nextSegmentStrokeVector)
}
var previousSegmentStrokeVector: CGVector {
if let sampleBefore = self.sampleBefore {
return fromSample.location - sampleBefore.location
} else {
return segmentStrokeVector
}
}
var segmentStrokeVector: CGVector {
return toSample.location - fromSample.location
}
var nextSegmentStrokeVector: CGVector {
if let sampleAfter = self.sampleAfter {
return sampleAfter.location - toSample.location
} else {
return segmentStrokeVector
}
}
init(sample: StrokeSample) {
self.sampleAfter = sample
self.fromSampleIndex = -2
}
#discardableResult
func advanceWithSample(incomingSample: StrokeSample?) -> Bool {
if let sampleAfter = self.sampleAfter {
self.sampleBefore = fromSample
self.fromSample = toSample
self.toSample = sampleAfter
self.sampleAfter = incomingSample
self.fromSampleIndex += 1
return true
}
return false
}
}
class StrokeSegmentIterator: IteratorProtocol {
private let stroke: Stroke
private var nextIndex: Int
private let sampleCount: Int
private let predictedSampleCount: Int
private var segment: StrokeSegment!
init(stroke: Stroke) {
self.stroke = stroke
nextIndex = 1
sampleCount = stroke.samples.count
predictedSampleCount = stroke.predictedSamples.count
if (predictedSampleCount + sampleCount) > 1 {
segment = StrokeSegment(sample: sampleAt(0)!)
segment.advanceWithSample(incomingSample: sampleAt(1))
}
}
func sampleAt(_ index: Int) -> StrokeSample? {
if index < sampleCount {
return stroke.samples[index]
}
let predictedIndex = index - sampleCount
if predictedIndex < predictedSampleCount {
return stroke.predictedSamples[predictedIndex]
} else {
return nil
}
}
func next() -> StrokeSegment? {
nextIndex += 1
if let segment = self.segment {
if segment.advanceWithSample(incomingSample: sampleAt(nextIndex)) {
return segment
}
}
return nil
}
}
for example at true 25 seconds, the app displays the total time at 20 seconds.
A Timer is not something to count elapsed time. It is a tool used to trigger an execution after some time has elapsed. But just "after" some time has elapsed, not "exactly after" some time has elapsed. So for instance doing something like:
var secondsElapsed: TimeInterval = 0.0
let timeInitiated = Date()
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
secondsElapsed += 1
print("\(secondsElapsed) seconds should have passed but in reality \(Date().timeIntervalSince(timeInitiated)) second have passed")
}
you will see that the two are not the same but are pretty close. But as soon as I add some extra work like this:
var secondsElapsed: TimeInterval = 0.0
let timeInitiated = Date()
func countTo(_ end: Int) {
var string = ""
for i in 1...end {
string += String(i)
}
print("Just counted to string of lenght \(string.count)")
}
Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { _ in
countTo(100000)
}
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
secondsElapsed += 1
print("\(secondsElapsed) seconds should have passed but in reality \(Date().timeIntervalSince(timeInitiated)) second have passed")
}
we get to situations like "14.0 seconds should have passed but in reality 19.17617702484131 second have passed".
We made the application busy so it doesn't have time to count correctly.
In your case you will need to use one of two solutions:
If you are interested in time elapsed simply use timeIntervalSince as demonstrated in first code snippet.
If you need to ensure triggering every N seconds you should optimize your code, consider multithreading... But mostly keep in mind that you can only get close to "every N seconds", it should not be possible to guarantee an execution exactly every N seconds.
I have a variable that will changes every millisecond or so.
I want to calculate the time between them correctly without delay.
I want to know how long takes to get the new one.
Is that possible in swift?
I know that there is a timer in swift but according to apple documentation:
that's not exact.i need to get the millisecond time between each
receiving variable.
Use a property observer didSet with Date arithmetic to compute the interval between changes.
Here is an example:
class ViewController: UIViewController {
private var setTime: Date?
private var intervals = [Double]()
var value: Int = 0 {
didSet {
let now = Date()
if let previous = setTime {
intervals.append(now.timeIntervalSince(previous) * 1000)
}
setTime = now
}
}
override func viewDidLoad() {
super.viewDidLoad()
for i in 1...20 {
value = i
}
print(intervals)
}
}
Console output
[0.0020265579223632812, 0.12600421905517578, 0.00095367431640625, 0.0050067901611328125, 0.0010728836059570312, 0.00095367431640625, 0.00095367431640625, 0.0010728836059570312, 0.00095367431640625, 0.0020265579223632812, 0.00095367431640625, 0.0010728836059570312, 0.00095367431640625, 0.0, 0.0010728836059570312, 0.00095367431640625, 0.00095367431640625, 0.0020265579223632812, 0.0010728836059570312]
You can capture time whenever the value changes and calculate the difference. like:
var yourVar: Int {
willSet {
//you can capture the time here
}
didSet {
//or here
}
}
var _variable = 0
var variable : Int {
get{
return _variable
}
set{
let start = DispatchTime.now()
_variable = newValue
let dst = start.distance(to: DispatchTime.now())
print("Interval = \(dst)")
}
}
variable = 1
variable = 2
Output:
Interval = nanoseconds(2239301)
Interval = nanoseconds(69482)
I'm using this function to make the text write letter by letter:
extension SKLabelNode {
func setTextWithTypeAnimation(typedText: String, characterInterval: NSTimeInterval = 0.05) {
text = ""
self.fontName = "PressStart2P"
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)) {
for character in typedText.characters {
dispatch_async(dispatch_get_main_queue()) {
self.text = self.text! + String(character)
}
NSThread.sleepForTimeInterval(characterInterval)
}
}
}
And, if the user clicks the screen, I want to make the for loop stop and show the complete text instantly.
I would do something like this:
var ignoreSleeper = false
#IBAction func pressButton(sender: UIButton) {
ignoreSleeper = true
}
extension SKLabelNode {
func setTextWithTypeAnimation(typedText: String, characterInterval: NSTimeInterval = 0.05) {
text = ""
self.fontName = "PressStart2P"
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)) {
for character in typedText.characters {
dispatch_async(dispatch_get_main_queue()) {
self.text = self.text! + String(character)
}
if(!ignoreSleeper){
NSThread.sleepForTimeInterval(characterInterval)
}
}
}
}
Edit: like #Breek already mentioned
I'd suggest to implement a tiny NSTimer with a counter for the number of chars to display. Start the Timer with a repeat count of typedText.characters.count and the desired delay and you're good to go (with one thread). Increment the number of chars counter on each timer loop. You can stop this timer at any time with a button press by calling invalidate on the timer.
Example
var timer: NSTimer?
var numberOfCharsToPrint = 1
let text = "Hello, this is a test."
func updateLabel() {
if numberOfCharsToPrint == text.characters.count {
welcomeLabel.text = text
timer?.invalidate()
}
let index = text.startIndex.advancedBy(numberOfCharsToPrint)
welcomeLabel.text = text.substringToIndex(index)
numberOfCharsToPrint++;
}
Then initialize your timer whenever you want the animation to start.
timer = NSTimer.scheduledTimerWithTimeInterval(0.25, target: self, selector: "updateLabel", userInfo: nil, repeats: true)
You can invalidate/stop the timer at any given time with timer?.invalidate().