AVPlayer


3つ目のテーマは音声再生についてです.簡単な音楽プレーヤーを作って核心概念を学ぶ

クリティカルオブジェクトのクリーンアップ


AVPlayerItem


音声ファイルをオブジェクト化します.assetまたはurlを使用してinitできます.

AVPlayer


実際の音声再生キャラクタのオブジェクトとして機能します.再生や停止など、音声再生に関する方法はいくつかあります.

ベース(再生、一時停止)


UI準備


十分なボタン再生ボタンと一時停止ボタンしか追加されていません.各ボタンは、再生および一時停止を実行するコレクタに接続されています.
import UIKit

class VC3: UIViewController {
    
    // MARK: Properties

    let player = AVPlayer()
    
    let playButton: UIButton = {
        let button = UIButton()
        button.setImage(UIImage(systemName: "play.fill"), for: .normal)
        button.tintColor = .black
        button.widthAnchor.constraint(equalToConstant: 50).isActive = true
        button.heightAnchor.constraint(equalToConstant: 50).isActive = true
        button.contentVerticalAlignment = .fill
        button.contentHorizontalAlignment = .fill
        button.addTarget(self, action: #selector(play), for: .touchUpInside)
        return button
    }()
    
    let pauseButton: UIButton = {
        let button = UIButton()
        button.setImage(UIImage(systemName: "pause.fill"), for: .normal)
        button.tintColor = .black
        button.widthAnchor.constraint(equalToConstant: 50).isActive = true
        button.heightAnchor.constraint(equalToConstant: 50).isActive = true
        button.contentVerticalAlignment = .fill
        button.contentHorizontalAlignment = .fill
        button.addTarget(self, action: #selector(pause), for: .touchUpInside)
        return button
    }()
    
    // MARK: LifeCycle

    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
    }
    
    // MARK: Selector
    
    @objc func play() {
        
    }
    
    @objc func pause() {
        
    }
    
    // MARK: Helpers
    
    func configureUI() {
        view.backgroundColor = .white
        
        view.addSubview(playButton)
        playButton.translatesAutoresizingMaskIntoConstraints = false
        playButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: -50).isActive = true
        playButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -100).isActive = true
        
        view.addSubview(pauseButton)
        pauseButton.translatesAutoresizingMaskIntoConstraints = false
        pauseButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 50).isActive = true
        pauseButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -100).isActive = true
    }

}

AVPlayer ItemをAVPlayerに入れる


サウンドファイルを束に配置します.前述したように、AVPlayerItemはurlまたはassetを使用してinitすることができる.ここではurlでinitを初期化しました.
AVPlayer ItemをAVPlayerの代替現在のItemメソッドとして指定します.
func configurePlayer() {
    guard let url = Bundle.main.url(forResource: "sound", withExtension: "mp3") else {
        print("Failed to load sound")
        return
    }
    let item = AVPlayerItem(url: url)
    player.replaceCurrentItem(with: item)
}

再生、一時停止


再生と一時停止は簡単です.AVPlayerのPlay()とPause()メソッドを使用します.
@objc func play() {
    player.play()
}

@objc func pause() {
    player.pause()
}

詳細(再生時間の表示とナビゲート)


UI準備


上のUIにナビゲーション用のスライダと現在の時刻と合計時刻を表示するラベルを追加します.
スライダは2つの関数を接続します.1つ目はisSeeking変数を切り替えるコレクタで、ナビゲーションの開始と終了時にそれぞれナビゲーション中であることを示します.2つ目は、スライダの値が変更されたときに実際のナビゲーションを実行するコレクタです.
import UIKit
import AVFoundation

let buttonSize: CGFloat = 30

class VC3: UIViewController {
    
    // MARK: Properties
    
    let player = AVPlayer()
    
    let playButton: UIButton = {
        let button = UIButton()
        button.setImage(UIImage(systemName: "play.fill"), for: .normal)
        button.tintColor = .black
        button.widthAnchor.constraint(equalToConstant: buttonSize).isActive = true
        button.heightAnchor.constraint(equalToConstant: buttonSize).isActive = true
        button.contentVerticalAlignment = .fill
        button.contentHorizontalAlignment = .fill
        button.addTarget(self, action: #selector(play), for: .touchUpInside)
        return button
    }()
    
    let pauseButton: UIButton = {
        let button = UIButton()
        button.setImage(UIImage(systemName: "pause.fill"), for: .normal)
        button.tintColor = .black
        button.widthAnchor.constraint(equalToConstant: buttonSize).isActive = true
        button.heightAnchor.constraint(equalToConstant: buttonSize).isActive = true
        button.contentVerticalAlignment = .fill
        button.contentHorizontalAlignment = .fill
        button.addTarget(self, action: #selector(pause), for: .touchUpInside)
        return button
    }()
    
    let timeSlider: UISlider = {
        let slider = UISlider()
        slider.thumbTintColor = .black
        slider.addTarget(self, action: #selector(toggleIsSeeking), for: .editingDidBegin)
        slider.addTarget(self, action: #selector(toggleIsSeeking), for: .editingDidEnd)
        slider.addTarget(self, action: #selector(seek(sender:)), for: .valueChanged)
        return slider
    }()
    
    var isSeeking: Bool = false

    let currentTimeLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 20)
        label.text = "현재 시간"
        return label
    }()
    
    let fullTimeLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 20)
        label.text = "전체 시간"
        return label
    }()
    
    // MARK: LifeCycle

    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
        configurePlayer()
    }
    
    // MARK: Selector
    
    @objc func play() {
        player.play()
    }
    
