I'm looking for an easy way to parse a string that contains an ISO-8601 duration in Objective C. The result should be something usable like a NSTimeInterval.
An example of an ISO-8601 duration: P1DT13H24M17S, which means 1 day, 13 hours, 24 minutes and 17 seconds.
Swift3,4,5 implementation:
https://github.com/Igor-Palaguta/YoutubeEngine/blob/master/Source/YoutubeEngine/Parser/NSDateComponents%2BISO8601.swift
Example:
let components = try DateComponents(ISO8601String: "P1Y2M3DT4H5M6S")
Tests:
https://github.com/Igor-Palaguta/YoutubeEngine/blob/master/Tests/YoutubeEngineTests/ISO8601DurationTests.swift
Update: fixed for DougSwith case "P3W3DT20H31M21"
A pure Objective C version...
NSString *duration = #"P1DT10H15M49S";
int i = 0, days = 0, hours = 0, minutes = 0, seconds = 0;
while(i < duration.length)
{
NSString *str = [duration substringWithRange:NSMakeRange(i, duration.length-i)];
i++;
if([str hasPrefix:#"P"] || [str hasPrefix:#"T"])
continue;
NSScanner *sc = [NSScanner scannerWithString:str];
int value = 0;
if ([sc scanInt:&value])
{
i += [sc scanLocation]-1;
str = [duration substringWithRange:NSMakeRange(i, duration.length-i)];
i++;
if([str hasPrefix:#"D"])
days = value;
else if([str hasPrefix:#"H"])
hours = value;
else if([str hasPrefix:#"M"])
minutes = value;
else if([str hasPrefix:#"S"])
seconds = value;
}
}
NSLog(#"%#", [NSString stringWithFormat:#"%d days, %d hours, %d mins, %d seconds", days, hours, minutes, seconds]);
This version parse every youtube duration without errors.
Important: This version use ARC.
- (NSString*)parseISO8601Time:(NSString*)duration
{
NSInteger hours = 0;
NSInteger minutes = 0;
NSInteger seconds = 0;
//Get Time part from ISO 8601 formatted duration http://en.wikipedia.org/wiki/ISO_8601#Durations
duration = [duration substringFromIndex:[duration rangeOfString:#"T"].location];
while ([duration length] > 1) { //only one letter remains after parsing
duration = [duration substringFromIndex:1];
NSScanner *scanner = [[NSScanner alloc] initWithString:duration];
NSString *durationPart = [[NSString alloc] init];
[scanner scanCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:#"0123456789"] intoString:&durationPart];
NSRange rangeOfDurationPart = [duration rangeOfString:durationPart];
duration = [duration substringFromIndex:rangeOfDurationPart.location + rangeOfDurationPart.length];
if ([[duration substringToIndex:1] isEqualToString:#"H"]) {
hours = [durationPart intValue];
}
if ([[duration substringToIndex:1] isEqualToString:#"M"]) {
minutes = [durationPart intValue];
}
if ([[duration substringToIndex:1] isEqualToString:#"S"]) {
seconds = [durationPart intValue];
}
}
return [NSString stringWithFormat:#"%02d:%02d:%02d", hours, minutes, seconds];
}
If you know exactly which fields you'll be getting, you can use one invocation of sscanf():
const char *stringToParse = ...;
int days, hours, minutes, seconds;
NSTimeInterval interval;
if(sscanf(stringToParse, "P%dDT%dH%dM%sS", &days, &hours, &minutes, &seconds) == 4)
interval = ((days * 24 + hours) * 60 + minutes) * 60 + seconds;
else
; // handle error, parsing failed
If any of the fields might be omitted, you'll need to be a little smarter in your parsing, e.g.:
const char *stringToParse = ...;
int days = 0, hours = 0, minutes = 0, seconds = 0;
const char *ptr = stringToParse;
while(*ptr)
{
if(*ptr == 'P' || *ptr == 'T')
{
ptr++;
continue;
}
int value, charsRead;
char type;
if(sscanf(ptr, "%d%c%n", &value, &type, &charsRead) != 2)
; // handle parse error
if(type == 'D')
days = value;
else if(type == 'H')
hours = value;
else if(type == 'M')
minutes = value;
else if(type == 'S')
seconds = value;
else
; // handle invalid type
ptr += charsRead;
}
NSTimeInterval interval = ((days * 24 + hours) * 60 + minutes) * 60 + seconds;
slightly modifying function of the user
Sergei Pekar
+ (NSString*)parseISO8601Time:(NSString*)duration
{
NSInteger hours = 0;
NSInteger minutes = 0;
NSInteger seconds = 0;
//Get Time part from ISO 8601 formatted duration http://en.wikipedia.org/wiki/ISO_8601#Durations
if ([duration rangeOfString:#"T"].location == NSNotFound || [duration rangeOfString:#"P"].location == NSNotFound) {
NSLog(#"Time is not a part from ISO 8601 formatted duration");
return #"0:00 Error";
}
duration = [duration substringFromIndex:[duration rangeOfString:#"T"].location];
while ([duration length] > 1) { //only one letter remains after parsing
duration = [duration substringFromIndex:1];
NSScanner *scanner = [[NSScanner alloc] initWithString:duration];
NSString *durationPart = [[NSString alloc] init];
[scanner scanCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:#"0123456789"] intoString:&durationPart];
NSRange rangeOfDurationPart = [duration rangeOfString:durationPart];
if ((rangeOfDurationPart.location + rangeOfDurationPart.length) > duration.length) {
NSLog(#"Time is not a part from ISO 8601 formatted duration");
return #"0:00 Error";
}
duration = [duration substringFromIndex:rangeOfDurationPart.location + rangeOfDurationPart.length];
if ([[duration substringToIndex:1] isEqualToString:#"H"]) {
hours = [durationPart intValue];
}
if ([[duration substringToIndex:1] isEqualToString:#"M"]) {
minutes = [durationPart intValue];
}
if ([[duration substringToIndex:1] isEqualToString:#"S"]) {
seconds = [durationPart intValue];
}
}
if (hours != 0)
return [NSString stringWithFormat:#"%ld:%02ld:%02ld", (long)hours, (long)minutes, (long)seconds];
else
return [NSString stringWithFormat:#"%ld:%02ld", (long)minutes, (long)seconds];
}
Here is an example for swift:
(only for hours, minutes and seconds)
func parseDuration(duration: String) -> Int {
var days = 0
var hours = 0
var minutes = 0
var seconds = 0
var decisionMaker = 0
var factor = 1
let specifiers: [Character] = ["M", "H", "T", "P"]
let length = count(duration)
for i in 1...length {
let index = advance(duration.startIndex, length - i)
let char = duration[index]
for specifier in specifiers {
if char == specifier {
decisionMaker++
factor = 1
}
}
if let value = String(char).toInt() {
switch decisionMaker {
case 0:
seconds += value * factor
factor *= 10
case 1:
minutes += value * factor
factor *= 10
case 2:
hours += value * factor
factor *= 10
case 4:
days += value * factor
factor *= 10
default:
break
}
}
}
return seconds + (minutes * 60) + (hours * 3600) + (days * 3600 * 24)
}
Here is swift 3 version of headkaze example: This format was most suitable in my case:
private func parseISO8601Time(iso8601: String) -> String {
let nsISO8601 = NSString(string: iso8601)
var days = 0, hours = 0, minutes = 0, seconds = 0
var i = 0
while i < nsISO8601.length {
var str = nsISO8601.substring(with: NSRange(location: i, length: nsISO8601.length - i))
i += 1
if str.hasPrefix("P") || str.hasPrefix("T") { continue }
let scanner = Scanner(string: str)
var value = 0
if scanner.scanInt(&value) {
i += scanner.scanLocation - 1
str = nsISO8601.substring(with: NSRange(location: i, length: nsISO8601.length - i))
i += 1
if str.hasPrefix("D") {
days = value
} else if str.hasPrefix("H") {
hours = value
} else if str.hasPrefix("M") {
minutes = value
} else if str.hasPrefix("S") {
seconds = value
}
}
}
if days > 0 {
hours += 24 * days
}
if hours > 0 {
return String(format: "%d:%02d:%02d", hours, minutes, seconds)
}
return String(format: "%d:%02d", minutes, seconds)
}
I looked up this Wikipedia article for a reference to how ISO-8601 actually works. I'm no Cocoa expert, but I'm betting if you can parse that string and extract the component hour, minute, second, day, etc., getting it in to an NSTimeInterval should be easy. The tricky part is parsing it. I'd probably do it something like this:
First, split the string in to two separate strings: one representing the days, and one representing the times. NSString has an instance method componentsSeparatedByString:NSString that returns an NSArray of substrings of your original NSString separated by the parameter you pass in. It would look something like this:
NSString* iso8601 = /*However you're getting your string in*/
NSArray* iso8601Parts = [iso8601 componentsSeparatedByString:#"T"];
Next, search the first element of iso8601Parts for each of the possible day duration indicators (Y, M, W, and D). When you find one, grab all the preceeding digits (and possibly a decimal point), cast them to a float, and store them somewhere. Remember that if there was only a time element, then iso8601Parts[0] will be the empty string.
Then, do the same thing looking for time parts in the second element of iso8601Parts for possible time indicators (H, M, S). Remember that if there was only a day component (that is, there was no 'T' character in the original string), then iso8601Parts will only be of length one, and an attempt to access the second element will cause an out of bounds exception.
An NSTimeInterval is just a long storing a number of seconds, so convert the individual pieces you pulled out in to seconds, add them together, store them in your NSTimeInterval, and you're set.
Sorry, I know you asked for an "easy" way to do it, but based on my (admittedly light) searching around and knowledge of the API, this is the easiest way to do it.
Quick and dirty implementation
- (NSInteger)integerFromYoutubeDurationString:(NSString*)duration{
if(duration == nil){
return 0;
}
NSString *startConst = #"PT";
NSString *hoursConst = #"H";
NSString *minutesConst = #"M";
NSString *secondsConst = #"S";
NSString *hours = nil;
NSString *minutes = nil;
NSString *seconds = nil;
NSInteger totalSeconds = 0;
NSString *clean = [duration componentsSeparatedByString:startConst][1];
if([clean containsString:hoursConst]){
hours = [clean componentsSeparatedByString:hoursConst][0];
clean = [clean componentsSeparatedByString:hoursConst][1];
totalSeconds = [hours integerValue]*3600;
}
if([clean containsString:minutesConst]){
minutes = [clean componentsSeparatedByString:minutesConst][0];
clean = [clean componentsSeparatedByString:minutesConst][1];
totalSeconds = totalSeconds + [minutes integerValue]*60;
}
if([clean containsString:secondsConst]){
seconds = [clean componentsSeparatedByString:secondsConst][0];
totalSeconds = totalSeconds + [seconds integerValue];
}
return totalSeconds;
}
There are answers already, but I ended up implementing yet another version using NSScanner. This version ignores year and month since they cannot be converted to number of seconds.
static NSTimeInterval timeIntervalFromISO8601Duration(NSString *duration) {
NSTimeInterval timeInterval = 0;
NSScanner *scanner = [NSScanner scannerWithString:duration];
NSCharacterSet *designators = [NSCharacterSet characterSetWithCharactersInString:#"PYMWDTHMS"];
BOOL isScanningTime = NO;
while (![scanner isAtEnd]) {
double scannedNumber = 0;
BOOL didScanNumber = [scanner scanDouble:&scannedNumber];
NSString *scanned = nil;
if ([scanner scanCharactersFromSet:designators intoString:&scanned]) {
if (didScanNumber) {
switch ([scanned characterAtIndex:0]) {
case 'D':
timeInterval += scannedNumber * 60 * 60 * 24;
break;
case 'H':
timeInterval += scannedNumber * 60 * 60;
break;
case 'M':
if (isScanningTime) {
timeInterval += scannedNumber * 60;
}
break;
case 'S':
timeInterval += scannedNumber;
break;
default:
break;
}
}
if ([scanned containsString:#"T"]) {
isScanningTime = YES;
}
}
}
return timeInterval;
}
Now in Swift! (Yes it's a little long, but it handles all cases and singular/plural).
Handles Years, Months, Weeks, Days, Hours, Minutes, and Seconds!
func convertFromISO8601Duration(isoValue: AnyObject) -> String? {
var displayedString: String?
var hasHitTimeSection = false
var isSingular = false
if let isoString = isoValue as? String {
displayedString = String()
for val in isoString {
if val == "P" {
// Do nothing when parsing the 'P'
continue
}else if val == "T" {
// Indicate that we are now dealing with the 'time section' of the ISO8601 duration, then carry on.
hasHitTimeSection = true
continue
}
var tempString = String()
if val >= "0" && val <= "9" {
// We need to know whether or not the value is singular ('1') or not ('11', '23').
if let safeDisplayedString = displayedString as String!
where count(displayedString!) > 0 && val == "1" {
let lastIndex = count(safeDisplayedString) - 1
let lastChar = safeDisplayedString[advance(safeDisplayedString.startIndex, lastIndex)]
//test if the current last char in the displayed string is a space (" "). If it is then we will say it's singular until proven otherwise.
if lastChar == " " {
isSingular = true
} else {
isSingular = false
}
}
else if val == "1" {
// if we are just dealing with a '1' then we will say it's singular until proven otherwise.
isSingular = true
}
else {
// ...otherwise it's a plural duration.
isSingular = false
}
tempString += "\(val)"
displayedString! += tempString
} else {
// handle the duration type text. Make sure to use Months & Minutes correctly.
switch val {
case "Y", "y":
if isSingular {
tempString += " Year "
} else {
tempString += " Years "
}
break
case "M", "m":
if hasHitTimeSection {
if isSingular {
tempString += " Minute "
} else {
tempString += " Minutes "
}
}
else {
if isSingular {
tempString += " Month "
} else {
tempString += " Months "
}
}
break
case "W", "w":
if isSingular {
tempString += " Week "
} else {
tempString += " Weeks "
}
break
case "D", "d":
if isSingular {
tempString += " Day "
} else {
tempString += " Days "
}
break
case "H", "h":
if isSingular {
tempString += " Hour "
} else {
tempString += " Hours "
}
break
case "S", "s":
if isSingular {
tempString += " Second "
} else {
tempString += " Seconds "
}
break
default:
break
}
// reset our singular flag, since we're starting a new duration.
isSingular = false
displayedString! += tempString
}
}
}
return displayedString
}
Swift 4.2 version
Works with years, months, days, hours, minutes, seconds.
Seconds can be float number.
extension String{
public func parseISO8601Time() -> Duration {
let nsISO8601 = NSString(string: self)
var days = 0, hours = 0, minutes = 0, seconds: Float = 0, weeks = 0, months = 0, years = 0
var i = 0
var beforeT:Bool = true
while i < nsISO8601.length {
var str = nsISO8601.substring(with: NSRange(location: i, length: nsISO8601.length - i))
i += 1
if str.hasPrefix("P") || str.hasPrefix("T") {
beforeT = !str.hasPrefix("T")
continue
}
let scanner = Scanner(string: str)
var value: Float = 0
if scanner.scanFloat(&value) {
i += scanner.scanLocation - 1
str = nsISO8601.substring(with: NSRange(location: i, length: nsISO8601.length - i))
i += 1
if str.hasPrefix("Y") {
years = Int(value)
} else if str.hasPrefix("M") {
if beforeT{
months = Int(value)
}else{
minutes = Int(value)
}
} else if str.hasPrefix("W") {
weeks = Int(value)
} else if str.hasPrefix("D") {
days = Int(value)
} else if str.hasPrefix("H") {
hours = Int(value)
} else if str.hasPrefix("S") {
seconds = value
}
}
}
return Duration(years: years, months: months, weeks: weeks, days: days, hours: hours, minutes: minutes, seconds: seconds)
}
Duration struct:
public struct Duration {
let daysInMonth: Int = 30
let daysInYear: Int = 365
var years: Int
var months: Int
var weeks: Int
var days: Int
var hours: Int
var minutes: Int
var seconds: Float
public func getMilliseconds() -> Int{
return Int(round(seconds*1000)) + minutes*60*1000 + hours*60*60*1000 + days*24*60*60*1000 + weeks*7*24*60*60*1000 + months*daysInMonth*24*60*60*1000 + years*daysInYear*24*60*60*1000
}
public func getFormattedString() -> String{
var formattedString = ""
if years != 0{
formattedString.append("\(years)")
formattedString.append(" ")
formattedString.append(years == 1 ? "year".localized() : "years".localized())
formattedString.append(" ")
}
if months != 0{
formattedString.append("\(months)")
formattedString.append(" ")
formattedString.append(months == 1 ? "month".localized() : "months".localized())
formattedString.append(" ")
}
if weeks != 0{
formattedString.append("\(weeks)")
formattedString.append(" ")
formattedString.append(weeks == 1 ? "week".localized() : "weeks".localized())
formattedString.append(" ")
}
if days != 0{
formattedString.append("\(days)")
formattedString.append(" ")
formattedString.append(days == 1 ? "day".localized() : "days".localized())
formattedString.append(" ")
}
if seconds != 0{
formattedString.append(String(format: "%02d:%02d:%.02f", hours, minutes, seconds))
}else{
formattedString.append(String(format: "%02d:%02d", hours, minutes))
}
return formattedString
}
}
Related
stackoverflow is my last chance. I hope you can help me. I have a countdown in my app which is set to 2 hours and 30 minutes. When it hits 0 it starts to count up to 30 minutes. After the 30 minutes are over, the countdown is back at 2:30. Everything is working fine except the part when the timer is switching from the countdown part to the countup part. Here is the problem: The timer is going negative when hitting zero.
#objc func startTimer() {
print("Countdown : \(totalTime)")
countdownTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
}
#objc func updateTime() {
if (totalTime > 10800) {
totalTime = totalTime - 10800
}
totalTime -= 1
var time = totalTime
//When in session...
if (time > 9000) {
//Calculate CountUP
time = 10800 - time
//if session has started
if(isSessionActive == false) {
isSessionActive = true
lastFiveMinutes = false
self.setSessionStarted()
}
} else {
//If session has ended...
if(isSessionActive == true) {
isSessionActive = false
lastFiveMinutes = false
self.setSessionEnded()
}
//If last 5 minutes have started
if(time < 300 && lastFiveMinutes == false) {
lastFiveMinutes = true
self.setLastFiveMinutes()
}
}
countdownLabel.text = "\(timeFormatted(time))"
//update Participant Count every x second, ONLY if in last 5 Minutes or Session is active
if (lastFiveMinutes == true || isSessionActive == true) {
if (totalTime % 5 == 0) {
setParticipants(n: httpController.getPar())
}
} else {
setParticipants(n: "...")
}
}
The part "var time = totalTime" is not necessary, i know.
In Android the code is exactly the same except the -=1 part, but it is working.
* Update *
Inital Value of totalTime:
#objc var totalTime = 0
And here is the next part where it gets a real value.
#objc func initCountdown() {
var currentSec = 0.0
// Get Current Time from Server
serverTimeReturn { (getResDate) -> Void in
let dFormatter = DateFormatter()
dFormatter.dateStyle = DateFormatter.Style.long
dFormatter.timeStyle = DateFormatter.Style.long
dFormatter.timeZone = NSTimeZone(abbreviation: "GMT")! as TimeZone
let dateGet = dFormatter.string(from: getResDate! as Date)
currentSec = Double((getResDate?.timeIntervalSince1970)!)
print("Formatted Hour : \(dateGet)")
let todaySec = Int(currentSec) % 86400
print("Today Sec Hour : \(todaySec)")
let countDownSec = 10800 - todaySec % 10800
// Check status to init
if(countDownSec > 9000) {
self.isSessionActive = true
self.setSessionStarted()
} else {
self.setSessionEnded()
}
if(countDownSec < 300) {
self.setLastFiveMinutes()
self.lastFiveMinutes = true
}
self.totalTime = countDownSec
}
print("Formatted Hour : \(totalTime)")
startTimer()
}
I don't see any code where you count upwards so I am not surprised it isn't working. I would introduce a boolean property isCountingDown to keep track of how to handle the timer and then have two separate methods for up and down
var isCountingDown: boolean
#objc func initCountdown() {
//existing code
isCountingDown = true
startTimer()
}
#objc func updateTime() {
if isCountingDown {
doCountDown()
} else {
doCountUp()
}
}
And then in doCountDown() and doCountUp() I would check against time is 0 or 30 and update isCountingDown accordingly
func doCountDown {
if (totalTime > 10800) {
totalTime = totalTime - 10800
}
totalTime -= 1
if totalTime == 0 {
isCountingDown = false
//Maybe return here?
}
var time = totalTime
//rest of code
}
func doCountUp() {
totalTime += 1
if totalTime == 30 {
isCountingDown = true
//Maybe return here?
}
//rest of code
}
I'm having difficulty wording this, but here goes.
I have an array of Datapoint objects and each of these objects has a createdAt NSDate property. The array has about 10 or so Datapoint objects for each day of the past week.
I want to filter the array so that there is only one datapoint for each day of the week.
I was thinking something along the lines of:
let today = NSDate()
var endRange = today.dateByAddingTimeInterval(-7 * 24 * 60 * 60)
var allThisWeeksPoints = datapoints.filter({ $0.createdAt >= endRange && $0.createdAt < today })
let c = NSCalendar.currentCalendar()
var thisWeeksPoints : [Datapoint] = []
while !c.isDate(endRange, inSameDayAsDate: today) {
//take one datapoint, eliminate all the other datapoints in the same day and then increment the endRange date by a day
}
Here is the code for my Datapoint object:
class Datapoint: NSObject {
let object : PFObject
let objectId : String
let userId : String
let createdAt : NSDate
let totalPosts : Int
let followerCount : Int
let followingCount : Int
let totalLikes : Int
let averageLikes : Float
let totalComments : Int
let averageComments : Float
}
Step One:
Make NSDate Comparable Found here
public func ==(lhs: NSDate, rhs: NSDate) -> Bool {
return lhs === rhs || lhs.compare(rhs) == .OrderedSame
}
public func <(lhs: NSDate, rhs: NSDate) -> Bool {
return lhs.compare(rhs) == .OrderedAscending
}
extension NSDate: Comparable { }
A few general remarks:
Use an input and output.
Respect upper and lower camel naming conventions
Split the function in two parts, one for dividing the points in weeks, another to apply the filter. This makes it easier to maintain.
Use dateByAddingUnit as suggested in the comments. This will prevent localisation issues. Not every week is the same and not every calendar used in the world uses the same system of weeks.
func weeklyPoints(dataPoints: [DataPoint]) -> [[DataPoint]]? {
guard let firstPoint = dataPoints.first else { // alternative to an .isEmpty check since we need the first point anyway
return nil
}
guard let gregorian = NSCalendar(identifier:NSCalendarIdentifierGregorian) else {
return nil
}
var weeklies : [[DataPoint]] = [[]]
var weekStart : NSDate = firstPoint.createdAt // force unwrap allowed because of the guard at the beginning
var maybeWeekEnd : NSDate? = gregorian.dateByAddingUnit(.WeekOfYear, value: 1, toDate: weekStart, options: [])
for point in dataPoints {
guard let weekEnd = maybeWeekEnd else {
break
}
guard point.createdAt >= weekStart && point.createdAt < weekEnd else {
weekStart = weekEnd
maybeWeekEnd = gregorian.dateByAddingUnit(.WeekOfYear, value: 1, toDate: weekStart, options: [])
weeklies.append([])
continue
}
let currentWeekIndex = (weeklies.count - 1)
weeklies[currentWeekIndex].append(point)
}
return weeklies
}
The very simple filter function.
func onePointPerWeek(dataPoints: [DataPoint]) -> [DataPoint]? {
guard let weeklies = weeklyPoints(dataPoints) else {
return nil
}
var pointPerWeek : [DataPoint] = []
for week in weeklies {
guard let point = week.first else { // maybe sort them first if needed
continue
}
pointPerWeek.append(point)
}
return pointPerWeek
}
So I figured out a way to do it:
func thisWeeksPoints() -> [Datapoint] {
let today = NSDate()
var endRange = today.dateByAddingTimeInterval(-7 * 24 * 60 * 60)
var allThisWeeksPoints = datapoints.filter({ $0.createdAt >= endRange && $0.createdAt < today })
let c = NSCalendar.currentCalendar()
var thisWeeksPoints : [Datapoint] = []
while !c.isDate(endRange, inSameDayAsDate: today) {
if let point = allThisWeeksPoints.first {
allThisWeeksPoints = allThisWeeksPoints.filter({!c.isDate(point.createdAt, inSameDayAsDate: $0.createdAt)})
thisWeeksPoints.append(point)
}
endRange = endRange.dateByAddingTimeInterval(24 * 60 * 60)
}
return thisWeeksPoints
}
How do I convert this to Swift:
NSString *searchString = [NSString stringWithFormat:#"%#", #"Apple_HFS "];
NSRange range = [tempString rangeOfString:searchString];
NSUInteger *idx = range.location + range.length;
Thanks
If you use String, you can just reference endIndex:
let searchString: String = "Apple_HFS "
if let range: Range<String.Index> = tempString.rangeOfString(searchString) {
let index = range.endIndex
let stringAfter = tempString.substringFromIndex(index)
// do something with `stringAfter`
} else {
// not found
}
I included the types so you could see what's going on, but generally I'd just write:
let searchString = "Apple_HFS "
if let range = tempString.rangeOfString(searchString) {
let stringAfter = tempString.substringFromIndex(range.endIndex)
// do something with `stringAfter`
} else {
// not found
}
Is that what you're looking for ?
var str : NSString = "A string that include the searched string Apple_HFS inside"
let searchString : NSString = "Apple_HFS "
let range : NSRange = str.rangeOfString(searchString) // (42, 10)
let idx : Int = range.location + range.length // 52
Demonstration : http://swiftstub.com/831969865/
Simple solution without using ANY objc methods or types:
Suppose you have:
let nsrange = NSRange(location: 3, length: 5)
let string = "My string"
And now you need to convert NSRange to Range:
let range = Range(start: advance(string.startIndex, nsrange.location), end: advance(string.startIndex, nsrange.location + nsrange.length))
This is my code in the viewDidLoad :
AVPlayerItem* playerItem = [AVPlayerItem playerItemWithURL:[NSURL URLWithString:#"http://groove.wavestreamer.com:7321/listen.pls?sid=1"]];
[playerItem addObserver:self forKeyPath:#"timedMetadata" options:NSKeyValueObservingOptionNew context:nil];
music = [[AVPlayer playerWithPlayerItem:playerItem] retain];
[music play];
My question:
How can I create a button, that fast-forwards / fast-backwards the stream 5 seconds, when it´s pressed?
Thank you for your answers... :)
EDIT: How can I add to my current time...
CMTime currentTime = music.currentTime;
...the 5 seconds?
In Swift,
fileprivate let seekDuration: Float64 = 5
#IBAction func doForwardJump(_ sender: Any) {
guard let duration = player.currentItem?.duration else{
return
}
let playerCurrentTime = CMTimeGetSeconds(player.currentTime())
let newTime = playerCurrentTime + seekDuration
if newTime < CMTimeGetSeconds(duration) {
let time2: CMTime = CMTimeMake(Int64(newTime * 1000 as Float64), 1000)
player.seek(to: time2)
}
}
#IBAction func doBackwardJump(_ sender: Any) {
let playerCurrentTime = CMTimeGetSeconds(player.currentTime())
var newTime = playerCurrentTime - seekDuration
if newTime < 0 {
newTime = 0
}
let time2: CMTime = CMTimeMake(Int64(newTime * 1000 as Float64), 1000)
player.seek(to: time2)
}
In Objective-C,
#define seekDuration (float)5
- (IBAction)backwardButtonAction:(UIButton *)sender {
float playerCurrentTime = [self getCurrentTime];
float newTime = playerCurrentTime - seekDuration;
if (newTime < 0) {
newTime = 0;
}
CMTime time = CMTimeMake(newTime*1000, 1000);
[self.player seekToTime:time completionHandler:^(BOOL finished) {
dispatch_async(dispatch_get_main_queue(), ^{
playerSliderisScrubbing = NO;
});
}];
}
- (IBAction)forwardButtonAction:(UIButton *)sender {
float duration = [self getPlayerDuration];
float playerCurrentTime = [self getCurrentTime];
float newTime = playerCurrentTime + seekDuration;
if (newTime < duration) {
CMTime time = CMTimeMake(newTime*1000, 1000);
[self.player seekToTime:time completionHandler:^(BOOL finished) {
dispatch_async(dispatch_get_main_queue(), ^{
playerSliderisScrubbing = NO;
});
}];
}
}
- (float)getCurrentTime {
float seconds = 0;
if (_player) {
seconds = CMTimeGetSeconds([_player currentTime]);
}
return seconds;
}
- (float)getPlayerDuration {
float seconds = 0;
if (_player) {
seconds = CMTimeGetSeconds([[_player currentItem] duration]);
}
return seconds;
}
Swift 4, 4.2 & 5
var player : AVPlayer!
#IBAction func fastForwardBtn(_ sender: UIButton) {
let moveForword : Float64 = 5
if player == nil { return }
if let duration = player!.currentItem?.duration {
let playerCurrentTime = CMTimeGetSeconds(player!.currentTime())
let newTime = playerCurrentTime + moveForword
if newTime < CMTimeGetSeconds(duration)
{
let selectedTime: CMTime = CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000)
player!.seek(to: selectedTime)
}
player?.pause()
player?.play()
}
}
#IBAction func rewindBtn(_ sender: UIButton) {
let moveBackword: Float64 = 5
if player == nil
{
return
}
let playerCurrenTime = CMTimeGetSeconds(player!.currentTime())
var newTime = playerCurrenTime - moveBackword
if newTime < 0
{
newTime = 0
}
player?.pause()
let selectedTime: CMTime = CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000)
player?.seek(to: selectedTime)
player?.play()
}
Use AVPlayer method seekToTime
AVPlayer *player=..;
[player seekToTime:time toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
here is a reference
Hope it helps
You can use avPlayer.rate to fast backward/forward
Rates: 1.0 normal
0.0 pause
But first, you should check if avPlayerItem can do one of your action
For more info: https://developer.apple.com/LIBRARY/ios/documentation/AVFoundation/Reference/AVPlayer_Class/index.html#//apple_ref/occ/instp/AVPlayer/rate
Swift 5 function to seek forwards or backwards
private let seekDuration: Float64 = 15
private func seekPlayback(isForward: Bool) {
guard
let player = player,
let duration = player.currentItem?.duration
else {
return
}
let currentElapsedTime = player.currentTime().seconds
var destinationTime = isForward ? (currentElapsedTime + seekDuration) : (currentElapsedTime - seekDuration)
if destinationTime < 0 {
destinationTime = 0
}
if destinationTime < duration.seconds {
let newTime = CMTime(value: Int64(destinationTime * 1000 as Float64), timescale: 1000)
player.seek(to: newTime)
}
}
I'm successfully using AVPlayer to stream audio from a server and what I want to do now is to show a custom UISlider who shows the progress of the buffering.
Something like this:
With AVPlayer there doesn't seem to be a way to get the total download size or the current downloaded amount for the audio file, only the current playing time and total play time.
There's any workarounds for this?
I am just working on this, and so far have the following:
- (NSTimeInterval) availableDuration;
{
NSArray *loadedTimeRanges = [[self.player currentItem] loadedTimeRanges];
CMTimeRange timeRange = [[loadedTimeRanges objectAtIndex:0] CMTimeRangeValue];
Float64 startSeconds = CMTimeGetSeconds(timeRange.start);
Float64 durationSeconds = CMTimeGetSeconds(timeRange.duration);
NSTimeInterval result = startSeconds + durationSeconds;
return result;
}
It should work well:
Objective-C:
- (CMTime)availableDuration
{
NSValue *range = self.player.currentItem.loadedTimeRanges.firstObject;
if (range != nil){
return CMTimeRangeGetEnd(range.CMTimeRangeValue);
}
return kCMTimeZero;
}
Swift version:
func availableDuration() -> CMTime
{
if let range = self.player?.currentItem?.loadedTimeRanges.first {
return CMTimeRangeGetEnd(range.timeRangeValue)
}
return .zero
}
To watch current time value you can use:
CMTimeShow([self availableDuration]);
or CMTimeShow(availableDuration()) (for swift)
Personally I do not agree that the timeRanges value will always have a count of 1.
According to the documentation
The array contains NSValue objects containing a CMTimeRange value indicating the times ranges for which the player item has media data readily available. The time ranges returned may be discontinuous.
So this may have values similar to:
[(start1, end1), (start2, end2)]
From my experience with the hls.js framework within the desktop web world, the holes between these time ranges could be very small or large depending on a multitude of factors, ex: seeking, discontinuities, etc.
So to correctly get the total buffer length you would need to loop through the array and get the duration of each item and concat.
If you are looking for a buffer value from current play head you would need to filter the time ranges for a start time that's greater than the current time and an end time that's less than current time.
public extension AVPlayerItem {
public func totalBuffer() -> Double {
return self.loadedTimeRanges
.map({ $0.timeRangeValue })
.reduce(0, { acc, cur in
return acc + CMTimeGetSeconds(cur.start) + CMTimeGetSeconds(cur.duration)
})
}
public func currentBuffer() -> Double {
let currentTime = self.currentTime()
guard let timeRange = self.loadedTimeRanges.map({ $0.timeRangeValue })
.first(where: { $0.containsTime(currentTime) }) else { return -1 }
return CMTimeGetSeconds(timeRange.end) - currentTime.seconds
}
}
This method will return buffer time interval for your UISlider
public var bufferAvail: NSTimeInterval {
// Check if there is a player instance
if ((player.currentItem) != nil) {
// Get current AVPlayerItem
var item: AVPlayerItem = player.currentItem
if (item.status == AVPlayerItemStatus.ReadyToPlay) {
var timeRangeArray: NSArray = item.loadedTimeRanges
var aTimeRange: CMTimeRange = timeRangeArray.objectAtIndex(0).CMTimeRangeValue
var startTime = CMTimeGetSeconds(aTimeRange.start)
var loadedDuration = CMTimeGetSeconds(aTimeRange.duration)
return (NSTimeInterval)(startTime + loadedDuration);
}
else {
return(CMTimeGetSeconds(kCMTimeInvalid))
}
}
else {
return(CMTimeGetSeconds(kCMTimeInvalid))
}
}
Selected answer may cause you problems if returned array is empty. Here's a fixed function:
- (NSTimeInterval) availableDuration
{
NSArray *loadedTimeRanges = [[_player currentItem] loadedTimeRanges];
if ([loadedTimeRanges count])
{
CMTimeRange timeRange = [[loadedTimeRanges objectAtIndex:0] CMTimeRangeValue];
Float64 startSeconds = CMTimeGetSeconds(timeRange.start);
Float64 durationSeconds = CMTimeGetSeconds(timeRange.duration);
NSTimeInterval result = startSeconds + durationSeconds;
return result;
}
return 0;
}
The code from Suresh Kansujiya in Objective C
NSTimeInterval bufferAvail;
if (player.currentItem != nil) {
AVPlayerItem *item = player.currentItem;
if (item.status == AVPlayerStatusReadyToPlay) {
NSArray *timeRangeArray = item.loadedTimeRanges;
CMTimeRange aTimeRange = [[timeRangeArray objectAtIndex:0] CMTimeRangeValue];
Float64 startTime = CMTimeGetSeconds(aTimeRange.start);
Float64 loadedDuration = CMTimeGetSeconds(aTimeRange.duration);
bufferAvail = startTime + loadedDuration;
NSLog(#"%# - %f", [self class], bufferAvail);
} else {
NSLog(#"%# - %f", [self class], CMTimeGetSeconds(kCMTimeInvalid)); }
}
else {
NSLog(#"%# - %f", [self class], CMTimeGetSeconds(kCMTimeInvalid));
}