【iOS】マナーモードでも音量固定で音を再生する

### 2020/08/13追記
マナーモード下で音を再生する方法について誤りがあったため、修正しました。

##概要
iOSアプリにおいて、
- デバイスがマナーモードの場合でも音を鳴らしたい
- その際、ユーザの音量設定に寄らず、固定の音量で鳴らしたい
場合があったので、実装方法を調べてみました。


##環境
- mac OS Catalina (10.15.4)
- Swift5
- Xcode11.4

##実装
下記リポジトリに実装サンプルを置いてあります。
https://github.com/r-kojima/FixedVolumeViewController

また、単純な音源の再生に関しては解説しません。
`AVAudioPlayer`などで検索をかけると実装サンプルがたくさん出てくると思います。

### マナーモード下で音を鳴らす
`AVAudioSession`インスタンスの`setCategory(_:)`関数で`.playback`を指定します。

```Swift
let audioSession = AVAudioSession.sharedInstance()
// マナーモードでも音を鳴らすようにする
try audioSession.setCategory(.playback)
```
これでマナーモードでも音が鳴るようになりました。

### デバイスの音量設定を一時的に操作する
デバイスの音量は、デバイスの物理ボタンまたは`MediaPlayer.framework`の`MPVolumeView`を用いて操作することができます。
この`MPVolumeView`は`UISlider`を備えていて、普通はスワイプ操作をして音量を調整しますが、
今回はアプリ側から音量をいじりたいので、タッチ不可かつ、ユーザから見えないように表示します。

```Swift
let volumeView = MPVolumeView(frame: .zero)

func setVolumeView() {
     volumeView.setVolumeThumbImage(UIImage(), for: UIControl.State())
     volumeView.isUserInteractionEnabled = false
     volumeView.alpha = 0.0001 // 限りなく透明に
     self.view.addSubview(volumeView)
}
```

音源を再生した後は、ユーザが元々設定していた音量に戻してあげたいので、
音量を保存する変数を宣言し、一時的にユーザの設定を避難してから、アプリ側から音量を設定します。
以下はそれを行う関数です。

```Swift
var savedVolume: Float = 0

// 現在の音量を保存
func saveVolume() {
     savedVolume = audioSession.outputVolume
}

// デバイスの音量を書き換える
func setVolume(volumeLevel: Float) {
     guard let slider = volumeView.subviews.compactMap({ $0 as? UISlider }).first else {
         print("Slider Not Found")
         return
     }
                 
     slider.value = volumeLevel // 音量の設定
}
```

避難した音量は、音源の再生が終わったタイミングで戻します。
音源の再生が終わったことはデリゲートが知らせてくれます。

```Swift
 // MARK: - AVAudioPlayerDelegate プロトコルの実装
 extension FixedVolumeViewController: AVAudioPlayerDelegate {
     // 音源の再生が終わった時に呼ばれる
     func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
         self.setVolume(volumeLevel: self.savedVolume)
     }
 }
```

### 実装まとめ
ここまでの実装と、音源再生部分をまとめます。

```Swift
import UIKit
import AVFoundation
import MediaPlayer

class FixedVolumeViewController: UIViewController {
    let volumeView = MPVolumeView(frame: .zero)
    
    let audioSession = AVAudioSession.sharedInstance()

    var audioPlayer: AVAudioPlayer?
    
    var savedVolume: Float = 0
    
    // MARK: - Lifecycle Events
    override func viewDidLoad() {
        super.viewDidLoad()
        setVolumeView()
    }
    
    // MARK: - User Actions
    @IBAction func onTouchPlayButton(_ sender: UIButton) {
        self.playSound(forResource: "sample", ofType: "mp3", volume: 0.4)
    }
}

// MARK: - Audio Settings
extension FixedVolumeViewController {
    
    func setVolumeView() {
        volumeView.setVolumeThumbImage(UIImage(), for: UIControl.State())
        volumeView.isUserInteractionEnabled = false
        volumeView.alpha = 0.0001
        self.view.addSubview(volumeView)
    }
    
    // 現在の音量を保存
    func saveVolume() {
         savedVolume = audioSession.outputVolume
    }
    
    // デバイスの音量を書き換える
    func setVolume(volumeLevel: Float) {
        guard let slider = volumeView.subviews.compactMap({ $0 as? UISlider }).first else {
            print("Slider Not Found")
            return
        }
        slider.value = volumeLevel
    }
    
    // 音源の再生
    func playSound(forResource resource: String, ofType type: String = "mp3", volume: Float) {
        guard let path = Bundle.main.path(forResource: resource, ofType: type) else {
            print("File Not Found")
            return
        }
        
        // 音量の保存
        saveVolume()
        
        // 音量の上書き
        setVolume(volumeLevel: volume)
        
        do {
            // AVAudioPlayerのインスタンス化
            audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
            // デリゲートの設定
            audioPlayer!.delegate = self
            // マナーモードでも音を鳴らすようにする
            try audioSession.setCategory(.playback)
                
        } catch {
            print("Audio Setting Failed.")
            return
        }
        
        // 音量設定が間に合わない恐れがあるので、0.5秒後に音源を再生する
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.audioPlayer!.play() //再生
        }
    }
}

// MARK: - AVAudioPlayerDelegate プロトコルの実装
extension FixedVolumeViewController: AVAudioPlayerDelegate {
    // 音源の再生が終わった時に呼ばれる
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        // 音量を元に戻す
        self.setVolume(volumeLevel: savedVolume)
    }
}



```
##最後に
※[Human Interface Guidline](https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/audio/)には以下のように記載されています。
>Don’t repurpose audio controls. People expect audio controls to behave consistently in all apps. Never redefine the meaning of an audio control. If your app doesn’t support certain controls, then it simply shouldn’t respond to them.


ストアに公開する際は自己責任でお願いします。