Swift: ползунок аудио трека подлагивает

В моем приложении есть аудиоплеер. И ползунок для перемещения звуковой дорожки. Но, например, в моем приложении на экране блокировки ползунок не подлагивает, когда его скролишь влево вправо. И он не пытается проигрывать аудио в момент перемотки. Когда его зажимаешь он продолжает проигрывает аудио, а проигрывать новое время он начинает только после отпускания ползунка. Поесть он нормально работает как в Apple Music.

А внутри моего приложения тоже слайдер и он зависает, когда я его скроллю. И чем больше размер аудиофайла, тем сильнее зависает ползунок при прокрутке. То есть если я его из начала трека перемещаю в конец, то попутно он цепляет куски аудио трека и пытается проиграть их. А не плавно перемещается как на заблокированном экране. Как это исправить?

@IBAction func slide(_ slider: UISlider) {
    musicOperation.cancelAllOperations()
    let operation = BlockOperation()
    audioPlayer.currentTime = TimeInterval(slider.value)
    musicOperation.addOperation(operation)
}

Update

В updateTime() закомментировал slider.value = Float.init(audioPlayer.currentTime) это помогло убрать подергивания и мигание. Но в fastForward() и fastBackward() при нажатии трек перематывался, но слайдер был на месте поэтому закомментировал в этих функциях вызов //updateTime() и добавил вместо slider.value = Float.init(audioPlayer.currentTime). Помогло.

Так же слайдер перестал запоминать свое местоположение при блокировке экрана, перемотке трека на заблокированном экране и следующем возвращении в приложение. Потому что как писал ранее убрал эту строку slider.value = Float.init(audioPlayer.currentTime) в updateTime(). Поэтому решил добавить вызов applicationWillEnterForeground. Правильно ли я все сделал?

