How to get Scrubbing to work on UISlider using AVAudioPlayer - ios

So I'm using a rather unique version of audio player and everything works except for track scrubbing. The tracks, it should be noted, are buffered and collected from a website. I thought it might have to do with the observers I'm using. But I don't know if it's that really. I'm able to call the duration of the track and I'm able to get the actual UISlider to animate in conjunction with the track progression. But I can't scrub forward or backward. What ends up happening is I can move the slider but the track's progress doesn't move along with it. Here's what I've got:
override func viewDidLoad() {
audio.initialize(config: ["loop": true]) { (playerItem, change) in
var duration = CMTimeGetSeconds(playerItem.duration)
self.scrubSlider.maximumValue = Float(duration)
}
scrubSlider.setThumbImage(UIImage(named: "position_knob"), for: .normal)
}
override func viewDidAppear(_ animated: Bool) {
scrubSlider.value = 0.0
print(audio.seekDuration())
self.scrubSlider.maximumValue = audio.seekDuration()
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.trackAudio), userInfo: nil, repeats: true)
print(self.scrubSlider.maximumValue)
}
#objc func trackAudio() {
var currentTime = audio.seekCurrentTime()
print(currentTime)
scrubSlider.value = currentTime
var duration = audio.seekDuration()
let remainingTimeInSeconds = duration - currentTime
timeElapsedLabel.text = audio.createTimeString(time: currentTime)
timeRemainingLabel.text = audio.createTimeString(time: remainingTimeInSeconds)
}
#IBAction func scrubberValueChanged(_ sender: UISlider) {
print("Entro")
var currentTime = (CMTimeGetSeconds((audio.playerQueue?.currentItem!.currentTime())!))
currentTime = (Float64)((scrubSlider.value))
}
Here's the observer where I collect the duration:
/**
Setup observers to monitor playback flow
*/
private func setupObservers(completion: #escaping (AVPlayerItem, Any) -> ()) {
// listening for current item change
self.audioQueueObserver = self.playerQueue?.observe(\.currentItem, options: [.new]) { [weak self] (player, _) in
print("media item changed...")
print("media number ", self?.playerQueue?.items() as Any, self?.playerQueue?.items().count as Any, self?.playerQueue?.currentItem as Any)
// loop here if needed //
if self?.audioPlayerConfig["loop"] as! Bool == true && self?.playerQueue?.items().count == 0 && self?.playerQueue?.currentItem == nil {
self?.playerQueue?.removeAllItems()
self?.playerQueue?.replaceCurrentItem(with: nil)
for item:AVPlayerItem in (self?.AVItemPool)! {
item.seek(to: CMTime.zero)
self?.playerQueue?.insert(item, after: nil)
}
self?.playerQueue?.play()
}
}
// listening for current item status change
self.audioQueueStatusObserver = self.playerQueue?.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { (playerItem, change) in
guard let currentItemDuration = self.playerQueue?.currentItem?.duration else { return }
if playerItem.status == .readyToPlay {
print("current item status is ready")
print("media Queue ", self.playerQueue?.items() as Any, self.playerQueue?.items().count as Any)
print("item duration ", CMTimeGetSeconds(playerItem.duration))
print("itemD ", CMTimeGetSeconds(currentItemDuration))
print(currentItemDuration)
completion(playerItem, change)
}
})

Related

AVPlayer.status doesn't run when wrapped in a DispatchWorkItem with a Delay

