I need to do some time math in iOS, with Swift.
I have to use dispatch_walltime. I hope that can be taken as axiomatic. Where time math is concerned, I think I'm likely to get the response "just use NSDate," but please take it on faith: I am bound to dispatch_walltime.
Now, it's plain why someone might suggest NSDate, because when you're using NSTimeInterval and NSDate and that good stuff, it's pretty easy to make custom timestamps and compare them and do all kinds of time math.
But I have to use dispatch_time_t, and specifically dispatch_walltime created like this:
//Get the timeInterval of now.
let nowInterval = NSDate().timeIntervalSince1970
//Make a timespec from it.
var nowStruct = timespec(tv_sec: Int(nowInterval), tv_nsec: 0)
//Make a walltime definition from that.
let referenceWalltime = dispatch_walltime(&nowStruct, 0)
Later on I need to use that reference time in various ways. For instance, I need to get the time interval between the reference time and whatever time it happens to be.
I am attempting to do this the same way I would with NSTimeInterval, in other words, make a new one and subtract the old one from it:
//Repeat everything from before to make a new wall time.
let newNowInterval = NSDate().timeIntervalSince1970
var newNowStruct = timespec(tv_sec: Int(newNowInterval), tv_nsec: 0)
let newWalltime = dispatch_walltime(& newNowStruct, 0)
//Time math a la NSTimeInterval to find the interval:
let walltimeInterval = newWalltime - referenceWalltime
Will that work?
The short answer is: no. That code will crash.
The better answer is: no, but it can be done, and it's not all that different in the end.
I did some investigating on my own in a Playground and learned some interesting things, and I believe I figured out the right way to do this.
I'm pasting the entirety of my Playground here, so that others can copy-paste it and figure out how to do their own dispatch_time math.
Comments marked by //********* in the code denote the key things I learned.
import UIKit
import XCPlayground
XCPSetExecutionShouldContinueIndefinitely(continueIndefinitely: true)
public class IntervalMaker {
var referenceWalltime: dispatch_time_t = 0
var newWalltime: dispatch_time_t = 0
var walltimeInterval: dispatch_time_t = 0
func scheduleWalltimeSequence () {
let threeSeconds = Int64(NSEC_PER_SEC) * 3
let now = walltimeNow()
let dispatchTimeInThree = dispatch_time(now, threeSeconds)
let dispatchTimeInSix = dispatch_time(now,
2 * threeSeconds)
setReferenceWalltimeToNow()
dispatch_after(dispatchTimeInThree, dispatch_get_main_queue(),
setNewWalltimeToNow)
dispatch_after(dispatchTimeInSix,
dispatch_get_main_queue(), dispatchBasedOnDispatchMath)
}
func walltimeNow()->dispatch_time_t{
let nowInterval = NSDate().timeIntervalSince1970
var nowStruct = timespec(tv_sec: Int(nowInterval), tv_nsec: 0)
return dispatch_walltime(&nowStruct, 0)
}
func setReferenceWalltimeToNow () {
referenceWalltime = walltimeNow()
}
func setNewWalltimeToNow (){
newWalltime = walltimeNow()
}
func dispatchBasedOnDispatchMath () {
computeInterval() //Should be three seconds
let nineTheWrongWay = referenceWalltime + (walltimeInterval * 3)
let nineTheRightWay = dispatch_time(referenceWalltime,
Int64(walltimeInterval) * 3)
dispatch_after(nineTheWrongWay,
dispatch_get_main_queue(), finalPrintln)
//********** THE ABOVE DOES NOT WORK CORRECTLY - prints 6 seconds later
dispatch_after(nineTheRightWay,
dispatch_get_main_queue(), finalPrintln)
//********** THE ABOVE WORKS CORRECTLY - prints 9 seconds later
}
func finalPrintln () {
let now = walltimeNow()
println("I should be printing nine seconds from reference time.")
println("It's actually \(referenceWalltime - now) nanoseconds after")
}
func computeInterval () {
walltimeInterval = referenceWalltime - newWalltime
//********** dispatch_walltimes actually count backwards, and *CANNOT* be
//********** negative: writing `newWalltime - referenceWalltime` will crash
}
}
let intervaller = IntervalMaker()
intervaller.scheduleWalltimeSequence()
Related
There is a NSTimer object in my app that counts elapsed time in seconds.
I want to format an UILabel in my app's interface in the such way it matches the well know standard.
Example
00:01 - one second
01:00 - 60 seconds
01:50:50 - 6650 seconds
I wonder how to do that, do you know any pods/libraries that creates such String based on Int number of seconds?
Obviously I can come with a complicated method myself, but since it's recommended to not reinvent the wheel, I'd prefer to use some ready-to-use solution.
I haven't found anything relevant in Foundation library, nor in HealthKit
Do you have any suggestions how to get it done? If you say "go and write it yourself" - that's ok. But I wanted to be sure I'm not missing any simple, straightforward solution.
thanks in advance
(NS)DateComponentsFormatter can do that:
func timeStringFor(seconds : Int) -> String
{
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.second, .minute, .hour]
formatter.zeroFormattingBehavior = .pad
let output = formatter.string(from: TimeInterval(seconds))!
return seconds < 3600 ? output.substring(from: output.range(of: ":")!.upperBound) : output
}
print(timeStringFor(seconds:1)) // 00:01
print(timeStringFor(seconds:60)) // 01:00
print(timeStringFor(seconds:6650)) // 1:50:50
Figured it out based on this answer, quite simple!
func createTimeString(seconds: Int)->String
{
var h:Int = seconds / 3600
var m:Int = (seconds/60) % 60
var s:Int = seconds % 60
let a = String(format: "%u:%02u:%02u", h,m,s)
return a
}
This question already has answers here:
How can I make a countdown timer like in a music player?
(2 answers)
Closed 7 years ago.
I'm trying to make an app that tell us the rest of time from the present time till one hour later.
This is the code but now it only has a function that tell us the countdown time by decreasing one second from the present time.
I'm thinking that I haven't definite the definition of the "cnt"
so that's why I'm thinking it doesn't work.
Can somebody tell me the reason and a solution?
import UIKit
class ViewController: UIViewController {
var cnt : Int = 0
var timer : NSTimer!//NSTimerというデフォルト機能から引っ張る
var myInt:Int = 0
override func viewDidLoad() {
let myDate: NSDate = NSDate()
let myCalendar: NSCalendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
let myComponents = myCalendar.components([.Year, .Hour, .Minute, .Second],
fromDate: myDate) // myDate、すなわちNSDateから要素として引っ張り出してる
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "onUpdate:", userInfo: nil, repeats: true)//カウントダウンのインターバル
timer.fire()
var myStr: String = "\(myComponents.hour)"
myStr += "\(myComponents.minute)"
myStr += "\(myComponents.second)"
myInt = Int(myStr)! // toInt()がSwift2より無効になったようです。myInt=Str(my components,hour,minute,second)=現時刻
}
func onUpdate(timer : NSTimer){
cnt += 1//cnt+1=cnt,
let count = myInt - cnt //残り時間=現在時刻ー現在時刻に1時間足した時刻
print(count) // println()は、Swift2よりDeprecatedになりました。
}
}
It is difficult to understand what you're asking, but I will do my best.
In your viewDidLoad method, you're setting myInt to the integer representation of myStr. If the time is 18:30:50, myInt will be equal to 183050. That is not an appropriate representation of the time. Time is base 60, integers are base 10, for one thing. If you want to represent time as a single number, you can use timeIntervalSinceDate, or timeIntervalSinceReferenceDate or timeIntervalSince1970 to get the NSTimeInterval (ie. fractional seconds) representation of the date relative to a certain epoch either of your choosing or one built into Foundation.
Subtracting 1 from myInt each time the timer fires isn't going to give you an indication of the time remaining.
Also, NSTimer is not an accurate way to keep time. You should instead save the start date as a property and determine the time remaining based on timeIntervalSinceDate
e.g.
func onUpdate(timer : NSTimer){
let currentTime = NSDate()
let timeElapsed = currentTime.timeIntervalSinceDate(myDate)
println(timeElapsed)
}
If you want to show time elapsed in minutes, you can divide it by 60. You can look into NSDateComponentsFormatter to easily get a string representation of time intervals.
If you want the countdown to stop after an hour, then check for when timeElapsed is over 3600.
If you want it to show a countdown from 1 hour, then subtract the timeElapsed from 3600.
"Walltime" is a little-known time format used by Grand Central Dispatch. Apple talks about it here:
https://developer.apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/
There are some things it's really handy for, though, but it's a sticky wicket. It's hard to make it play nice with other time formats, which is what my question's about.
I can make a walltime by turning an NSDate into a timespec, and then using with dispatch_walltime:
let now = NSDate().timeIntervalSince1970
let nowWholeSecsFloor = floor(now)
let nowNanosOnly = now - nowWholeSecsFloor
let nowNanosFloor = floor(nowNanosOnly * Double(NSEC_PER_SEC))
var thisStruct = timespec(tv_sec: Int(nowWholeSecsFloor),
tv_nsec: Int(nowNanosFloor))
let wallTime = dispatch_walltime(& thisStruct, 0)
But lord love a duck, I can't figure out how to get it back into an NSDate. Here's my try:
public func toNSDate(wallTime: dispatch_time_t)->NSDate {
let wallTimeAsSeconds = Double(wallTime) / Double(NSEC_PER_SEC)
let date = NSDate(timeIntervalSince1970: wallTimeAsSeconds)
return date
}
The resulting NSDate is not just off, but somewhat hilariously off, like five hundred years or something. As Martin R pointed out, the problem is that dispatch_time_t is an opaque value, with an undocumented representation of time.
Does anyone know how to do this?
EDIT: if the process of creating the walltime is confusing, this is basically what's going on:
NSDate defines time with a Double, and everything after the decimal point is the nanoseconds. dispatch_time, which can create a walltime, defines time with UInt64, so you have to convert between Double and UInt64 to use it. To do that conversion you need to use a timespec, which takes seconds and nanoseconds as separate arguments, each of which must be Int.
A whole lotta convertin' going on!
The real answer is: you can't.
In the "time.h" header file it is stated:
/*!
* #typedef dispatch_time_t
*
* #abstract
* A somewhat abstract representation of time; where zero means "now" and
* DISPATCH_TIME_FOREVER means "infinity" and every value in between is an
* opaque encoding.
*/
typedef uint64_t dispatch_time_t;
So dispatch_time_t uses an undocumented "abstract" representation of time, which
may even change between releases.
That being said, let's have some fun and try to figure out what
a dispatch_time_t really is. So we have a look at "time.c", which contains the implementation of
dispatch_walltime():
dispatch_time_t
dispatch_walltime(const struct timespec *inval, int64_t delta)
{
int64_t nsec;
if (inval) {
nsec = inval->tv_sec * 1000000000ll + inval->tv_nsec;
} else {
nsec = (int64_t)_dispatch_get_nanoseconds();
}
nsec += delta;
if (nsec <= 1) {
// -1 is special == DISPATCH_TIME_FOREVER == forever
return delta >= 0 ? DISPATCH_TIME_FOREVER : (dispatch_time_t)-2ll;
}
return (dispatch_time_t)-nsec;
}
The interesting part is the last line: it takes the negative value of the
nanoseconds, and this value is cast back to an (unsigned) dispatch_time_t. There are also some special cases.
Therefore, to reverse the conversion, we have to negate the
dispatch_time_t and take that as nanoseconds:
public func toNSDate(wallTime: dispatch_time_t)->NSDate {
// Tricky part HERE:
let nanoSeconds = -Int64(bitPattern: wallTime)
// Remaining part as in your question:
let wallTimeAsSeconds = Double(nanoSeconds) / Double(NSEC_PER_SEC)
let date = NSDate(timeIntervalSince1970: wallTimeAsSeconds)
return date
}
And indeed, this converts the walltime correctly back to the original
NSDate, at least when I test it in an OS X application.
But again: don't do it! You would rely on an undocumented representation which could change between OS releases. There may also
be special cases that are not considered in the above code.
Also the representation in the iOS runtime could be different, I did
not try that.
You have been warned!
I have run into a problem when trying to reload my UITableView from within my TimersManager.swift file. The TimersManager.swift is used to control/manage all the timers in my to-do list/timers app. I am trying to update the UILabel to show the updated time as the timer ticks away. For some reason it will not update the table. Please have a look below and hopefully you can give me a nudge in the right direction. Thanks.
top of listTableViewController.swift:
var prepMgr: listTableViewController = listTableViewController()
var cell:customCell!
class listTableViewController: UITableViewController, UITableViewDelegate, UITableViewDataSource {
update func in listTableViewController: (this is called by another func in my TimersManager.swift file)
func update (indexPathRow: Int) {
for task in taskMgr.tasks {
if task.timerOn == true {
//calculate the time to display in 0:00:00 format.
let date1 : NSDate = task.timerFinishDate
let date2 : NSDate = NSDate()
let compareResult = date1.compare(date2)
let length = Int(round(date1.timeIntervalSinceDate(date2)))
var tmpHours = length / 3600
var tmpMinutes = (length % 3600) / 60
var tmpSeconds = length % 60
var timeString = "\(tmpHours):\(tmpMinutes):\(tmpSeconds)"
println(task.subText) //test, display old value before update - WORKS
taskMgr.updateTask(indexPathRow, name: taskMgr.tasks[indexPathRow].name, subText: timeString, timerOn: taskMgr.tasks[indexPathRow].timerOn, completed: taskMgr.tasks[indexPathRow].completed, timerFinishDate: taskMgr.tasks[indexPathRow].timerFinishDate, taskID: taskMgr.tasks[indexPathRow].taskID, sliderHours: taskMgr.tasks[indexPathRow].sliderHours, sliderMinutes:taskMgr.tasks[indexPathRow].sliderMinutes, sliderSeconds: taskMgr.tasks[indexPathRow].sliderSeconds)
println(task.subText) //test, display updated value after update - WORKS
println(timeString) //test, display time remaining in timer 0:00:00 - WORKS
}
self.tableView.reloadData() // DOES NOT UPDATE TABLE.
}
}
the code for the NSTimer selector in TimersManager.swift:
func tickTock (length:NSTimer!) {
println(length.userInfo)
var count = 0
for timer in timers {
let date1 : NSDate = timer.fireDate
let date2 : NSDate = NSDate()
let compareResult = date1.compare(date2)
let length = Int(round(date1.timeIntervalSinceDate(date2)))
if length <= 0 {
//Invalidate NSTimer
timer.myTimer.invalidate()
//Remove from array
timers.removeAtIndex(count)
}
count++
println(length) //test, shows how many seconds are left - WORKS
//update labels.
prepMgr.update(timer.indexPathRow) //Call to listTableViewController func - Half working, calls the function. updates the correct task. But table is not reloaded.
}
//update labels, reload table
prepMgr.tableView.reloadData() //Test, Not working
}
You could also use a NSNotification to handle the "Reload Function" of the Table. And just call them if you need an update of your table.
I'm really out of ideas so I'll have to ask you guys again...
I'm building an iPhone application which uses three instances of AVPlayer. They all play at the same time and it's very important that they do so. I used to run this code:
CMClockRef syncTime = CMClockGetHostTimeClock();
CMTime hostTime = CMClockGetTime(hostTime);
[self.playerOne setRate:1.0f time:kCMTimeInvalid atHostTime:hostTime];
[self.playerTwo setRate:1.0f time:kCMTimeInvalid atHostTime:hostTime];
[self.playerThree setRate:1.0f time:kCMTimeInvalid atHostTime:hostTime];
which worked perfectly. But a few days ago it just stopped working, the three players are delayed by about 300-400ms (which is way to much, everything under 100ms would be okay). Two of these AVPlayer have some Audio processing, which takes some time more than the "normal" AVPlayer, but it used to work before and the currentTime property tells me, that these players are delayed, so the syncing seems to fail.
I have no idea why it stopped working, I didn't really changed something, but I'm using an observer where i can ask the self.playerX.currentTime property, which gives me a delay of about .3-.4 seconds... I already tried to resync the players if delay>.1f but the delay is still there. So I think the audio processing of player1 and 2 can't be responsable for the delay, as the currentTime property does know they are delayed (i hope you know what I mean). Maybe someone of you guys know why I'm having such a horrible delay, or is able to provide me another idea.
Thanks in advance!
So, I found the solution. I forgot to [self.playerX prerollAtRate:]. I thought if the observer is AVPlayerReadyToPlay it means, that the player is "really" ready. In fact, it does not. After AVPlayer is readyToPlay, it has to be pre rolled. Once that is done you can sync your placer. The delay is now somewhere at 0.000006 seconds.
Full func to sync avplayer's across multiple iOS devices
private func startTribePlayer() {
let dateFormatterGet = DateFormatter()
dateFormatterGet.dateFormat = "yyyy-MM-dd"
guard let refDate = dateFormatterGet.date(from: "2019-01-01") else { return }
let tsRef = Date().timeIntervalSince(refDate)
//currentDuration is avplayeritem.duration().seconds
let remainder = tsRef.truncatingRemainder(dividingBy: currentDuration)
let ratio = remainder / currentDuration
let seekTime = ratio * currentDuration
let bufferTime = 0.5
let bufferSeekTime = seekTime + bufferTime
let mulFactor = 10000.0
let timeScale = CMTimeScale(mulFactor)
let seekCMTime = CMTime(value: CMTimeValue(CGFloat(bufferSeekTime * mulFactor)), timescale: timeScale)
let syncTime = CMClockGetHostTimeClock()
let hostTime = CMClockGetTime(syncTime)
tribeMusicPlayer?.seek(to: seekCMTime, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { [weak self] (successSeek) in
guard let tvc = self, tvc.tribeMusicPlayer?.currentItem?.status == .readyToPlay else { return }
tvc.tribeMusicPlayer?.preroll(atRate: 1.0, completionHandler: { [tvc] (successPreroll) in
tvc.tribePlayerDidPlay = true
tvc.tribeMusicPlayer?.setRate(1.0, time: seekCMTime, atHostTime: hostTime)
})
})
}