Synchronous start of the event - ios

Is there a way to run the code simultaneously on different devices? Let's say I want that when I click on a button on one of the devices, the function starts simultaneously on both the first and the second device? I tried to use a timer with a time check for 3 seconds ahead, but the function is triggered with a delay of 0.5 seconds on the second device
func getEventTime() -> UInt64{
let now = Date()
let interval = now.timeIntervalSince1970
let result = (UInt64(interval) + (3)) * 1000
return result
}
func getCurrentTime() -> UInt64{
let now = Date()
let interval = now.timeIntervalSince1970
let result = UInt64(interval * 1000)
return result
}
func startTimer(time : UInt64){
Timer.scheduledTimer(withTimeInterval: 0.0001, repeats: true) { timer in
switch getCurrentTime() {
case time - 1000 :
DispatchQueue.main.async {
countdownImageTimer.image = UIImage(named: "Start")
}
break
case time :
DispatchQueue.main.async {
countdownImageTimer.removeFromSuperview()
}
self.setShips()
timer.invalidate()
break
default:
break
}
}
}

Related

Swift - Re-add time back into Timer

I have a countdown Timer that shows seconds and milliseconds. The user can start/stop recording multiple times until the timer hits zero. The user can also delete a previous recording at which point I have to re-add that deleted time back into the initial 20 secs. There are 2 issues.
The first issue is when the timer is stopped, the remaining time that shows on the timer label doesn't match the time culmination of the recordings. From my understanding this might be a RunLoop issue and I don't think there is anything that I can do about the inaccuracies.
let initialTime = 20.0
var cumulativeTimeForAllAssests = 0.0
for asset in arrOfAssets {
let assetDuration = CMTimeGetSeconds(asset.duration)
print("assetDuration: ", assetDuration)
cumulativeTimeForAllAssests += assetDuration
}
print("\ncumulativeTimeForAllAssests: ", cumulativeTimeForAllAssests)
After starting/stopping 5 times, the remaining time on the timer label says 16.5 but the culmination of the assets time is 4.196666.... The timer label should say 15.8, it's 0.7 milli off. The more I start/stop the recording, the more inaccurate/further off the culmination time - the initial time and the timer label time is.
assetDuration: 0.7666666666666667
assetDuration: 0.9666666666666667
assetDuration: 0.7983333333333333
assetDuration: 0.7333333333333333
assetDuration: 0.9316666666666666
cumulativeTimeForAllAssests: 4.196666666666667
The second issue is because I'm using seconds and milliseconds in my timerLabel, when I add re-add the subtracted time back in via deleteAssetAndUpdateTimer(...), I use the parts of modf() to update the seconds and milliseconds. I couldn't think of another way to update the timer. I know there has to be a more accurate way to do it.
Timer code:
weak var timer: Timer?
var seconds = 20
var milliseconds = 0
let initialTime = 20.0
func startTimer() {
invalidateTimer()
if seconds == Int(initalTime) && milliseconds == 0 {
timerIsRunning()
}
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] _ in
self?.timerIsRunning()
})
}
func timerIsRunning() {
updateTimerLabel()
if milliseconds == 0 {
seconds -= 1
}
milliseconds -= 1
if milliseconds < 0 {
milliseconds = 9
}
if seconds == 0 && milliseconds == 0 {
invalidateTimer()
updateTimerLabel()
}
}
func invalidateTimer() {
timer?.invalidate()
timer = nil
}
func updateTimerLabel() {
let milisecStr = "\(milliseconds)"
let secondsStr = seconds > 9 ? "\(seconds)" : "0\(seconds)"
timerLabel.text = "\(secondsStr).\(milisecStr)"
}
Delete asset and update timer code:
// the timer is stopped when this is called
func deleteAssetAndUpdateTimer(_ assetToDelete: AVURLAsset) {
var cumulativeTimeForAllAssests = 0.0
for asset in arrOfAssets {
let assetDuration = CMTimeGetSeconds(asset.duration)
cumulativeTimeForAllAssests += assetDuration
}
let timeFromAssetToDelete = CMTimeGetSeconds(assetToDelete.duration)
let remainingTime = self.initialTime - cumulativeTimeForAllAssests
let updatedTime = remainingTime + timeFromAssetToDelete
let mod = modf(updatedTime)
self.seconds = Int(mod.0)
self.milliseconds = Int(mod.1 * 10)
updateTimerLabel()
// remove assetToDelete from array
}
The big issue here was I was using a Timer to countdown which was incorrect. Following #LeoDabus' comments, I instead used CACurrentMediaTime():
let timerLabel = UILabel()
let maxRecordingTime = 30.0
lazy var elapsedTime = maxRecordingTime
var startTime: CFTimeInterval?
var endTime: CFTimeInterval?
weak var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
updateTimerLabel(with: Int(maxRecordingTime))
}
#IBAction func recordButtonPressed(_ sender: UIButton) {
if startTime == nil {
startTimer()
} else {
stopTimer(updateElapsed: true)
}
}
func startTimer() {
if elapsedTime == 0 { return }
stopTimer()
startTime = CACurrentMediaTime()
endTime = startTime! + elapsedTime
print("startTime: \(startTime!) | endTime: \(endTime!)")
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] _ in
self?.timerIsRunning()
}
}
func timerIsRunning() {
guard let startTime = startTime, let endTime = endTime else { return }
let currentTime = CACurrentMediaTime()
let remainingTime = currentTime - startTime
print("%2d %.3lf", elapsedTime, remainingTime)
if currentTime >= endTime {
print("stopped at - currentTime: \(currentTime) | endTime: \(endTime)")
stopTimer(updateElapsed: true, currentTime: currentTime)
return
}
let countDownTime: Double = elapsedTime - remainingTime
let seconds = Int(countDownTime)
updateTimerLabel(with: seconds)
}
func updateTimerLabel(with seconds: Int) {
let secondsStr = seconds > 9 ? "\(seconds)" : "0\(seconds)"
timerLabel.text = secondsStr
}
func stopTimer(updateElapsed: Bool = false, currentTime: Double? = nil) {
timer?.invalidate()
timer = nil
if updateElapsed {
updateElapsedTime(using: currentTime)
}
startTime = nil
endTime = nil
}
func updateElapsedTime(using currentTime: Double? = nil) {
guard let startTime = startTime else { return }
var timeNow = CACurrentMediaTime()
if let currentTime = currentTime {
timeNow = currentTime
}
var updatedTime = elapsedTime - (timeNow - startTime)
if updatedTime < 0 {
updatedTime = 0
}
elapsedTime = updatedTime
}
func resetElapsedTime() { // This is for a resetButton not shown here
elapsedTime = maxRecordingTime
}