Because I'm playing videos in cells I have an AVPlayer that plays videos in certain circumstances immediately and others it runs a few seconds later. When it runs immediately the .status works fine. But when I wrap it in a DispatchWorkItem with a .asyncAfter delay that same exact .status is never called. I also tried to use a perform(_:, with:, afterDelay:) and a Timer but this didn't work either.
var player: AVPlayer?
var playerItem: AVPlayerItem?
var observer: NSKeyValueObservation? = nil
var workItem: DispatchWorkItem? = nil
var startImmediately = false
var timer: Timer?
viewDidLoad() {
// add asset to playerItem, add playerItem to player ...
}
func someCircumstance() {
if startImmediately {
setNSKeyValueObserver() // this works fine and .status is called
} else {
createWorkItem()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.33, execute: executeWorkItem) // this delay runs but .status is never called
// perform(#selector(executeWorkItem), with: nil, afterDelay: 0.33) // same issue
// timer = Timer.scheduledTimer(timeInterval: 0.33, target: self, selector: #selector(executeWorkItem), userInfo: nil, repeats: false) // same issue
// RunLoop.current.add(timer!, forMode: .common)
}
}
func createWorkItem() {
workItem = DispatchWorkItem {
DispatchQueue.main.async { [weak self] in
self?.setNSKeyValueObserver()
}
}
}
#objc func executeWorkItem() {
guard let workItem = workItem else { return }
workItem.perform()
}
func setNSKeyValueObserver() {
// without a long explanation this sometimes has to start with a delay because of scrolling reason I also might have to cancel it
observer = player?.observe(\.status, options: [.new, .old]) { [weak self] (player, change) in
switch (player.status) {
case .readyToPlay:
print("Media Ready to Play")
case .failed, .unknown:
print("Media Failed to Play")
#unknown default:
print("Unknown Error")
}
}
}
I also tried to use the older KVO API .status observer instead but the same issue occurred when using a delay
private var keepUpContext = 0
viewDidLoad() {
// add asset to playerItem, add playerItem to player ...
player?.addObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp", options: [.old, .new], context: &keepUpContext)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &keepUpContext {
if let player = player, let item = player.currentItem, item.isPlaybackLikelyToKeepUp {
print("Media Ready to Play")
}
}
}
The problem seems to be the delay.
Update
I just tried the following code and and this doesn't work either:
if startImmediately {
setNSKeyValueObserver()
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
self?.setNSKeyValueObserver()
}
}
It looks like you're adding your observer after the player has loaded the content. It likely loads between the viewDidLoad and the delay. If you add .initial to the list of options when adding the observer, you'll be sure to get the state notification even if the player is already ready.
Can you try to change the forKeyPath parameter value to #keyPath(AVPlayerItem.status)

Swift 4 - UISlider with AVAudioPlayer

I am using AVAudioPlayer to play audio, now I am trying to user UISlider as the audio timeline
#IBOutlet var audioSlider: UISlider!
override func viewDidLoad() {
audioSlider.setValue(0, animated: false)
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: (#selector(PlayerController.updateTimer)), userInfo: nil, repeats: true)
}
#objc func updateTimer() {
if(audio != nil)
{
audioSlider.setValue(Float(Int((self.audio?.currentTime)!)), animated: false)
}
}
But I don't think its working, my UISlider goes from the start to the end right away. I am expected a smooth transition with my UISlider
Here is how I am playing audio:
#IBAction func playButtonPressed(_ sender: Any) {
if(playButton.titleLabel?.text == "Play")
{
postPlay(postid: self.postid) { results in
DispatchQueue.main.async {
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
}
catch {
}
self.audio?.delegate = self
self.audio?.numberOfLoops = 0
self.playButton.titleLabel?.text = "Stop"
self.audio?.play()
self.playCount = self.playCount + 1
self.plays.text = "Total Plays: " + self.playCount.description
}
}
}
else
{
playButton.titleLabel?.text = "Resume"
audio?.stop()
}
}
The UISlider expects a value between 0.0 and 1.0. To achieve this you must divide the current progress by the total time.
For Example
audioSlider.setValue(Float(self.audio?.currentTime! / self.audio?.duration!), animated: false)

Jumpy UISlider when scrubbing - Using UISlider with AVPlayer

