[SwiftUI][AVKit] AirPlay の再生先を選ぶ

SwiftUI2021

     
自アプリ内で、AirPlay の再生先を選べるようにする方法を説明します

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterey 13 Beta5
  • Xcode 14.0 Beta5
  • iOS 16.0 beta

AVRoutePickerView

AirPlay の再生先を選ぶことができるような View が UIKit/AppKit 向けにはすでに AVRoutePickerView として用意されています。
Apple のドキュメントは、こちら

ですが、SwiftUI 向けには用意されていないので、UIViewRepresentable/NSViewRepresentable を使用して wrap する必要があります。

UIViewRepresentable/NSViewRepresentable

使用する UIView/NSView

AVRouterPickerView です。UIKit/AppKit でそれぞれに用意されていますので、同じクラス名を使用することができます。

delegate も、同じ AVRoutePickerViewDelegate ですので、特に UIKit/ AppKit それぞれに作成する必要はありません。 今回作成したものは、Coordinator を AVRoutePickerViewDelegate 準拠にしています。

当初、Delegate で選択先の出力デバイスがわかると思っていたのですが、別途取得しなければいけないことがわかったので、Delegate 内ではなにもしていません。

出力デバイスが変更されると、NotificationCenter から通知が発行されるので、その通知を受けて データを更新するクラスを別途作成しています。

実装

AirPlayRouterPicker という名称で作成しました。

# UIKit/AppKit 共に、使用するクラスは、AVRouterPickerView なので、コードの共有が容易だと思っていたのですが、結局 #if #elseif が多用してしまっています。

多用してしまっているのは、以下の点が理由になっています。
・UIViewRepresentable/NSViewRepresentable は同じような構造を持つが、要求されるメソッド名が異なる。(例:UIViewRepresetable は makeUIView、NSViewRepresentable は、makeNSView というメソッドを要求する。)
・AVRouterPickerView の持っているプロパティが、OSによって 少し異なる。(例:prioritizesVideoDevices は、UIKit 版にのみ 存在する)


//
//  AirPlayRoutePicker.swift
//
//  Created by : Tomoaki Yagishita on 2022/08/18
//  © 2022  SmallDeskSoftware
//

import Foundation
import Combine
import AVKit
import SwiftUI

#if os(iOS)
typealias NSUIViewRepresentable = UIViewRepresentable
#elseif os(macOS)
typealias NSUIViewRepresentable = NSViewRepresentable
#else
#warning("unsupported platform")
#endif


struct AirPlayRoutePicker: NSUIViewRepresentable {
    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }

#if os(iOS)
    func makeUIView(context: Context) -> AVRoutePickerView {
        let view = AVRoutePickerView()
        view.prioritizesVideoDevices = false // work only for iOS
        view.delegate = context.coordinator
        return view
    }
#elseif os(macOS)
    func makeNSView(context: Context) -> AVRoutePickerView {
        let view = AVRoutePickerView()
        view.delegate = context.coordinator
        return view
    }
#endif

#if os(iOS)
    func updateUIView(_ uiView: AVRoutePickerView, context: Context) {
        // do something iff needed
    }
#elseif os(macOS)
    func updateNSView(_ nsView: AVRoutePickerView, context: Context) {
        // do something iff needed
    }
#endif

    class Coordinator: NSObject, AVRoutePickerViewDelegate {
        override init() {super.init()}
        func routePickerViewWillBeginPresentingRoutes(_ routePickerView: AVRoutePickerView) {
            // do something iff needed
        }
        func routePickerViewDidEndPresentingRoutes(_ routePickerView: AVRoutePickerView) {
            // do something iff needed
        }
    }
}

出力デバイス情報の取得

AVRoutePickerView(UIKit/AppKit)/AirPlayRoutePicker(上記) は、デバイスを切り替えるUIを表示して、切り替えることはできるのですが、切り替えた先のデバイス情報を取得するには、別途 AVAudioSession を調べる必要があります。

以下の CurrentRoutePortNames を作成し、NotificationCenter からの通知により自動でアップデートされるクラスを作成しました。ObservableObject に準拠させて、名称を @Published 指定しているので、変更に準じて、UI も更新されます。


import Foundation
import Combine
import AVKit
import SwiftUI

class CurrentRoutePortNames: ObservableObject {
    @Published var deviceNames: [String] = []
    var cancelables:[AnyCancellable] = []

    init() {
        NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification)
            .sink { notification in
                self.updateDeviceName()
            }
            .store(in: &cancelables)
        updateDeviceName()
    }
    
    public func updateDeviceName() {
        let currentRoute = AVAudioSession.sharedInstance().currentRoute
        deviceNames = currentRoute.outputs.compactMap({$0.portName})
    }
    
}

出力デバイスのポート名を記憶していますが、出力先情報は、AVAudioSessionPortDescription 型なので、必要に応じて、保持する情報を追加削除して使うことを想定しています。
Apple のドキュメントは、こちら

サンプルアプリ

画面上に、AirPlay マークが表示され、選択されたデバイスが表示される”だけ”のアプリです。

SpeakerSelected
SelectionInAirPlay
AirMacSelected

//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2022/08/18
//  © 2022  SmallDeskSoftware
//

import SwiftUI
import AVKit



struct ContentView: View {
    @State private var selection: String = ""
    @StateObject var portNames = CurrentRoutePortNames()
    var body: some View {
        VStack {
            AirPlayRoutePicker()
                .frame(width: 50, height: 50)
            
            Text("selection: \(portNames.deviceNames.first ?? "no device")")
        }
        .padding()
        .onAppear{
            portNames.updateDeviceName()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

まとめ

AirPlay 出力先選択を簡単にできる View が用意されている

AirPlay 出力先選択を簡単にできる View
  • UIKit/AppKit では、AVRoutePickerView を使う
  • SwiftUI からは、UIViewRepresentable/NSViewRepresentable で wrap して使用する
  • 選択された出力先は、AVAudioSession から取得する
  • 出力先が変更されたときは、NotificationCenter から通知される

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMastery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle"という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

コメントを残す

メールアドレスが公開されることはありません。