How to animate incrementing number in UILabel - iphone

I have a label showing a number and I want to change it to a higher number, however I'd like to add a bit of flare to it.
I'd like to have the number increment up to the higher number with an ease inout curve so it speeds up then slows down.
This answer shows how to make it increment (the 2nd answer, not the accepted answer) but I'd rather animate it so I could also make it increase in size slightly then shrink again as well as the ease inout curve.
how to do a running score animation in iphone sdk
Any ideas how best to achieve this?
Thanks
The start/end numbers will be user inputted and I want it to increment up the the end number in the same amount of time. So if I have start 10 end 100 or start 10 end 1000 I want it to count up to the end number in say 5 seconds.

I actually made such a class just for this called UICountingLabel:
http://github.com/dataxpress/UICountingLabel
It allows you to specify whether you want the counting mode to be linear, ease in, ease out, or ease in/out. Ease in/out starts counting slowly, speeds up, and then finishes slowly - all in whatever amount of time you specify.
It doesn't currently support setting the actual font size of the label based on the current value, though I may add support for that if it's a feature that's in-demand. Most of the labels in my layouts don't have a lot of room to grow or shrink, so I'm not sure how you want to use it. However, it behaves totally like a normal label, so you can change the font size on your own as well.

Here #malex's answer in swift 3.
func incrementLabel(to endValue: Int) {
let duration: Double = 2.0 //seconds
DispatchQueue.global().async {
for i in 0 ..< (endValue + 1) {
let sleepTime = UInt32(duration/Double(endValue) * 1000000.0)
usleep(sleepTime)
DispatchQueue.main.async {
self.myLabel.text = "\(i)"
}
}
}
}
However, I strongly recommend simply downloading this class from GitHub and dragging it into your project, I've resorted to using it because the timing in my code doesn't seem to adjust properly for lower/higher count numbers. This class works great and looks very good. See this medium article for reference.

You can use GCD to shift delays to background threads.
Here is the example of value animation (from 1 to 100 in 10 seconds)
float animationPeriod = 10;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
for (int i = 1; i < 101; i ++) {
usleep(animationPeriod/100 * 1000000); // sleep in microseconds
dispatch_async(dispatch_get_main_queue(), ^{
yourLabel.text = [NSString stringWithFormat:#"%d", i];
});
}
});

You could use a flag to see if it has to go up or down.
Instead of a for loop, use a while loop.
In this way, you are creating a loop that keeps going, so you have to find a way to stop it also, f.e. by a button press.

There you have it without blocking sleeps!
stick this to a UILabel and it counts up and down from the value currently displaying, cleaning non numbers first.
Adjust from Double to Int or Float if needed.
yourlabel.countAnimation(upto: 100.0)
another simple alternative
extension UILabel {
func countAnimation(upto: Double) {
let from: Double = text?.replace(string: ",", replacement: ".").components(separatedBy: CharacterSet.init(charactersIn: "-0123456789.").inverted).first.flatMap { Double($0) } ?? 0.0
let steps: Int = 20
let duration = 0.350
let rate = duration / Double(steps)
let diff = upto - from
for i in 0...steps {
DispatchQueue.main.asyncAfter(deadline: .now() + rate * Double(i)) {
self.text = "\(from + diff * (Double(i) / Double(steps)))"
}
}
}
}

Swift 4 Code :
let animationPeriod: Float = 1
DispatchQueue.global(qos: .default).async(execute: {
for i in 1..<Int(endValue) {
usleep(useconds_t(animationPeriod / 10 * 10000)) // sleep in microseconds
DispatchQueue.main.async(execute: {
self.lbl.text = "\(i+1)"
})
}
})

If you want a fast counting animation, you can use a recursive func like so:
func updateGems(diff: Int) {
animateIncrement(diff: diff, index: 0, start: 50)
}
func animateIncrement(diff: Int, index: Int, start: Int) {
if index == diff {return}
gemsLabel.text = "\(start + index)"
DispatchQueue.main.asyncAfter(deadline: .now() + 0.002) {
self.animateIncrement(diff: diff, index: index + 1, start: start)
}
}

For the ones looking for a linear slow down counter (Swift 5) :
func animateCountLabel(to userCount: Int) {
countLabel.text = " "
let countStart: Int = userCount - 10
let countEnd: Int = userCount
DispatchQueue.global(qos: .default).async { [weak self] in
for i in (countStart...countEnd) {
let delayTime = UInt64(pow(2, (Float(i) - Float(countStart))))
usleep(useconds_t( delayTime * 1300 )) // sleep in microseconds
DispatchQueue.main.async { [weak self] in
self?.countLabel.text = "\(i)"
}
}
}
}
You can change the start end points and slow down speed by changing static values to your needs ;)

My variation of #Sergio's answer:
extension UILabel {
func countAnimation(upto: Double) {
let fromString = text?.replacingOccurrences(of: "Score: ", with: "")
let from: Double = fromString?.replacingOccurrences(of: ",", with: ".")
.components(separatedBy: CharacterSet.init(charactersIn: "-0123456789.").inverted)
.first.flatMap { Double($0) } ?? 0.0
let duration = 0.4
let diff = upto - from
let rate = duration / diff
for item in 0...Int(diff) {
DispatchQueue.main.asyncAfter(deadline: .now() + rate * Double(item)) {
self.text = "Score: \(Int(from + diff * (Double(item) / diff)))"
}
}
}
}
So amount of steps is based on the difference between starting and ending values

