SwiftUI | Picture in Picture (PiP)の実装方法

音声・動画

SwiftUIでのPicture in Picture (PiP)の実装方法を説明する。

結論

以下の具体例の手順で実装する。

具体例

App概要

動画を視聴するをタップすると動画を開始する。

動画再生中にホーム画面に行くとPicture in Picture (PiP)で再生する。PC上のシミュレーターではPicture in Picture (PiP)できないので以下の画像は実機で実験した画像である。

作成方法

XcodeでTARGETS -> Signing & Capabilities -> Background Modes の Audio, AirPlay, and Picture in Pictureをチェックする。

XcodeでTARGETS -> Build Phases -> Copy Bundle Resources を開きmp4動画をドラッグ&ドロップする。

Copy items if needed、Create groupsをチェックしてfinish。

以下のコードを記述する。

import SwiftUI
import AVKit
import Combine

let my動画 = AVModel(title: "地球の動画",
                    url: Bundle.main.url(forResource: "地球の動画", 
                                         withExtension: "mp4")!)

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink("▶ 動画を視聴する", destination: 動画ビュー(動画: my動画))
        }
    }
}

struct 動画ビュー: View {
    @StateObject private var viewModel = AVViewModel()
    let 動画: AVModel
    
    var body: some View {
        AVView(avViewModel: viewModel)
            .onAppear {
                viewModel.avModel = 動画
                viewModel.player.play()
            }
    }
}

// View
struct AVView: UIViewControllerRepresentable {
    @ObservedObject var avViewModel: AVViewModel
    
    func makeUIViewController(context: Context) -> AVPlayerViewController {
        let vc = AVPlayerViewController()
        vc.player = avViewModel.player
        vc.canStartPictureInPictureAutomaticallyFromInline = true
        return vc
    }
    
    func updateUIViewController(_ uiViewController: AVPlayerViewController, 
                                context: Context) { }
    
}

// ViewModel
class AVViewModel: ObservableObject {
    @Published var avModel: AVModel?
    let player = AVPlayer()
    private var cancellable: AnyCancellable?
    
    init() {
        setAudioSessionCategoryToPlayback()
        cancellable = $avModel.sink(receiveValue: {
            guard let model = $0 else { return }
            self.player.replaceCurrentItem(with: AVPlayerItem(url: model.url))
        })
    }
    
    func setAudioSessionCategoryToPlayback() {
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(.playback)
        } catch {
           print("Setting category to AVAudioSessionCategoryPlayback failed.")
        }
    }
}

// Model
struct AVModel {
    let title: String
    let url: URL
}

解説

Model

  • 動画のタイトルとURLを入れる箱のstructを準備する。
  • このstructはMVVM (Model–view–viewmodel)アーキテクチャのModelを意識してAVModelと名付ける。
// Model
struct AVModel {
    let title: String
    let url: URL
}

ViewModel

  1. aVModelが変化する(aVModelの動画タイトルとURLが設定される)のを.sinkで受け取る。
  2. .sinkを受け取ったら、動画プレイヤーplayerが再生する対象をaVModelのurlに設定する。
  3. AudioSessionCategoryは.Playbackにしないといけないことになっているので設定するだけ。
  4. このclassはMVVM (Model–view–viewmodel)アーキテクチャのViewModelを意識してAVViewModelと名付ける。
// ViewModel
class AVViewModel: ObservableObject {                                           // ? 4
    @Published var avModel: AVModel?
    let player = AVPlayer()
    private var cancellable: AnyCancellable?
    
    init() {
        setAudioSessionCategoryToPlayback()                                     // ? 3
        cancellable = $avModel.sink(receiveValue: {                             // ? 1
            guard let model = $0 else { return }
            self.player.replaceCurrentItem(with: AVPlayerItem(url: model.url))  // ? 2
        })
    }
    
    func setAudioSessionCategoryToPlayback() {
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(.playback)
        } catch {
           print("Setting category to AVAudioSessionCategoryPlayback failed.")
        }
    }
}

View

  1. UIViewControllerRepresentable準拠のstructを準備する。
  2. canStartPictureInPictureAutomaticallyFromInline = true でPicture in Picture を有効にする。
  3. このstructはMVVM (Model–view–viewmodel)アーキテクチャのViewを意識してAVViewと名付ける。
// View
struct AVView: UIViewControllerRepresentable {                               // ? 1
    @ObservedObject var avViewModel: AVViewModel
    
    func makeUIViewController(context: Context) -> AVPlayerViewController {
        let vc = AVPlayerViewController()
        vc.player = avViewModel.player
        vc.canStartPictureInPictureAutomaticallyFromInline = true            // ? 2
        return vc
    }
    
    func updateUIViewController(_ uiViewController: AVPlayerViewController, 
                                context: Context) { }
    
}

最後にAVModel、AVViewModel、AVViewを使って動画ビューを構築する。

struct 動画ビュー: View {
    @StateObject private var viewModel = AVViewModel()
    let 動画: AVModel
    
    var body: some View {
        AVView(avViewModel: viewModel)
            .onAppear {
                viewModel.avModel = 動画
                viewModel.player.play()
            }
    }
}

この動画ビューをContentViewから見ることができるようにする。

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink("▶ 動画を視聴する", destination: 動画ビュー(動画: my動画))
        }
    }
}

動画はAppに入れておいたmp4ファイルを設定する。

let my動画 = AVModel(title: "地球の動画",
                    url: Bundle.main.url(forResource: "地球の動画", 
                                         withExtension: "mp4")!)

まとめ

SwiftUIでのPicture in Picture (PiP)の実装方法を説明した。

コメント

タイトルとURLをコピーしました