Осталась проблема 3. И еще два маленьких вопроса:

  1. Я так понимаю что мне вообще не нужно использовать musicOperation верно?

  2. После того как начал использовать Touch Up Inside появилось странное поведение. Например, если я передвигаю туда сюда ползунок в +- вертикальных рамках слайдера (то есть его размер 30 по стандарту, а палец я держу чуть выше или ниже) то все нормально. Но если я зажму слайдер а палец уведу на верх экрана iPhone или вниз, то я увижу как бы анимацию отжимания ползунка, но все равно смогу им управлять. И если я отожму палец и ползунок остановится, то он просто не выполнит свою функцию. Аудио не переключится на выбранное место. Раньше до использования Touch Up Inside делал тоже самое уводил палец вверх не отжимая слайдер при этом видел анимацию отжимания, но после отпускания пальца трек играл в месте остановки ползунка

     var audioPlayer: AVAudioPlayer!
     let musicOperation = OperationQueue()
    
     override func viewDidLoad() {
         super.viewDidLoad()
    
         NotificationCenter.default.addObserver( self, selector: #selector(ViewController.applicationWillEnterForeground(notification:)), name:NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
    
     musicOperation.maxConcurrentOperationCount = 1       
     try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
    
     let url = Bundle.main.url(forResource: "0", withExtension: "m4a")!
    
         do {
    
             audioPlayer = try AVAudioPlayer(contentsOf: url)
             audioPlayer.delegate = self
             audioPlayer.prepareToPlay()
             play(sender:AnyObject.self as AnyObject)
    
             setupMediaPlayerNotificationView(true)
             lockScreen()
    
         } catch {
    
         }
    
     }
    
     @IBAction func play(sender: AnyObject) {
         if !audioPlayer.isPlaying{
             audioPlayer.play()
             slider.maximumValue = Float(audioPlayer.duration)
             timer = Timer(timeInterval: 0.1, target: self, selector: #selector(self.updateTime), userInfo: nil, repeats: true)
             RunLoop.main.add(timer!, forMode: .commonModes)
             restorePlayerCurrentTime()
             playButton.setImage(UIImage(named: "pause.png"), for: UIControlState.normal)
         } else {
             audioPlayer.pause()
             playButton.setImage(UIImage(named: "play.png"), for: UIControlState.normal)
             timer?.invalidate()
         }
     }
    
     @IBAction func fastForward(sender: AnyObject) {
         var time: TimeInterval = audioPlayer.currentTime
         time += 15.0 // Go Forward by 15 Seconds
         if time > audioPlayer.duration {
             audioPlayerDidFinishPlaying(audioPlayer, successfully: true)
         } else {
             audioPlayer.currentTime = time
             slider.value = Float.init(audioPlayer.currentTime) //updateTime()
         }
         self.lockScreen()
     }
    
     @IBAction func fastBackward(sender: AnyObject) {
         var time: TimeInterval = audioPlayer.currentTime
         time -= 15.0 // Go Back by 15 Seconds
         if time < 0 {
             audioPlayer.currentTime = 0
             slider.value = Float.init(audioPlayer.currentTime) //updateTime()
         } else {
             audioPlayer.currentTime = time
             slider.value = Float.init(audioPlayer.currentTime) //updateTime()
         }
         self.lockScreen()
     }
    
     private func restorePlayerCurrentTime() {
         let currentTimeFromUserDefaults : Double? = UserDefaults.standard.value(forKey: "currentTime\(masterIndex)\(index)") as! Double?
         if let currentTimeFromUserDefaultsValue = currentTimeFromUserDefaults {
             audioPlayer.currentTime = currentTimeFromUserDefaultsValue
             slider.value = Float.init(audioPlayer.currentTime)
         }
     }
    
     @objc func updateTime() {
         let currentTime = Int(audioPlayer.currentTime)
         let minutes = currentTime/60
         let seconds = currentTime - minutes * 60
    
         let durationTime = Int(audioPlayer.duration) - Int(audioPlayer.currentTime)
         let minutes1 = durationTime/60
         let seconds1 = durationTime - minutes1 * 60
    
         timeElapsed.text = NSString(format: "%02d:%02d", minutes,seconds) as String
         timeDuration.text = NSString(format: "-%02d:%02d", minutes1,seconds1) as String
    
         UserDefaults.standard.set(currentTime, forKey: "currentTime\(masterIndex)\(index)")
         UserDefaults.standard.set(durationTime, forKey: "durationTime\(masterIndex)\(index)")
    
         //slider.value = Float.init(audioPlayer.currentTime)
     }
    
     func audioPlayerDidFinishPlaying(_ audioPlayer: AVAudioPlayer, successfully flag: Bool) {
    
         playButton.setImage(UIImage(named: "play.png"), for: UIControlState.normal)
    
         let currentTime = 0
         let durationTime = 0.1
         UserDefaults.standard.set(currentTime, forKey: "currentTime\(masterIndex)\(index)")
         UserDefaults.standard.set(durationTime, forKey: "durationTime\(masterIndex)\(index)")
         slider.value = Float.init(audioPlayer.currentTime)
         timer?.invalidate()
    
         let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
         let documentDirectoryPath:String = path[0]
         let fileManager = FileManager()
         let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/\(masterIndex)/\(index+1).mp3"))
    
         if fileManager.fileExists(atPath: destinationURLForFile.path){
    
             if endOfChapterSleepTimer == true {
                 endOfChapterSleepTimer = false
             } else {
                 index = index + 1
                 viewDidLoad()
             }
    
         } else {
    
         }
     }
    
     @IBAction func slide(_ slider: UISlider) {
         musicOperation.cancelAllOperations()
         let operation = BlockOperation()
         audioPlayer.currentTime = TimeInterval(slider.value)
         self.lockScreen()
         musicOperation.addOperation(operation)
     }
    
     @objc func applicationWillEnterForeground(notification: NSNotification)  {
         slider.value = Float.init(audioPlayer.currentTime)
     }
    

Ответы (1 шт):

Автор решения: schmidt9

Назначьте проигрывание после перемотки на событие touchUpInside, то есть на момент отпускания слайдера

slider.addTarget(self, action: #selector(sliderTouchUpInside(_ :)), for: .touchUpInside)
@IBAction func sliderTouchUpInside(_ sender: UISlider) {
    musicOperation.cancelAllOperations()
    let operation = BlockOperation()
    audioPlayer.currentTime = TimeInterval(time)
    musicOperation.addOperation(operation)
}

Update

Здесь пример работы с плеером на основе вашего кода, BlockOperation здесь не требуется, касательно события Touch Up Inside - как следует из названия, это события завершения касания внутри контрола, чтобы отработать завершение касания и за его пределами, я добавил обработку Touch Up Outside. Также я уменьшил частоту срабатывания таймера, 0.5 сек вполне достаточно, если вы выводите время с точностью до секунд.

import UIKit
import AVKit

class ViewController: UIViewController {
    
    @IBOutlet var slider: UISlider!
    @IBOutlet var timeElapsed: UILabel!
    @IBOutlet var timeDuration: UILabel!
    @IBOutlet var playButton: UIButton!
    
    var audioPlayer: AVAudioPlayer!
    var timer: Timer?
    var isRewindMode = false
    
    // MARK: --
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setup()
    }
    
    private func setup() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(applicationWillEnterForeground(notification:)),
            name:UIApplication.willEnterForegroundNotification,
            object: nil)
        
        slider.addTarget(
            self,
            action: #selector(sliderTouchUp(_ :)),
            for: [.touchUpInside, .touchUpOutside])
        
        slider.addTarget(
            self,
            action: #selector(sliderTouchDown(_ :)),
            for: .touchDown)
        
        slider.addTarget(
            self,
            action: #selector(sliderValueChanged(_ :)),
            for: .valueChanged)
        
        setupTestPlayback()
    }
    
    // MARK: Playback
    
    private func setupTestPlayback() {
        do {
            UIApplication.shared.beginReceivingRemoteControlEvents()
            
            let session = AVAudioSession.sharedInstance()
            try session.setCategory(AVAudioSession.Category.playback,
                                    mode: .default,
                                    policy: .longFormAudio,
                                    options: [])
            try session.setActive(true, options: [])
            
            let url = Bundle.main.url(forResource: "0", withExtension: "mp3")!
            
            audioPlayer = try AVAudioPlayer(contentsOf: url)
            audioPlayer.delegate = self
            
            if !audioPlayer.prepareToPlay() {
                print("failed to prepare playback")
                return
            }
            
            restorePlayerCurrentTime()
            play()
        } catch {
            print(error)
        }
    }
    
    private func setPlayerCurrentTime(_ time: Float) {
        audioPlayer.currentTime = TimeInterval(time)
    }
    
    private func togglePlayback() {
        if audioPlayer.isPlaying {
            pause()
        } else {
            play()
        }
    }
    
    private func play() {
        audioPlayer.play()
        playButton.setTitle("Pause", for: .normal)
        slider.maximumValue = Float(audioPlayer.duration)
        timer = Timer.scheduledTimer(
            timeInterval: 0.5, // no need to set too frequent period
            target: self,
            selector: #selector(updateTime),
            userInfo: nil,
            repeats: true)
    }
    
    private func pause() {
        audioPlayer.pause()
        playButton.setTitle("Play", for: .normal)
        timer?.invalidate()
    }
    
    @objc func updateTime() {
        let currentTime = Int(audioPlayer.currentTime)
        let minutes = currentTime / 60
        let seconds = currentTime - minutes * 60
        
        let durationTime = Int(audioPlayer.duration) - Int(audioPlayer.currentTime)
        let minutes1 = durationTime / 60
        let seconds1 = durationTime - minutes1 * 60

        timeElapsed.text = NSString(format: "%02d:%02d", minutes, seconds) as String
        timeDuration.text = NSString(format: "-%02d:%02d", minutes1, seconds1) as String

        UserDefaults.standard.set(currentTime, forKey: "currentTime")
        UserDefaults.standard.set(durationTime, forKey: "durationTime")

        if !isRewindMode {
            slider.value = Float(audioPlayer.currentTime)
        }
    }
    
    private func restorePlayerCurrentTime() {
        let currentTime = UserDefaults.standard.float(forKey: "currentTime")
        setPlayerCurrentTime(currentTime)
        slider.value = Float(audioPlayer.currentTime)
    }
    
    // MARK: UI Events
    
    @IBAction func playButtonTouchUpInside(_ sender: UIButton) {
        togglePlayback()
    }
    
    @IBAction func sliderValueChanged(_ sender: UISlider) {
        isRewindMode = true
        
        if !audioPlayer.isPlaying {
            // rewind time if playback is stopped
            setPlayerCurrentTime(sender.value)
            updateTime()
        }
    }
    
    @IBAction func sliderTouchUp(_ sender: UISlider) {
        print(#function)
        setPlayerCurrentTime(sender.value)
        isRewindMode = false
    }
    
    @IBAction func sliderTouchDown(_ sender: UISlider) {
        print(#function)
        isRewindMode = true
    }
    
    @objc func applicationWillEnterForeground(notification: NSNotification)  {
        restorePlayerCurrentTime()
    }
    
}

extension ViewController : AVAudioPlayerDelegate {
    // TODO: implement
}

→ Ссылка