This is how i did it :
- (void)setupAndStartCounter:(CGFloat)duration {
NSUInteger step = 3;//use your own logic here to define the step.
NSUInteger remainder = numberYouWantToCount%step;//for me it was 30
if (step < remainder) {
remainder = remainder%step;
}
self.aTimer = [self getTimer:[self getInvocation:#selector(animateLabel:increment:) arguments:[NSMutableArray arrayWithObjects:[NSNumber numberWithInteger:remainder], [NSNumber numberWithInteger:step], nil]] timeInterval:(duration * (step/(float) numberYouWantToCountTo)) willRepeat:YES];
[self addTimerToRunLoop:self.aTimer];
}
- (void)animateLabel:(NSNumber*)remainder increment:(NSNumber*)increment {
NSInteger finish = finalValue;
if ([self.aLabel.text integerValue] <= (finish) && ([self.aLabel.text integerValue] + [increment integerValue]<= (finish))) {
self.aLabel.text = [NSString stringWithFormat:#"%lu",(unsigned long)([self.aLabel.text integerValue] + [increment integerValue])];
}else{
self.aLabel.text = [NSString stringWithFormat:#"%lu",(unsigned long)([self.aLabel.text integerValue] + [remainder integerValue])];
[self.aTimer invalidate];
self.aTimer = nil;
}
}
#pragma mark -
#pragma mark Timer related Functions
- (NSTimer*)getTimer:(NSInvocation *)invocation timeInterval:(NSTimeInterval)timeInterval willRepeat:(BOOL)willRepeat
{
return [NSTimer timerWithTimeInterval:timeInterval invocation:invocation repeats:willRepeat];
}
- (NSInvocation*)getInvocation:(SEL)methodName arguments:(NSMutableArray*)arguments
{
NSMethodSignature *sig = [self methodSignatureForSelector:methodName];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:self];
[invocation setSelector:methodName];
if (arguments != nil)
{
id arg1 = [arguments objectAtIndex:0];
id arg2 = [arguments objectAtIndex:1];
[invocation setArgument:&arg1 atIndex:2];
[invocation setArgument:&arg2 atIndex:3];
}
return invocation;
}
- (void)addTimerToRunLoop:(NSTimer*)timer
{
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

You can also check https://github.com/leszek-s/LSCategories
It allows incrementing/decrementing number in UILabel with a single line of code like this:
[self.label lsAnimateCounterWithStartValue:10 endValue:100 duration:5 completionBlock:nil];

Details
Xcode 9.2, swift 4
Solution
class LoadingProcess {
let minValue: Int
let maxValue: Int
var currentValue: Int
private let progressQueue = DispatchQueue(label: "ProgressView")
private let semaphore = DispatchSemaphore(value: 1)
init (minValue: Int, maxValue: Int) {
self.minValue = minValue
self.currentValue = minValue
self.maxValue = maxValue
}
private func delay(stepDelayUsec: useconds_t, completion: #escaping ()->()) {
usleep(stepDelayUsec)
DispatchQueue.main.async {
completion()
}
}
func simulateLoading(toValue: Int, step: Int = 1, stepDelayUsec: useconds_t? = 10_000,
valueChanged: #escaping (_ currentValue: Int)->(),
completion: ((_ currentValue: Int)->())? = nil) {
semaphore.wait()
progressQueue.sync {
if currentValue <= toValue && currentValue <= maxValue {
usleep(stepDelayUsec!)
DispatchQueue.main.async {
valueChanged(self.currentValue)
self.currentValue += step
self.semaphore.signal()
self.simulateLoading(toValue: toValue, step: step, stepDelayUsec: stepDelayUsec, valueChanged: valueChanged, completion: completion)
}
} else {
self.semaphore.signal()
completion?(currentValue)
}
}
}
func finish(step: Int = 1, stepDelayUsec: useconds_t? = 10_000,
valueChanged: #escaping (_ currentValue: Int)->(),
completion: ((_ currentValue: Int)->())? = nil) {
simulateLoading(toValue: maxValue, step: step, stepDelayUsec: stepDelayUsec, valueChanged: valueChanged, completion: completion)
}
}
Usage
let loadingProcess = LoadingProcess(minValue: 0, maxValue: 100)
loadingProcess.simulateLoading(toValue: 80, valueChanged: { currentValue in
// Update views
})
DispatchQueue.global(qos: .background).async {
print("Start loading data")
sleep(5)
print("Data loaded")
loadingProcess.finish(valueChanged: { currentValue in
// Update views
}) { _ in
print("End")
}
}
Full sample
Do not forget to add the solution code here
import UIKit
class ViewController: UIViewController {
weak var counterLabel: UILabel!
weak var progressView: UIProgressView!
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .fillProportionally
stackView.spacing = 8
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 80).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -80).isActive = true
let label = UILabel()
label.textAlignment = .center
label.text = "0"
label.font = UIFont.systemFont(ofSize: 46)
stackView.addArrangedSubview(label)
counterLabel = label
let progressView = UIProgressView()
progressView.trackTintColor = .lightGray
progressView.progressTintColor = .blue
progressView.layer.cornerRadius = 4
progressView.clipsToBounds = true
progressView.heightAnchor.constraint(equalToConstant: 8).isActive = true
stackView.addArrangedSubview(progressView)
self.progressView = progressView
let button = UIButton()
button.setTitle("Start", for: .normal)
button.addTarget(self, action: #selector(startButtonTapped), for: .touchUpInside)
button.setTitleColor(.blue, for: .normal)
button.heightAnchor.constraint(equalToConstant: 30).isActive = true
stackView.addArrangedSubview(button)
}
#objc func startButtonTapped() {
sample()
}
private func setProcess(currentValue: Int) {
let value = 0.01 * Float(currentValue)
self.counterLabel?.text = "\(currentValue)"
self.progressView?.setProgress(value, animated: true)
print("\(currentValue)")
}
func sample() {
let loadingProcess = LoadingProcess(minValue: 0, maxValue: 100)
loadingProcess.simulateLoading(toValue: 80, valueChanged: { currentValue in
self.setProcess(currentValue: currentValue)
})
DispatchQueue.global(qos: .background).async {
print("Start loading data")
sleep(5)
print("Data loaded")
loadingProcess.finish(valueChanged: { currentValue in
self.setProcess(currentValue: currentValue)
}) { _ in
print("end")
}
}
}
}
Results

Related

How to prevent periodicTimeObserver updating the progressBar while user drags the slider manually?

I have a periodicTimeObserver and it updates the elapsed and remaining timeLabels in the way I want, but the slider is jumping. How to prevent periodicTimeObserver updating the UISlider while user drags the slider manually?
This is my UISlider
private lazy var progressBar: UISlider = {
let v = UISlider()
v.translatesAutoresizingMaskIntoConstraints = false
//v.minimumTrackTintColor = UIColor(named: "PlayerColors")
v.isContinuous = false
return v
}()
Periodic time observer which updates the UISlider and the elapsed and remaining time labels.
player = AVPlayer(playerItem: playerItem)
player!.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main) { (CMTime) -> Void in
if self.player!.currentItem?.status == .readyToPlay {
let currentTime : Float64 = CMTimeGetSeconds(self.player!.currentTime());
let totalTime : Float64 = CMTimeGetSeconds(self.player!.currentItem!.duration);
self.progressBar.value = Float(currentTime)
self.progressBar.minimumValue = 0
self.progressBar.maximumValue = Float(totalTime)
self.elapsedTimeLabel.text = self.stringFromTimeInterval(interval: currentTime)
self.remainingTimeLabel.text = self.stringFromTimeIntervalRemaining(interval: totalTime - currentTime)
The function that should seek to a point of the audio and update the time labels.
#objc func progressScrubbed(_ :UISlider) {
let seconds : Int64 = Int64(self.progressBar.value)
let targetTime:CMTime = CMTimeMake(value: seconds, timescale: 1)
player!.seek(to: targetTime)
if player!.rate == 0
{
play()
}
}
You need to know if user is interacting with a slider in order to ignore PeriodicTimeObserver. Moreover you need to reset PeriodicTimeObserver on each seek. So let's create a custom UISlider and override a one method:
class MySlider: UISlider {
var onTouchesBegan: (() -> ())?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
onTouchesBegan?()
}
}
Now you can create a parameter which will track if slider is touched or not and set it in closures of MySlider:
private var isTouchingSlider: Bool = false
private lazy var progressBar: MySlider = {
let v = MySlider()
v.translatesAutoresizingMaskIntoConstraints = false
v.isContinuous = false
v.onTouchesBegan = { [weak self] in
self?.isTouchingSlider = true
}
return v
}()
And your periodic observer methods would look like this:
var periodicObserverToken: Any?
func addPeriodicTimeObserver() {
let interval = CMTime(
seconds: 1,
preferredTimescale: CMTimeScale(NSEC_PER_SEC)
)
periodicObserverToken = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] _ in
guard let `self` = self, let player = self.player, player.currentItem?.status == .readyToPlay, let currentItem = self.player.currentItem, !self.isTouchingSlider else { return }
let currentTime : Float64 = CMTimeGetSeconds(player.currentTime());
let totalTime : Float64 = CMTimeGetSeconds(currentItem.duration);
self.progressBar.value = Float(currentTime)
self.progressBar.minimumValue = 0
self.progressBar.maximumValue = Float(totalTime)
}
}
private func removePeriodicTimeObserver() {
guard let periodicObserverToken = periodicObserverToken else { return }
player?.removeTimeObserver(periodicObserverToken)
self.playerPeriodicTimeObserver = nil
}
You need to make all the necessary updates when slider is updated:
#objc func progressScrubbed(_ :UISlider) {
let seconds : Int64 = Int64(self.progressBar.value)
let targetTime:CMTime = CMTimeMake(value: seconds, timescale: 1)
removePeriodicTimeObserver()
isTouchingSlider = false
player!.seek(to: targetTime)
addPeriodicTimeObserver()
if player!.rate == 0 {
play()
}
}