    @objc func pause() {
        player.pause()
    }
    
    @objc func seek() {
        
    }

    @objc func toggleIsSeeking() {
        isSeeking.toggle()
    }
    
    // MARK: Helpers
    
    func configureUI() {
        view.backgroundColor = .white
        
        view.addSubview(playButton)
        playButton.translatesAutoresizingMaskIntoConstraints = false
        playButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: -30).isActive = true
        playButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -100).isActive = true
        
        view.addSubview(pauseButton)
        pauseButton.translatesAutoresizingMaskIntoConstraints = false
        pauseButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 30).isActive = true
        pauseButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -100).isActive = true
        
        view.addSubview(timeSlider)
        timeSlider.translatesAutoresizingMaskIntoConstraints = false
        timeSlider.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 10).isActive = true
        timeSlider.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10).isActive = true
        timeSlider.bottomAnchor.constraint(equalTo: playButton.topAnchor, constant: -20).isActive = true
        
        view.addSubview(currentTimeLabel)
        currentTimeLabel.translatesAutoresizingMaskIntoConstraints = false
        currentTimeLabel.leftAnchor.constraint(equalTo: timeSlider.leftAnchor).isActive = true
        currentTimeLabel.bottomAnchor.constraint(equalTo: timeSlider.topAnchor, constant: -10).isActive = true
        
        view.addSubview(fullTimeLabel)
        fullTimeLabel.translatesAutoresizingMaskIntoConstraints = false
        fullTimeLabel.rightAnchor.constraint(equalTo: timeSlider.rightAnchor).isActive = true
        fullTimeLabel.bottomAnchor.constraint(equalTo: timeSlider.topAnchor, constant: -10).isActive = true
    }
    
    func configurePlayer() {
        guard let url = Bundle.main.url(forResource: "sound", withExtension: "mp3") else {
            print("Failed to load sound")
            return
        }
        let item = AVPlayerItem(url: url)
        player.replaceCurrentItem(with: item)
    }
}

ナビゲーションの実装


AVPlayerはseekと呼ばれる方法を提供する.ただし,この方法はCMTimeオブジェクトをパラメータとして受け入れる.
CMTimeオブジェクトを簡単に紹介し、時間を点数と見なします.分子が大きいほど時間の量が多くなり,分母が大きいほど時間の精度が高くなる.(これはCMのコアメディア、すなわちAVFoundationの上部にあるメディアに関するモジュールです.)
既定では、スライダの位置は0~1の数字で表されます.スライダの値を使用してタイムゾーンを計算できます.AVPlayerItemの再生時間の合計長はduration属性によって決定される.CMTimeとして表示され、秒と呼ばれる2つの値に変換することで計算できます.
前回計算したDouble値をCMTimeに再変換してメソッドのパラメータとして渡せばよい!
@objc func seek(sender: UISlider) {
    // ✅ 현재 아이템을 가져온다 (없으면 함수 종료)
    guard let currentItem = player.currentItem else { return }

    // ✅ 슬라이더의 위치를 가져와서 이동할 시간으로 바꾼다.
    let position = Double(sender.value)
        // 👉 slide의 현재 위치 (0 ~ 1)를 Float -> Double
    let seconds = position * currentItem.duration.seconds
        // 👉 이동할 시간대를 계산
    let time = CMTime(seconds: seconds, preferredTimescale: 100)
        // 👉 CMTime 객체로 변경, 100은 소수점 아래 2째자리 까지만 쓰겠다는 뜻 (분모)

    // ✅ 해당 시간대로 이동
    player.seek(to: time)
}

🚫  CMTime init注意事項!


CMTimeには複数のイニシエータがあります.次の2つのinitializerの意味は異なり、よく見て使用する必要があります.
CMTime(value: 100, timescale: 100)
	//👉 100 / 100 즉 1초를 의미함.

CMTime(seconds: 100, preferredTimescale: 100)
	//👉 100초를 분모 100으로 나타낸다 즉 10000 / 100을 의미함.

リアルタイムで再生時間を更新


傍観者を取り付ける


AVPlayerオブジェクトは、モジュールを特定の時間に実行するために傍観者を追加することができる.時間間隔を最初のパラメータで受信します.
次に、どのDispatchQueueがエンクロージャを実行するかをパラメータ形式で受信します.UIに関連するタスクを行うので、ホームチーム列で実行しましょう.
最後のパラメータは、実行するモジュールです.ループ参照を防止するために,弱いselfを用いた.
func configureObserver() {
    let interval = CMTime(seconds: 1, preferredTimescale: 100)
    player.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main) { [weak self] _ in
        self?.updateTime()
    }
}

UI更新関数


オプティカル(光学式)ドライブのエンクロージャで動作するエンクロージャを実装します.現在の再生時間とAVPlayerItemの完全な時間を取得し、ラベル上のテキストにそれぞれ割り当てます.
最後のナビゲーションなしでスライダのvalueを更新します.(ナビゲーション中に更新すると、ナビゲーション中にスライダの値が変更されるため、ナビゲートできません.)
func updateTime() {
    let currentTime = self.player.currentItem?.currentTime().seconds ?? 0
    let totalTime = self.player.currentItem?.duration.seconds ?? 0
    
    self.currentTimeLabel.text = "\(Int(currentTime))"
    self.fullTimeLabel.text = "\(Int(totalTime))"
    
    if !isSeeking {
        self.timeSlider.value = Float(currentTime / totalTime)
    }
}

結果


また、現在の再生時間は1秒ごとに更新され、スライダでナビゲートできることもわかります.