Get just the x value from iOS Accelerometer data - swift

I'm quite new to this and really enjoying swift, I have managed to work must stuff out but this has me stumped. I want to get just the x value (a double) from my accelerometer data.
The below pulls the data and prints out :x 0.476379 y -0.849365 z -0.041885 # 568028.476360
This is great, but I just want :0.476379 - any ideas?
class ViewController: UIViewController {
#objc func update() {
if let accelerometerData = motionManager.accelerometerData {
print(accelerometerData)
}
}
let motionManager = CMMotionManager()
var timer: Timer!
override func viewDidLoad()
{
super.viewDidLoad()
motionManager.startAccelerometerUpdates()
timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(ViewController.update), userInfo: nil, repeats: true)
}

SwiftUI
let accelTuple = (acceleration.x)
import Foundation
import CoreMotion
class Motion {
var motion = CMMotionManager()
#Published var timer = Timer()
private var motionManager = CMMotionManager()
func startAccelerometer() {
if motion.isAccelerometerAvailable {
self.motion.accelerometerUpdateInterval = 5
motion.startAccelerometerUpdates()
self.timer = Timer(fire: Date(), interval: (10),
repeats: true, block: { (timer) in
if let data = self.motion.accelerometerData {
let acceleration = data.acceleration
self.accelerometerData = ("Accelerometer data : \(data)")
print(self.accelerometerData)
let accelTuple = (acceleration.x, acceleration.y, acceleration.z)
valuesArray.append(accelTuple)
}
})
RunLoop.current.add(timer, forMode: .default)
}
}
func startGyroscope() {
if motion.isGyroAvailable {
self.motion.gyroUpdateInterval = 5
motion.startGyroUpdates()
self.timer = Timer(fire: Date(), interval: (10),
repeats: true, block: { (timer) in
if let data = self.motion.gyroData {
let x = data.rotationRate.x
let y = data.rotationRate.y
let z = data.rotationRate.z
self.gyroscopeData = ("Gyroscope data : \(data)")
print(self.gyroscopeData)
let gyroTuple = (x, y, z)
valuesArray.append(gyroTuple)
}
})
RunLoop.current.add(timer, forMode: .default)
}
}
func startMagnetometer() {
if motion.isMagnetometerAvailable {
self.motion.magnetometerUpdateInterval = 5
motion.startMagnetometerUpdates()
self.timer = Timer(fire: Date(), interval: (10),
repeats: true, block: { (timer) in
if let data = self.motion.magnetometerData {
let acceleration = data.magneticField
self.magnetometerData = ("Magnetometer data : \(data)")
print(self.magnetometerData)
let magneTuple = (acceleration.x, acceleration.y, acceleration.z)
valuesArray.append(magneTuple)
}
})
RunLoop.current.add(timer, forMode: .default)
}
}
}

Working code thanks to Nick:
class ViewController: UIViewController {
#objc func update() {
if let accelerometerData = motionManager.accelerometerData?.acceleration.x {
print(accelerometerData)
}
}

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()
}
}

i want an animation view when the game ends. and my animationView will appear first then next is congratulationview animated.?

this is my code thank you can someone know how to do that?
this scene is when the the game is over
func prepareTimeCount() {
self.timer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(self.timeCount),
userInfo: nil,
repeats: true)
}
func prepareWinView() {
let images:NSMutableArray! = NSMutableArray()
for i in 1...12
{
let image = UIImage(named:"animationView\(i).png")
images.add(image!)
}
self.congratulationView.animationImages = images as [AnyObject] as? [UIImage]
self.congratulationView.animationDuration = 3
self.congratulationView.startAnimating()
self.view.addSubview(self.congratulationView)
}
#objc func timeCount() {
var timeCount = Int(self.timeCountLabel!.text!) ?? 0
if(timeCount <= 0 ) {
self.timer.invalidate()
self.gameOver()
} else {
timeCount-=1
}
self.timeCountLabel.text = String(timeCount)
}
func gameOver() {
self.isGameOver = true
self.birdView.stopAnimating()
self.timer.invalidate()
}
and here is the codes when the game ends
func gameWin() {
let defaults = UserDefaults.standard
defaults.setValue("\(self.levelNomber)", forKey: "level")
self.txtMessageTitle.text = "Congratulations"
self.txtMessageSubTitle.text = "You won the game"
self.isGameOver = true
self.birdView.stopAnimating()
viewMessage.isHidden = false
congratulationView.isHidden = false
self.timer.invalidate()
}
can someone help me solve this error?
i want the the animation will see first before the congratulation messages appear. thank you

Loading string from plist swift

I am trying to load a string from plist with the code below, then i want to use it in typewriter animation. But I failed to convert the string into the array. How should I convert it?
The line var myText = Array(Data[0].characters) gives error.
class ViewController: UIViewController {
var Data = [String]()
override func viewDidLoad() {
super.viewDidLoad()
let word = NSBundle.mainBundle().pathForResource("Chapter 1", ofType: "plist")
let dict = NSDictionary(contentsOfFile: word!)
Data = dict!.objectForKey("Chapter 1") as! [String]
}
#IBOutlet weak var myTypeWriter: UILabel!
var myText = Array(Data[0].characters)
var myCounter = 0
var timer:NSTimer?
func fireTimer(){
timer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: #selector(ViewController.typeLetter), userInfo: nil, repeats: true)
}
func typeLetter(){
if myCounter < myText.count {
myTypeWriter.text = myTypeWriter.text! + String(myText[myCounter])
let randomInterval = Double((arc4random_uniform(8)+1))/100
timer?.invalidate()
timer = NSTimer.scheduledTimerWithTimeInterval(randomInterval, target: self, selector: #selector(ViewController.typeLetter), userInfo: nil, repeats: false)
} else {
timer?.invalidate()
}
myCounter += 1
}

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)
}
}
}