Animate Single Character in String Swift

How can we animate a single character within a string? ie: I have the string: "Hey 👋". I would like to animate the emoji by setting it to rotate back and forth so it looks like it's waving.
To detect the emoji, I'm using:
extension String {
// Not needed anymore in swift 4.2 and later, using `.count` will give you the correct result
var glyphCount: Int {
let richText = NSAttributedString(string: self)
let line = CTLineCreateWithAttributedString(richText)
return CTLineGetGlyphCount(line)
}
var isSingleEmoji: Bool {
return glyphCount == 1 && containsEmoji
}
var containsEmoji: Bool {
return unicodeScalars.contains { $0.isEmoji }
}
I'm using the emoji code here: Find out if Character in String is emoji?.
I'm unsure how to set up the animation
I've very slightly modified this excellent answer by Rob in this post.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
animateEmojiBackandForth("👋")
}
func animateEmojiBackandForth (_ searchText: String) {
let beginning = textView.beginningOfDocument
guard let string = textView.text,
let range = string.range(of: searchText),
let start = textView.position(from: beginning, offset: string.distance(from: string.startIndex, to: range.lowerBound)),
let end = textView.position(from: beginning, offset: string.distance(from: string.startIndex, to: range.upperBound)),
let textRange = textView.textRange(from: start, to: end)
else { return }
textView.selectionRects(for: textRange)
.forEach { selectionRect in
guard let snapshotView = textView.resizableSnapshotView(from: selectionRect.rect, afterScreenUpdates: false, withCapInsets: .zero) else { return }
snapshotView.frame = view.convert(selectionRect.rect, from: textView)
view.addSubview(snapshotView)
UIView.animate(withDuration: 1, delay: 0, options: .autoreverse, animations: {
snapshotView.transform = CGAffineTransform(rotationAngle: CGFloat( CGFloat.pi / 4))
}, completion: { _ in
snapshotView.removeFromSuperview()
})
}
}
You can modify the duration of the animation and the angle of the "wave" if you wish. I hope this was helpful!