I am using AvPlayer and am trying to set up a slider to allow scrubbing of audio files. Im having a problem with the slider jumping all over the place when its selected. It then goes back to the origin position for a second before going back to the location it was dragged to.
You cant see my cursor on the Gif, but the smooth elongated drags are me moving the knob, then the quick whips are the slider misbehaving.
Ive spent hours googling and combing through Stack Overflow and cant figure out what I'm doing wrong here, a lot of similar questions are quite old and in ObjC.
This is the section of code i think is responsible for the problem, it does handle the event of the slider being moved: Ive tried it without the if statement also and didn't see a different result.
#IBAction func horizontalSliderActioned(_ sender: Any) {
horizontalSlider.isContinuous = true
if self.horizontalSlider.isTouchInside {
audioPlayer?.pause()
let seconds : Int64 = Int64(horizontalSlider.value)
let preferredTimeScale : Int32 = 1
let seekTime : CMTime = CMTimeMake(seconds, preferredTimeScale)
audioPlayerItem?.seek(to: seekTime)
audioPlayer?.play()
} else {
let duration : CMTime = (self.audioPlayer?.currentItem!.asset.duration)!
let seconds : Float64 = CMTimeGetSeconds(duration)
self.horizontalSlider.value = Float(seconds)
}
}
I will include my entire class below for reference.
import UIKit
import Parse
import AVFoundation
import AVKit
class PlayerViewController: UIViewController, AVAudioPlayerDelegate {
#IBOutlet var horizontalSlider: UISlider!
var selectedAudio: String!
var audioPlayer: AVPlayer?
var audioPlayerItem: AVPlayerItem?
var timer: Timer?
func getAudio() {
let query = PFQuery(className: "Part")
query.whereKey("objectId", equalTo: selectedAudio)
query.getFirstObjectInBackground { (object, error) in
if error != nil || object == nil {
print("The getFirstObject request failed.")
} else {
print("There is an object now get the Audio. ")
let audioFileURL = (object?.object(forKey: "partAudio") as! PFFile).url
self.audioPlayerItem = AVPlayerItem(url: NSURL(string: audioFileURL!) as! URL)
self.audioPlayer = AVPlayer(playerItem: self.audioPlayerItem)
let playerLayer = AVPlayerLayer(player: self.audioPlayer)
playerLayer.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
self.view.layer.addSublayer(playerLayer)
let duration : CMTime = (self.audioPlayer?.currentItem!.asset.duration)!
let seconds : Float64 = CMTimeGetSeconds(duration)
let maxTime : Float = Float(seconds)
self.horizontalSlider.maximumValue = maxTime
self.audioPlayer?.play()
self.timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(PlayerViewController.audioSliderUpdate), userInfo: nil, repeats: true)
}
}
}
#IBOutlet var playerButton: UIButton!
func playerButtonTapped() {
if audioPlayer?.rate == 0 {
audioPlayer?.play()
self.playerButton.setImage(UIImage(named: "play"), for: UIControlState.normal)
} else {
audioPlayer?.pause()
self.playerButton.setImage(UIImage(named: "pause"), for: UIControlState.normal)
}
}
override func viewDidLoad() {
super.viewDidLoad()
horizontalSlider.minimumValue = 0
horizontalSlider.value = 0
self.playerButton.addTarget(self, action: #selector(PlayerViewController.playerButtonTapped), for: UIControlEvents.touchUpInside)
getAudio()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(PlayerViewController.finishedPlaying), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.audioPlayerItem)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillAppear(animated)
// remove the timer
self.timer?.invalidate()
// remove the observer when leaving page
NotificationCenter.default.removeObserver(audioPlayer?.currentItem! as Any)
}
func finishedPlaying() {
// need option to play next track
self.playerButton.setImage(UIImage(named: "play"), for: UIControlState.normal)
let seconds : Int64 = 0
let preferredTimeScale : Int32 = 1
let seekTime : CMTime = CMTimeMake(seconds, preferredTimeScale)
audioPlayerItem!.seek(to: seekTime)
}
#IBAction func horizontalSliderActioned(_ sender: Any) {
horizontalSlider.isContinuous = true
if self.horizontalSlider.isTouchInside {
audioPlayer?.pause()
let seconds : Int64 = Int64(horizontalSlider.value)
let preferredTimeScale : Int32 = 1
let seekTime : CMTime = CMTimeMake(seconds, preferredTimeScale)
audioPlayerItem?.seek(to: seekTime)
audioPlayer?.play()
} else {
let duration : CMTime = (self.audioPlayer?.currentItem!.asset.duration)!
let seconds : Float64 = CMTimeGetSeconds(duration)
self.horizontalSlider.value = Float(seconds)
}
}
func audioSliderUpdate() {
let currentTime : CMTime = (self.audioPlayerItem?.currentTime())!
let seconds : Float64 = CMTimeGetSeconds(currentTime)
let time : Float = Float(seconds)
self.horizontalSlider.value = time
}
}
Swift 5, Xcode 11
I faced the same issue, it was apparently periodicTimeObserver which was causing to return incorrect time which caused lag or jump in the slider. I solved it by removing periodic time observer when the slider was changing and adding it back when seeking completion handler was called.
#objc func sliderValueChanged(_ playbackSlider: UISlider, event: UIEvent){
let seconds : Float = Float(playbackSlider.value)
let targetTime:CMTime = CMTimeMake(value: Int64(seconds), timescale: 1)
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .began:
// handle drag began
//Remove observer when dragging is in progress
self.removePeriodicTimeObserver()
break
case .moved:
// handle drag moved
break
case .ended:
// handle drag ended
//Add Observer back when seeking got completed
player.seek(to: targetTime, toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] (value) in
self?.addTimeObserver()
}
break
default:
break
}
}
}
you need to remove observers and invalidate timers as soon as user selects the thumb on slider and add them back again when dragging is done
to do add targets like this where you load your player:
mySlider.addTarget(self,
action: #selector(PlayerViewController.mySliderBeganTracking(_:)),
forControlEvents:.TouchDown)
mySlider.addTarget(self,
action: #selector(PlayerViewController.mySliderEndedTracking(_:)),
forControlEvents: .TouchUpInside)
mySlider.addTarget(self,
action: #selector(PlayerViewController.mySliderEndedTracking(_:)),
forControlEvents: .TouchUpOutside )
and remove observers and invalidate timers in mySliderBeganTracking then add observers in mySliderEndedTracking
for better control on what happens in your player write 2 functions : addObservers and removeObservers and call them when needed
Make sure to do the following:
isContinuous for the slider is NOT set to false.
Pause the player before seeking.
Seek to the position and use the completion handler to resume playing.
Example code:
#objc func sliderValueChanged(sender: UISlider, event: UIEvent) {
let roundedValue = sender.value.rounded()
guard let touchEvent = event.allTouches?.first else { return }
switch touchEvent.phase {
case .began:
PlayerManager.shared.pause()
case .moved:
print("Slider moved")
case .ended:
PlayerManager.shared.seek(to: roundedValue, playAfterSeeking: true)
default: ()
}
}
And here is the function for seeking:
func seek(to: Float, playAfterSeeking: Bool) {
player?.seek(to: CMTime(value: CMTimeValue(to), timescale: 1), completionHandler: { [weak self] (status) in
if playAfterSeeking {
self?.play()
}
})
}
Try using the time slider value like below:
#IBAction func timeSliderDidChange(_ sender: UISlider) {
AVPlayerManager.sharedInstance.currentTime = Double(sender.value)
}
var currentTime: Double {
get {
return CMTimeGetSeconds(player.currentTime())
}
set {
if self.player.status == .readyToPlay {
let newTime = CMTimeMakeWithSeconds(newValue, 1)
player.seek(to: newTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero) { ( _ ) in
self.updatePlayerInfo()
}
}
}
}
and pass the value of slider when user release the slider, also don't update the slider value of current playing while user interaction happening on the slider
This is a temporary solution for me, I observed that the rebound is only once, so I set an int value isSeekInProgress:
When sliderDidFinish, isSeekInProgress = 0
In reply to avplayer time change:
if (self.isSeekInProgress > 1) {
float sliderValue = 1.f / (self.slider.maximumValue - self.slider.minimumValue) * progress;
// if (sliderValue > self.slider.value ) {
self.slider.value = sliderValue;
}else {
self.isSeekInProgress += 1;
}