Drawing app with timer. Timers begins to lag after drawing for less than 20 seconds

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.

UILabel Text label and timer function issue on table view refresh

I have created a timer function in a subclass of UITableViewCell, which updates the label of itself (the label text) using a timer:
func handleCountdown(){
if timerIsRunning == false { //check if timer is NOT running, otherwise update timer
timerIsRunning = true
startTime = activity.countdownValue
runTimer()
}
}
func runTimer() {
timer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: (#selector(self.updateTimer)), userInfo: nil, repeats: true)
}
#objc func updateTimer(){
let hours = Int(startTime) / 3600
let minutes = Int(startTime) / 60 % 60
if startTime > 0 {
startTime -= 1
print("The start time for " + String(activity.name) + "is " + String(startTime))
switch startTime {
case 0:
activity.isActive = false
activity.timerIsRunning = false
activity.countdownValue = 0
timer.invalidate()
activitiesRef = Database.database().reference().child("Users/\(userID)/Activities")
activitiesRef.child(self.activity.id).setValue([ "id": self.activity.id,
"name": self.activity.name,
"isActive": self.activity.isActive,
"locString": self.activity.locationString,
"locLat": self.activity.locLat,
"locLong": self.activity.locLong,
"privacySetting": self.activity.privacySetting,
"timerIsRunning": self.activity.timerIsRunning,
"countdownValue": self.activity.countdownValue])
returnToInactiveDelegate?.activeToInactive(data: activity)
case 1..<60: //display seconds
let count = String(Int(startTime))
countdownTimer.text = count
print("The start time for " + String(activity.name) + "is " + String(startTime))
//countdownTimer.text = String(Int(startTime)) + "s"
case 60..<3600: //display minutes
print("The start time for " + String(activity.name) + "is " + String(startTime))
countdownTimer.text = String(Int(minutes)) + "m"
case 3600..<86400: // display hours and minutes
print("The start time for " + String(activity.name) + "is " + String(startTime))
countdownTimer.text = String(Int(hours)) + "h \(minutes)m"
default:
timer.invalidate()
activity.countdownValue = 0
}
}
When the timer reaches zero, a delegate function deletes the appropriate cell and moves it to a different section in the table view, as follows:
func activeToInactive(data: Activity) {
if let i = activeActivities.index(where: { $0.isActive == false }) {
print("array index to remove: should be where it is NOT active = \(i)")
activeActivities.remove(at: i)
}
inactiveActivities.insert(data, at: 0)
self.tableView.reloadData()
}
Finally, upon table view reload, it does one last check to see if the timer is still running or not (to see if it should start the timer from the initial start time, or not do anything and let it handle the countdown) as follows:
let inactiveCell = tableView.dequeueReusableCell(withIdentifier: inactiveIdentifer , for: indexPath) as! InactiveCell
let activeCell = tableView.dequeueReusableCell(withIdentifier: activeIdentifier, for: indexPath) as! ActiveCell
if indexPath.section == 0 && self.activeActivities.count != 0 {
activeCell.name.text = self.activeActivities[indexPath.row].name
activeCell.location.text = self.activeActivities[indexPath.row].locationString
activeCell.activity = self.activeActivities[indexPath.row]
activeCell.returnToInactiveDelegate = self
if activeCell.activity.isActive == true && activeCell.timerIsRunning == false {
activeCell.activity.countdownValue = self.activeActivities[indexPath.row].countdownValue
print(String(self.activeActivities[indexPath.row].countdownValue))
activeCell.handleCountdown()
}
activeCell.countdownTimer.text = String(self.activeActivities[indexPath.row].countdownValue)
//activeCell.countdownTimer.text = self.activeActivities[indexPath.row].
return activeCell
The issue I am having is that the countdown label text is not updating, despite it showing the correct output in terminal console. My error is repeatable via this example: there are two active activities, with countdowns functioning independently of each other.
activeActivity[0] runs to zero, then is handled by case 0 in the ojbc func updateTimer, and passed through the delegate that then deletes it from active and moves it to inactive. The remaining one (what used to be activeActivity[1], is now activeActivity[0]) moves "up" in the queue and the timer continues running according to the terminal log, but the countdownLabel.text is stuck at 1s, despite the terminal showing the execution of updateTimer, with it printing the correct start time that SHOULD be displayed in the table view cell.

UIImageView wait for animation complete

I'm trying to execute 2 different animations, after the first complete, using isAnimating.
but, I see only the first animation...
if anims[0] == 1{
startAnimation(image : #imageLiteral(resourceName: "first"))
}
if anims[1] == 2{
while myView.isAnimating {
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05))
}
}
startAnimation(image : #imageLiteral(resourceName: "second") , time : Int)
spriteSheet returns UIImage array after cropping..
func startAnimation(image : UIImage , time : Int){
myView.animationImages = image.spriteSheet(cols: 19, rows: 1)
myView.animationDuration = 1
myView.animationRepeatCount = time
myView.startAnimating()
}
You can always chain animations like
UIView.animate(withDuration: 1, animations: {
//do your animation here
}) { (state) in
UIView.animate(withDuration: 1, animations: {
//do your second animation
})
}
If you are using CABasicAnimations then you can use beginTime and duration to chain them up :)
var totalDuration = 0
let baseicAnim1 = CABasicAnimation()
baseicAnim1.beginTime = CACurrentMediaTime()
totalDuration += 10
baseicAnim1.duration = CFTimeInterval(totalDuration)
let basicAnim2 = CABasicAnimation()
basicAnim2.beginTime = CACurrentMediaTime() + CFTimeInterval(totalDuration)
totalDuration += 10
basicAnim2.duration = CFTimeInterval(totalDuration)
EDIT
Using while loop to keep checking if animation has completed its execution or not is never a suggested approach
EDIT :
Try this,
func startAnimation(image : UIImage , time : Int,completionBlock : (()->())?){
let animationDuration = 1
myView.animationImages = image.spriteSheet(cols: 19, rows: 1)
myView.animationDuration = animationDuration
myView.animationRepeatCount = time
myView.startAnimating()
DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration, execute: {
if let block = completionBlock {
block()
}
})
}
Now your startAnimation function takes completion block as its parameter and executes the completion block after animationDuration. So you can get to know when animation ends :)
to chain simply call
self.startAnimation(image: #imageLiteral(resourceName: "first"), time: 1) {
self.startAnimation(image: #imageLiteral(resourceName: "second"), time: 1, completionBlock: nil)
}
Hope it helps

unable to loop through a Counter in Swift

I guys I have a working countdown timer which I can set manually and countdown from this value.
What I'm trying to achieve now though is set my INTERVALS counter to say 3 and loop through the countdown timer the amount of times that I set in this counter then when the last interval is complete the timer finishes
sounds straight forward in my head I know I need a loop somewhere but everything I have tried just hast worked this is my current code before adding a loop. Have tried adding a while statement instead of the if statement but that didn't do the job
I've now added this loop but still no joy. Anyone know how to do this?
func runIntervalTimer() {
timerForIntervals = NSTimer.scheduledTimerWithTimeInterval(1, target: self,
selector: Selector("updateTimeInterval"), userInfo: nil, repeats: true)
}
func updateTimeInterval() {
do {
if intervalCountdown > 0 {
intervalHasBeenSet = true
intervalCountdown--
IntervalTimer.text = String(format: "%02d:%02d", intervalCountdown/60,
intervalCountdown%60) }
} while intervalHasBeenSet == true
intervalHasBeenSet = false
intervalCountdown = 0
IntervalTimer.text = String(format: "%02d:%02d", intervalCountdown/60,
intervalCountdown%60)
}
#IBAction func InterValTimerIncrease(sender: AnyObject) {
intervalCountdown++
IntervalTimer.text = String(format: "%02d:%02d", intervalCountdown/60,
intervalCountdown%60)
}
#IBAction func IntervalTimerDecrease(sender: AnyObject) {
intervalCountdown--
IntervalTimer.text = String(format: "%02d:%02d", intervalCountdown/60,
intervalCountdown%60)
}
#IBAction func IntervalStart(sender: AnyObject) {
runIntervalTimer()
}
You're probably in an infinite loop here:
do {
if intervalCountdown > 0 {
intervalHasBeenSet = true
intervalCountdown--
IntervalTimer.text = String(format: "%02d:%02d", intervalCountdown/60,
intervalCountdown%60) }
} while intervalHasBeenSet == true
Your condition is intervalHasBeenSet == true but it's never being set to false inside the loop, so it can never exit.
If you want the timer to count down each interval, I think all you need to do is:
func updateTimeInterval() {
println(intervalCountdown)
if intervalCountdown > 0 {
intervalCountdown--
IntervalTimer.text = String(format: "%02d:%02d", intervalCountdown/60,
intervalCountdown%60)
}
}
Because the updateTimeInterval should be called each interval by NSTimer, you shouldn't need a loop inside this function.
To have the timer count down multiple times, you need an extra variable (intervalNum):
func updateTimeInterval() {
println(intervalCountdown)
if intervalCountdown > 0 && intervalNum > 0 {
// countdown in the current interval
intervalCountdown--
IntervalTimer.text = String(format: "%02d:%02d", intervalCountdown/60,
intervalCountdown%60)
}
else
{
// move on to the next interval
intervalNum--
intervalCountdown = 20 // or grab the original value from somewhere
}
if (intervalNum == 0)
{
// stop the timer
timerForIntervals.invalidate()
}
}

Resources