Swift observevalueforkeypath + loadedTimeRanges only called limited times

I am trying to create a video player with AVPlayer.Videos load from youtube. I want when video current time changes, my uiSlider and my time label can update. I am using "observeValueForKeyPath" method to catch "loadedTimeRanges" status from player item. Here is my code:
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
print("keyPath: \(keyPath)")
let playerItem:AVPlayerItem = object as! AVPlayerItem
if keyPath == "status" {
if playerItem.status == AVPlayerItemStatus.ReadyToPlay{
print("AVPlayerItemStatusReadyToPlay")
} else if playerItem.status == AVPlayerItemStatus.Failed {
print("AVPlayerItemStatusFailed")
}
} else if keyPath == "loadedTimeRanges" {
let currentTime = playerItem.currentTime().seconds
let totalDuration = playerItem.duration.seconds
print("value: \(Float(currentTime/totalDuration))")
self.monitoringPlayback(player.currentItem!)
self.slider.setValue(Float(currentTime/totalDuration), animated: false)
}
}
func monitoringPlayback(playerItem:AVPlayerItem) {
let currentSecond:Double = playerItem.currentTime().seconds
self.updateVideoSlider(currentSecond)
let timeString = self.updateTime(playerItem.duration.seconds - currentSecond)
print("time string: \(timeString)")
self.lblTime.text = timeString
}
However, the "observeValueForKeyPath" method is only called 20-22 times everytime. Please, check the log screenshot: https://gyazo.com/5ca57ba532689d83aea855ae41387f53
It's the first time I use this method, so maybe I didn't understand how it work. If anyone who know it, please tell me why. Thanks for reading my question.
it, easy
var rightCurrentTimer: UILabel = {
let lbl = UILabel()
lbl.text = "00:00"
lbl.textColor = .white
lbl.translatesAutoresizingMaskIntoConstraints = false
lbl.font = UIFont.systemFont(ofSize: 11)
return lbl
}()
let leftWhatTimePlayed:UILabel = {
let lbl = UILabel()
lbl.translatesAutoresizingMaskIntoConstraints = false
lbl.textColor = .white
lbl.text = "00:00"
lbl.font = UIFont.systemFont(ofSize: 11)
return lbl
}()
let interval = CMTime(value: 1, timescale: 1)
player?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { (timeProgress) in
let secondsPlay = CMTimeGetSeconds(timeProgress)
let secondString = String(format:"%02d" , Int(secondsPlay) % 60 )
let minutesString = String(format:"%02d", Int(secondsPlay) / 60)
self.leftWhatTimePlayed.text = "\(minutesString):\(secondString)"
if let duration = self.player?.currentItem?.duration {
let totalSecond = CMTimeGetSeconds(duration)
let whatSecondIsPlay = totalSecond - secondsPlay
let secondsIsPlay = String(format:"%02d" , Int(whatSecondIsPlay) % 60)
let minutesIsPlay = String(format: "%02d", Int(whatSecondIsPlay) / 60)
self.rightCurrentTimer.text = "\(minutesIsPlay):\(secondsIsPlay)"
self.videoLengthTimeSlider.value = Float(secondsPlay/totalSecond)
}
})

Using NSCore and NSKeyedArchiver with SWIFT