AVPlayer layer showing blank screen when application running or idle for 15 minutes

I would like to create a custom video player using AVPlayer() and AVPlayerLayer() classes.
My code working fine for application fresh start but goes wrong and showing blank screen when the application running or idle for more than 15 minutes.
When the occurrences this issue all classes of my application having AVPlayerLayer showing blank screen. I don't know why this happens.
AVPlayer() and AVPlayerlayer() instances are initialized as below.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
dispatch_async(dispatch_get_main_queue(), {
let playerItem = AVPlayerItem(URL: self.itemUrl)
self.avPlayer = AVPlayer(playerItem: playerItem)
NSNotificationCenter.defaultCenter().addObserver(self,
selector: #selector(VideoPreviewView.restartVideoFromBeginning),
name: AVPlayerItemDidPlayToEndTimeNotification,
object: self.avPlayer.currentItem)
self.avPlayerLayer = AVPlayerLayer(player: self.avPlayer)
self.view.layer.addSublayer(self.avPlayerLayer)
self.avPlayerLayer.frame = self.view.bounds
self.avPlayer.pause()
})
}
}
Play function
func playVideo(sender: AnyObject) {
avPlayer.play()
if (avPlayer.rate != 0 && avPlayer.error == nil) {
print("playing")
}
slider.hidden=false
myTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: #selector(VideoPreviewView.updateSlider), userInfo: nil, repeats: true)
slider.addTarget(self, action: #selector(VideoPreviewView.sliderValueDidChange(_: )), forControlEvents: UIControlEvents.ValueChanged)
slider.minimumValue = 0.0
slider.continuous = true
pauseButton.hidden=false
playButton.hidden=true
closeViewButton.hidden = false
}
Restart video
func restartVideoFromBeginning() {
let seconds: Int64 = 0
let preferredTimeScale: Int32 = 1
let seekTime: CMTime = CMTimeMake(seconds, preferredTimeScale)
avPlayer?.seekToTime(seekTime)
avPlayer?.pause()
pauseButton.hidden=true
playButton.hidden = false
closeViewButton.hidden=false
}
func updateSlider() {
if (avPlayer.rate != 0 && avPlayer.error == nil) {
print("playing")
}
else{
print("Not playing.")
}
currentTime = Float(CMTimeGetSeconds(avPlayer.currentTime()))
duration = avPlayer.currentItem!.asset.duration
totalDuration = Float(CMTimeGetSeconds(duration))
slider.value = currentTime // Setting slider value as current time
slider.maximumValue = totalDuration // Setting maximum value as total duration of the video
}
func sliderValueDidChange(sender: UISlider!) {
timeInSecond=slider.value
newtime = CMTimeMakeWithSeconds(Double(timeInSecond), 1)// Setting new time using slider value
avPlayer.seekToTime(newtime)
}
#IBAction func closeViewAction(sender: AnyObject) {
pauseButton.hidden=true
self.avPlayer.pause()
self.avPlayerLayer.removeFromSuperlayer()
self.dismissViewControllerAnimated(true, completion: nil)
}