I been trying to get persistent data on my app to have a history of user entries. After I store my data in to array I want to archive it, and after I unarchive it i get weird value instead of what i want to see.
Here is my class for where i store my data
import Foundation
class MyHistory: NSObject, NSCoding {
var kicksNumber: Int
var durationNumber: Int
init(kicksNumber: Int,durationNumber: Int) {
self.kicksNumber = kicksNumber
self.durationNumber = durationNumber
}
required init(coder decoder: NSCoder) {
kicksNumber = decoder.decodeObjectForKey("kicksNumber") as! Int
durationNumber = decoder.decodeObjectForKey("durationNumber") as! Int
}
func encodeWithCoder(coder: NSCoder) {
coder.encodeObject(self.kicksNumber, forKey: "kicksNumber")
coder.encodeObject(self.durationNumber, forKey: "durationNumber")
}
}
Then here is my class where things happen, And where I am testing out the save and load process.
class Kicks: UIViewController {
var myHistoryArray: [MyHistory] = []
var currentMyHistory: MyHistory!
var newHistory = [MyHistory]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.view.backgroundColor = UIColor(patternImage: UIImage(named: "background13.png")!)
let defaults = NSUserDefaults.standardUserDefaults()
if let savedPeople = defaults.objectForKey("MyHistory") as? NSData {
newHistory = NSKeyedUnarchiver.unarchiveObjectWithData(savedPeople) as! [MyHistory]
//print("this is archived ", newHistory[0])
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
var count = 0 as Int
var countKicks = 0 as Int
var kickReached = false as Bool
var pressedOnce = true as Bool
var timer = NSTimer()
var test: MyHistory!
#IBOutlet var timerLabel: UITextField!
#IBOutlet var kicksLabel: UITextField!
#IBAction func kickButton() {
//currentMyHistory.kicksNumber = 5
if pressedOnce {
pressedOnce = false
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("counter"), userInfo: nil, repeats: true)
} else if kickReached {
// let date = NSDate()
// let calendar = NSCalendar.currentCalendar()
// let timer_total = calendar.components([ .Hour, .Minute, .Second], fromDate: date)
} else if !pressedOnce {
countKicks++
kicksLabel.text = "\(countKicks)"
if countKicks == 10 {
kickReached = true
timer.invalidate()
congratsAlert()
currentMyHistory = MyHistory(kicksNumber: 5, durationNumber: 10)
print("this is currentMyHistory", currentMyHistory.kicksNumber )
myHistoryArray.append(currentMyHistory)
test = myHistoryArray[0]
print("this is myHistoryArray0", test.kicksNumber)
//save data
let savedData = NSKeyedArchiver.archivedDataWithRootObject(myHistoryArray)
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(savedData, forKey: "MyHistory")
//load data
//let defaults = NSUserDefaults.standardUserDefaults()
// let person = people[indexPath.item]
//let historyUnarchived = NSKeyedUnarchiver.unarchiveObjectWithFile("/path/to/archive") as? [MyHistory]
// let data1 = NSUserDefaults.standardUserDefaults().objectForKey("myHistoryArray")
print("this is unrachived",newHistory[0])
clear()
}
}
}
// save countKicks, count, and stamp i
func congratsAlert() {
let alert = UIAlertController(title: "Congratulation", message: "Yay!!! Angelina kicked 10 times in less than 2 hours.",preferredStyle: .Alert)
let okAction = UIAlertAction(title: "Ok",style: .Default,handler:{(action:UIAlertAction) -> Void in})
alert.addAction(okAction)
presentViewController(alert,animated: true,completion: nil)
}
func clear() {
count = 0
countKicks = 0
kickReached = false
pressedOnce = true
timerLabel.text = "00:00:0\(count)"
kicksLabel.text = "\(countKicks)"
}
func counter() {
++count
let (hour,minutes,seconds) = secondsToHoursMinutesSeconds(count)
if seconds < 10 && minutes < 10 {
timerLabel.text = "0\(hour):0\(minutes):0\(seconds)"
} else if seconds > 9 && minutes < 10 {
timerLabel.text = "0\(hour):0\(minutes):\(seconds)"
} else if seconds > 9 && minutes > 9 {
timerLabel.text = "0\(hour):\(minutes):\(seconds)"
} else if seconds < 10 && minutes > 9 {
timerLabel.text = "0\(hour):\(minutes):0\(seconds)"
}
}
func secondsToHoursMinutesSeconds (seconds : Int) -> (Int, Int, Int) {
return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60)
}
/*
func savePlaces() {
let placesArray = [myHistory(kicksNumber: 420, durationNumber: 89)]
let placesData = NSKeyedArchiver.archivedDataWithRootObject(placesArray)
NSUserDefaults.standardUserDefaults().setObject(placesData, forKey: "kicks")
}
func loadPlaces() {
let placesData = NSUserDefaults.standardUserDefaults().objectForKey("kicks") as? NSData
if let placesData = placesData {
let placesArray = NSKeyedUnarchiver.unarchiveObjectWithData(placesData) as? [myHistory]
if let placesArray = placesArray {
// do something…
}
}
}*/
}
My output is like this:
this is currentMyHistory 5
this is myHistoryArray0 5
this is unrachived
Message from debugger: Terminated due to signal 15
why is unarchived is weird value?
In your MyHistory class you are using ints, so in your encodeWithCoder function you should be using
coder.encodeInteger(self.kicksNumber, forKey: "kicksNumber")
coder.encodeInteger(self.durationNumber, forKey: "durationNumber")
Likewise for your decoder you should be using decodeIntForKey, not decodeObjectForKey.
kicksNumber = decoder.decodeIntegerForKey("kicksNumber")
durationNumber = decoder.decodeIntegerForKey("durationNumber")

UISlider to control AVAudioPlayer

I'm trying to implement a little function in my app. I am currently playing sounds as AVAudioPlayers and that works fine. What I would like to add is to control the sound's position (currentTime) with an UISlider: is there a simple way to do it ?
I looked at an Apple project but it was quite messy....have you got samples or suggestions ?
Thanks to everyone in advance
Shouldn't be a problem - just set the slider to continuous and set the max value to your player's duration after loading your sound file.
Edit
I just did this and it works for me...
- (IBAction)slide {
player.currentTime = slider.value;
}
- (void)updateTime:(NSTimer *)timer {
slider.value = player.currentTime;
}
- (IBAction)play:(id)sender {
NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:#"sound.caf" ofType:nil]];
NSError *error;
player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
if (!player) NSLog(#"Error: %#", error);
[player prepareToPlay];
slider.maximumValue = [player duration];
slider.value = 0.0;
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(updateTime:) userInfo:nil repeats:YES];
[player play];
}
The slider is configured in IB, as is a button to start playing.
Swift 3.0 Update:
var player: AVAudioPlayer!
var sliderr: UISlider!
#IBAction func play(_ sender: Any) {
var url = URL(fileURLWithPath: Bundle.main.path(forResource: "sound.caf", ofType: nil)!)
var error: Error?
do {
player = try AVAudioPlayer(contentsOf: url)
}
catch let error {
}
if player == nil {
print("Error: \(error)")
}
player.prepareToPlay()
sliderr.maximumValue = Float(player.duration)
sliderr.value = 0.0
Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.updateTime), userInfo: nil, repeats: true)
player.play()
}
func updateTime(_ timer: Timer) {
sliderr.value = Float(player.currentTime)
}
#IBAction func slide(_ slider: UISlider) {
player.currentTime = TimeInterval(slider.value)
}
To extend on paull's answer, you'd set the slider to be continuous with a maximum value of your audio player's duration, then add some object of yours (probably the view controller) as a target for the slider's UIControlEventValueChanged event; when you receive the action message, you'd then set the AVAudioPlayer's currentTime property to the slider's value.
You might also want to use an NSTimer to update the slider's value as the audio player plays; +scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: is the easiest way to do that.
I needed to adapt the above answer a bit to get it to work. The issue is that using
slider.maximumValue = [player duration];
slider.value = player.currentTime;
player.currentTime = slider.value;
Do not work because the slider expects a float and the player currentTime and dration return CMTime. To make these work, I adapted them to read:
slider.maximumValue = CMTimeGetSeconds([player duration]);
slider.value = CMTimeGetSeconds(player.currentTime);
player.currentTime = CMTimeMakeWithSeconds((int)slider.value,1);
If you don't need any data in between drag, then you should simply set:
mySlider.isContinuous = false
Otherwise, try below code to controller each phase of touch.
// audio slider bar
private lazy var slider: UISlider = {
let slider = UISlider()
slider.translatesAutoresizingMaskIntoConstraints = false
slider.minimumTrackTintColor = .red
slider.maximumTrackTintColor = .white
slider.setThumbImage(UIImage(named: "sliderThumb"), for: .normal)
slider.addTarget(self, action: #selector(onSliderValChanged(slider:event:)), for: .valueChanged)
// slider.isContinuous = false
return slider
}()
#objc func onSliderValChanged(slider: UISlider, event: UIEvent) {
guard let player = AudioPlayer.shared.player else { return }
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .began:
// handle drag began
// I would stop the timer when drag begin
timer.invalidate()
case .moved:
// handle drag moved
// Update label's text for current playing time
case .ended:
// update the player's currTime and re-create the timer when drag is done.
player.currentTime = TimeInterval(slider.value)
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTime(_:)), userInfo: nil, repeats: true)
default:
break
}
}
}
Problems that I've faced during playing an audio file and show start/end time and controlling the song with the UISlider.
Not playing audio directly without downloading it in temp folder.
UISlider got crashed on main thread in lower iOS version i.e 12.4/13.1
Smooth Scrolling of UISlider.
Calculating and updating the start/end time of the song.
This answer needs some editing, but it will work without any doubt.
//UISlider init
lazy var slider: UISlider = {
let progress = UISlider()
progress.minimumValue = 0.0
progress.maximumValue = 100.0
progress.tintColor = UIColor.init(named: "ApplicationColor")
return progress
}()
var audioPlayer : AVAudioPlayer?
//First I've downloaded the audio and then playing it.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(trackAudio), userInfo: nil, repeats: true)
if let audioURLString = audioURL{
let urlstring = URL(string: audioURLString)!
downloadFromURL(url: urlstring) { (localURL, response, error) in
if let localURL = localURL{
self.playAudioFile(url: localURL)
}
}
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
stopTimer()
}
// Stop TimeInterval After View disappear
func stopTimer() {
if timer != nil {
timer?.invalidate()
audioPlayer?.stop()
audioPlayer = nil
timer = nil
}
}
#objc func sliderSelected(_ sender : UISlider){
if audioPlayer != nil{
if !isPlaying{
self.audioPlayer?.play()
playButton.setImage(UIImage.init(named: "AudioPause"), for: .normal)
isPlaying = true
}else{
self.audioPlayer?.currentTime = TimeInterval(Float(sender.value) * Float(self.audioPlayer!.duration) / 100.0)
if (sender.value / 100.0 == 1.0){
//Do something if audio ends while dragging the UISlider.
}
}
}
}
func downloadFromURL(url:URL,completion: #escaping((_ downladedURL: URL?,_ response :URLResponse?,_ error: Error?) -> Void)){
var downloadTask:URLSessionDownloadTask
downloadTask = URLSession.shared.downloadTask(with: url) {(URL, response, error) in
if let url = URL{
completion(url,nil,nil)
}else if let response = response{
completion(nil,response,nil)
}
if let error = error{
completion(nil,nil,error)
}
}
downloadTask.resume()
}
func playAudioFile(url:URL){
do{
self.audioPlayer = try AVAudioPlayer(contentsOf: url)
self.audioPlayer?.prepareToPlay()
self.audioPlayer?.delegate = self
self.audioPlayer?.play()
let audioDuration = audioPlayer?.duration
let audioDurationSeconds = audioDuration
minutes = Int(audioDurationSeconds!/60);
seconds = Int(audioDurationSeconds!.truncatingRemainder(dividingBy: 60))
} catch{
print("AVAudioPlayer init failed")
}
}
#objc func trackAudio() {
if audioPlayer != nil{
DispatchQueue.main.async {
print("HI")
let normalizedTime = Float(self.audioPlayer!.currentTime * 100.0 / self.audioPlayer!.duration)
self.slider.setValue(normalizedTime, animated: true)
let currentTime = self.audioPlayer?.currentTime
self.currentMinutes = Int(currentTime!/60);
self.currentSeconds = Int(currentTime!.truncatingRemainder(dividingBy: 60))
self.startTimeLabel.text = String(format: "%02i:%02i", self.currentMinutes, self.currentSeconds)
self.endTimeLabel.text = String(format: "%02i:%02i", self.minutes, self.seconds)
}
}
}
If anyone was looking for a simple TouchDown and TouchUp on UI slider then this turns out to be as simple as :
slider.addTarget(self, action: #selector(changeVlaue(_:)), for: .valueChanged)
slider.addTarget(self, action: #selector(sliderTapped), for: .touchDown)
slider.addTarget(self, action: #selector(sliderUntouched), for: .touchUpInside)
Here's the entire setup for an AVAudioPlayer. Some of the code in handleScrubbing() and fingerLiftedFromSlider() is duplicated but whatever...
This will let you show what's on the currentTimeLabel (usually on the left) and the totalDurationLabel (usually on the right) with the scrubber/slider in the the middle of them. When you slide the slider the currentTime will update to show wherever the slider is.
There is something to be aware about. If the the player was playing before you touch the slider, while you slide the slider, the player is still playing. In .began you need to check if the player was playing and if so pause it and set a variable like wasAudioPlayerPlayingBeforeSliderWasTouched to true so that when your finger is lifted it will continue playing from wherever you lift your finger. If you don't pause the player then the slider isn't going to slide smoothly.
When you lift your finger there is a check in onFingerLiftedStartAudioPlayerIfItWasPlayingBeforeTouchesBegan() to see if the slider is at its endTime. If it is instead of playing it'll run the code in audioEndTimeStopEverything().
In the startAudioPlayer method, I used an AVURLAsset get a set the actual url's duration. I got it from this answer which has a great explanation.
I used this code with a local url, not sure how this will work with a remote url.
import UIKit
import AVFoundation
class MyAudioController: UIViewController {
lazy var currentTimeLabel ... { ... }()
lazy var totalDurationLabel ... { ... }()
lazy vay pausePlayButton ... { ... }()
lazy var fastForwardButton ... { ... }()
lazy var rewindButton ... { ... }()
lazy var slider: UISlider = {
let s = UISlider()
s.translatesAutoresizingMaskIntoConstraints = false
s.isContinuous = true
s.minimumTrackTintColor = UIColor.red
s.maximumTrackTintColor = UIColor.blue
s.setThumbImage(UIImage(named: "circleIcon"), for: .normal)
s.addTarget(self, action: #selector(sliderValChanged(slider:event:)), for: .valueChanged)
return s
}()
weak var timer: Timer? // *** MAKE SURE THIS IS WEAK ***
var audioPlayer: AVAudioPlayer?
var wasAudioPlayerPlayingBeforeSliderWasTouched = false
override func viewDidLoad() {
super.viewDidLoad()
guard let myAudioUrl = URL(string: "...") else { return }
setAudio(with: myAudioUrl)
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print(error)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopAudioPlayer()
}
// 1. init your AVAudioPlayer here
func setAudioPlayer(with audioTrack: URL) {
do {
stopAudioPlayer() // if something was previously playing
audioPlayer = try AVAudioPlayer(contentsOf: audioTrack)
audioPlayer?.delegate = self
audioPlayer?.prepareToPlay()
audioPlayer?.volume = audioVolume
startAudioPlayer()
} catch let err as NSError {
print(err.localizedDescription)
}
}
// 2. Audio PLAYER - start / stop funcs
stopAudioPlayer() {
stopAudioTimer()
audioPlayer?.pause()
audioPlayer?.stop()
}
func startAudioPlayer() {
if let audioPlayer = audioPlayer, audioPlayer.isPlaying {
audioPlayer.pause()
}
audioPlayer?.currentTime = 0
audioPlayer?.play()
pausePlayButton.setImage(UIImage(named: "pauseIcon"), for: .normal)
startAudioTimer()
}
func startAudioTimer() {
stopAudioTimer()
slider.value = 0
currentTimeLabel.text = "00:00"
totalDurationLabel.text = "00:00"
guard let url = audioPlayer?.url else { return }
let assetOpts = [AVURLAssetPreferPreciseDurationAndTimingKey: true]
let asset = AVURLAsset(url: url, options: assetOpts)
let assetDuration: CMTime = asset.duration
let assetDurationInSecs: Float64 = CMTimeGetSeconds(assetDuration)
slider.maximumValue = Float(assetDurationInSecs)
totalDurationLabel.text = strFromTimeInterval(interval: TimeInterval(assetDurationInSecs))
runAudioTimer()
}
// 3. TIMER funcs
func runAudioTimer() {
if timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true, block: { [weak self](_) in
self?.audioTimerIsRunning()
})
}
}
func audioTimerIsRunning() {
guard let audioPlayer = audioPlayer else { return }
let currentTime = audioPlayer.currentTime
if Float(currentTime) >= Float(slider.maximumValue) {
stopAudioTimer()
}
currentTimeLabel.text = strFromTimeInterval(interval: currentTime)
slider.value = Float(currentTime)
}
func stopAudioTimer() {
if timer != nil {
timer?.invalidate()
timer = nil
}
}
// slider funcs
#objc func sliderValChanged(slider: UISlider, event: UIEvent) {
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .began:
checkIfAudioPlayerWasPlayingWhenSliderIsFirstTouched()
stopAudioTimer()
print("Finger Touched")
case .moved:
handleScrubbing()
print("Finger is Moving Scrubber")
case .ended:
print("Finger Lifted")
onFingerLiftedStartAudioPlayerIfItWasPlayingBeforeTouchesBegan()
fingerLiftedFromSlider()
default:
print("Something Else Happened In Slider")
}
}
}
func checkIfAudioPlayerWasPlayingWhenSliderIsFirstTouched() {
guard let audioPlayer = audioPlayer else { return }
if audioPlayer.isPlaying {
audioPlayer.pause()
wasAudioPlayerPlayingBeforeSliderWasTouched = true
}
}
func handleScrubbing() {
guard let audioPlayer = audioPlayer else { return }
let sliderValue = TimeInterval(slider.value)
currentTimeLabel.text = strFromTimeInterval(interval: sliderValue)
audioPlayer.currentTime = sliderValue
if audioPlayer.currentTime >= audioPlayer.duration {
audioEndTimeStopEverything()
}
}
func onFingerLiftedStartAudioPlayerIfItWasPlayingBeforeTouchesBegan() {
if wasAudioPlayerPlayingBeforeSliderWasTouched {
wasAudioPlayerPlayingBeforeSliderWasTouched = false
guard let audioPlayer = audioPlayer else { return }
if slider.value >= slider.maximumValue {
audioEndTimeStopEverything()
} else {
audioPlayer.play()
}
}
}
func fingerLiftedFromSlider() {
guard let audioPlayer = audioPlayer else { return }
if !audioPlayer.isPlaying { // this check is necessary because if you paused the audioPlayer, then started sliding, it should still be paused when you lift you finger up. It it's paused there is no need for the timer function to run.
let sliderValue = TimeInterval(slider.value)
currentTimeLabel.text = strFromTimeInterval(interval: sliderValue)
audioPlayer.currentTime = sliderValue
return
}
runAudioTimer()
}
func audioEndTimeStopEverything() {
stopAudioPlayer()
pausePlayButton.setImage(UIImage("named: playIcon"), for: .normal)
guard let audioPlayer = audioPlayer else { return }
// for some reason when the audioPlayer would reach its end time it kept resetting its currentTime property to zero. I don't know if that's meant to happen or a bug but the currentTime would be zero and the slider would be at the end. To rectify the issue I set them both to their end times
audioPlayer.currentTime = audioPlayer.duration
slider.value = slider.maximumValue
}
}
extension MyAudioController: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
audioEndTimeStopEverything()
}
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
if let error = error {
print(error.localizedDescription)
}
}
}
Here is the strFromTimeInterval(interval: ) function that I got from here. I only used it because I didn't want to bother with milliseconds. The code above was ran using audio files with minutes and seconds, not hours. If you have any problems with hours you can also swap this function out for this answer
extension MyAudioController {
func strFromTimeInterval(interval: TimeInterval) -> String {
let time = NSInteger(interval)
let seconds = time % 60
let minutes = (time / 60) % 60
let hours = (time / 3600)
var formatString = ""
if hours == 0 {
if (minutes < 10) {
formatString = "%2d:%0.2d"
} else {
formatString = "%0.2d:%0.2d"
}
return String(format: formatString,minutes,seconds)
} else {
formatString = "%2d:%0.2d:%0.2d"
return String(format: formatString,hours,minutes,seconds)
}
}
}