Running a timer when the phone is sleeping

I'm building an app and I need a timer to run if the user sends the screen to the background, or if they put the phone in sleep and open it again. I need the timer to still be going.
I tried recording the time when I exit the and enter it again, subtracting the two and adding that to the running count, and it seems to work fine on the Xcode simulator but when I run it on my phone it doesn't work. Any ideas?
Here is the code for reference.
And the timer starts with a button I didn't include that part but it's just a simple IBAction that calls the timer.fire() function.
var time = 0.0
var timer = Timer()
var exitTime : Double = 0
var resumeTime : Double = 0
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
exitTime = Date().timeIntervalSinceNow
}
override func awakeFromNib() {
super.awakeFromNib()
resumeTime = Date().timeIntervalSinceNow
time += (resumeTime-exitTime)
timer.fire()
}
func startTimer() {
if !isTimeRunning {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:
#selector(WorkoutStartedViewController.action), userInfo: nil, repeats: true)
isTimeRunning = true
}
}
func pauseTimer() {
timer.invalidate()
isTimeRunning = false
}
#objc func action()
{
time += 0.1
timerLabel.text = String(time)
let floorCounter = Int(floor(time))
let hour = floorCounter/3600
let minute = (floorCounter % 3600)/60
var minuteString = "\(minute)"
if minute < 10 {
minuteString = "0\(minute)"
}
let second = (floorCounter % 3600) % 60
var secondString = "\(second)"
if second < 10 {
secondString = "0\(second)"
}
if time < 3600.0 {
timerLabel.text = "\(minuteString):\(secondString)"
} else {
timerLabel.text = "\(hour):\(minuteString):\(secondString)"
}
}
You do have the right idea but the first problem I see is that viewWillDissapear is only called when you leave a view controller to go to a new viewController - It is not called when the app leaves the view to enter background (home button press)
I believe the callback functions you are looking for are UIApplication.willResignActive (going to background) and UIApplication.didBecomeActive (app re-opened)
You can access these methods in the AppDelegate or you can set them up on a view controller heres a mix of your code and some changes to produce a working sample on one initial VC:
import UIKit
import CoreData
class ViewController: UIViewController {
#IBOutlet weak var timerLabel: UILabel!
var time = 0.0
var timer = Timer()
var exitTime : Date? // Change to Date
var resumeTime : Date? // Change to Date
var isTimeRunning = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
startTimer()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self,
selector: #selector(applicationDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil)
// Add willResign observer
NotificationCenter.default.addObserver(self,
selector: #selector(applicationWillResign),
name: UIApplication.willResignActiveNotification,
object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
// Remove becomeActive observer
NotificationCenter.default.removeObserver(self,
name: UIApplication.didBecomeActiveNotification,
object: nil)
// Remove becomeActive observer
NotificationCenter.default.removeObserver(self,
name: UIApplication.willResignActiveNotification,
object: nil)
}
func startTimer() {
if !isTimeRunning {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:
#selector(self.action), userInfo: nil, repeats: true)
isTimeRunning = true
}
}
#objc func action() {
time += 0.1
timerLabel.text = String(time)
let floorCounter = Int(floor(time))
let hour = floorCounter/3600
let minute = (floorCounter % 3600)/60
var minuteString = "\(minute)"
if minute < 10 {
minuteString = "0\(minute)"
}
let second = (floorCounter % 3600) % 60
var secondString = "\(second)"
if second < 10 {
secondString = "0\(second)"
}
if time < 3600.0 {
timerLabel.text = "\(minuteString):\(secondString)"
} else {
timerLabel.text = "\(hour):\(minuteString):\(secondString)"
}
}
#objc func applicationDidBecomeActive() {
// handle event
lookForActiveTimers()
}
func lookForActiveTimers() {
var timers = [NSManagedObject]()
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Timers")
//3
do {
timers = try managedContext.fetch(fetchRequest)
print("timers: \(timers)")
var activeTimer: NSManagedObject?
for timer in timers {
if let active = timer.value(forKey: "active") as? Bool {
if active {
activeTimer = timer
}
}
}
if let activeTimer = activeTimer {
// Handle active timer (may need to go to a new view)
if let closeDate = activeTimer.value(forKey: "appCloseTime") as? Date {
if let alreadyTimed = activeTimer.value(forKey: "alreadyTimed") as? Double {
let now = Date()
let difference = now.timeIntervalSince(closeDate)
// Handle set up again here
print("App opened with a difference of \(difference) and already ran for a total of \(alreadyTimed) seconds before close")
time = alreadyTimed + difference
startTimer()
}
}
} else {
print("We dont have any active timers")
}
// Remove active timers because we reset them up
for timer in timers {
managedContext.delete(timer)
}
do {
print("deleted")
try managedContext.save() // <- remember to put this :)
} catch {
// Do something... fatalerror
}
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
#objc func applicationWillResign() {
// handle event
saveActiveTimer()
}
func saveActiveTimer() {
if isTimeRunning {
// Create a new alarm object
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let context = appDelegate.persistentContainer.viewContext
if let entity = NSEntityDescription.entity(forEntityName: "Timers", in: context) {
let newTimer = NSManagedObject(entity: entity, insertInto: context)
newTimer.setValue(true, forKey: "active")
let now = Date()
newTimer.setValue(now, forKey: "appCloseTime")
newTimer.setValue(self.time, forKey: "alreadyTimed")
do {
try context.save()
print("object saved success")
} catch {
print("Failed saving")
}
}
}
}
}
EDIT - Here is the full tested and working code on xCode 11.3 and a physical device iOS 13.2 - You have to figure out how to start and stop the timer according to your buttons - but this example simply starts the timer when the app is first opened and never stops or resets it.
You can reproduce this by creating a new single-view xCode project and replacing the code in the first view controller that it creates for you with the code above. Then create a label to attach to the outlet timerLabel on the VC
Also make sure to enable CoreData in your project while creating your new project * Then set up the entities and attributes in the xcdatamodel file:
Hope this helps

Resources