Help us understand the problem. What is going on with this article?

UIViewの背景に動画を流す簡単な方法

More than 1 year has passed since last update.

iOSで背景に動画を流す簡単な方法

初回起動画面などで背景に動画が流れているアプリってよくありますよね。
そのようにボタンなどのUIの後ろで動画を流す簡単な方法を紹介します。

最小実装

import UIKit
import AVFoundation

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // Bundle Resourcesからsample.mp4を読み込んで再生
        let path = Bundle.main.path(forResource: "sample", ofType: "mp4")!
        let player = AVPlayer(url: URL(fileURLWithPath: path))
        player.play()


        // AVPlayer用のLayerを生成
        let playerLayer = AVPlayerLayer(player: player)
        playerLayer.frame = view.bounds
        playerLayer.videoGravity = .resizeAspectFill
        playerLayer.zPosition = -1 // ボタン等よりも後ろに表示
        view.layer.insertSublayer(playerLayer, at: 0) // 動画をレイヤーとして追加
    }
}

demo

短い行で実現できましたね。

試しにラベルとボタンを置いてみます。

let label = UILabel(frame: CGRect(x: 0, y: 100, width: 200, height: 40))
label.text = "Hello!"
label.textColor = .white
label.font = UIFont.boldSystemFont(ofSize: 50)
label.textAlignment = .center
label.center.x = view.center.x
label.autoresizingMask = [.flexibleWidth, .flexibleBottomMargin]
view.addSubview(label)

let loginButton = UIButton(frame: .init(x: 30, y: view.frame.height - 150, width: view.frame.width - 60, height: 50))
loginButton.setTitle("LOG IN", for: .normal)
loginButton.setTitleColor(.white, for: .normal)
loginButton.layer.borderWidth = 1
loginButton.layer.borderColor = UIColor.white.cgColor
loginButton.layer.cornerRadius = 4
loginButton.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
view.addSubview(loginButton)

let signupButton = UIButton(frame: loginButton.frame)
signupButton.frame.origin.y = loginButton.frame.minY - 60
signupButton.setTitle("SIGN UP", for: .normal)
signupButton.setTitleColor(.white, for: .normal)
signupButton.backgroundColor = UIColor(red: 0, green: 168.0/255, blue: 107.0/255, alpha: 1)
signupButton.layer.cornerRadius = 4
signupButton.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
view.addSubview(signupButton)

demo

はい、よく見る画面になりましたね。

これだけでも十分ですが、さらによくある以下の実装を追加してみます。

  • 動画を少し薄暗くする
  • リピート再生する
  • アプリをバックグラウンドにした後、フォアグラウンドにしても自動再生される (デフォルトでは停止してしまいます)
  • 端末を回転してもサイズが調整される

動画を薄暗くする & リピート再生 & アプリ復帰後自動再生 & 端末回転対応

class ViewController: UIViewController {

    private var observers: (player: NSObjectProtocol, 
                            willEnterForeground: NSObjectProtocol, 
                            bounds: NSKeyValueObservation)?

    override func viewDidLoad() {
        super.viewDidLoad()

        let path = Bundle.main.path(forResource: "sample", ofType: "mp4")!
        let player = AVPlayer(url: URL(fileURLWithPath: path))
        player.actionAtItemEnd = .none
        player.play()

        let playerLayer = AVPlayerLayer(player: player)
        playerLayer.frame = view.bounds
        playerLayer.videoGravity = .resizeAspectFill
        playerLayer.zPosition = -2 // 次に追加するoverlayより後ろにする
        view.layer.insertSublayer(playerLayer, at: 0)

        // 動画の上に重ねる半透明の黒いレイヤー
        let dimOverlay = CALayer()
        dimOverlay.frame = view.bounds
        dimOverlay.backgroundColor = UIColor.black.cgColor
        dimOverlay.zPosition = -1
        dimOverlay.opacity = 0.4 // 不透明度
        view.layer.insertSublayer(dimOverlay, at: 0)

        // 最後まで再生したら最初から再生する
        let playerObserver = NotificationCenter.default.addObserver(
            forName: .AVPlayerItemDidPlayToEndTime,
            object: player.currentItem,
            queue: .main) { [weak playerLayer] _ in
                playerLayer?.player?.seek(to: kCMTimeZero)
                playerLayer?.player?.play()
        }

        // アプリがバックグラウンドから戻ってきた時に再生する
        let willEnterForegroundObserver = NotificationCenter.default.addObserver(
            forName: .UIApplicationWillEnterForeground,
            object: nil,
            queue: .main) { [weak playerLayer] _ in
                playerLayer?.player?.play()
        }

        // 端末が回転した時に動画レイヤーのサイズを調整する
        let boundsObserver = view.layer.observe(\.bounds) { [weak playerLayer, weak dimOverlay] view, _ in
            DispatchQueue.main.async {
                playerLayer?.frame = view.bounds
                dimOverlay?.frame = view.bounds
            }
        }

        observers = (playerObserver, willEnterForegroundObserver, boundsObserver)
    }

    deinit {
        // 画面が破棄された時に監視をやめる
        if let observers = observers {
            NotificationCenter.default.removeObserver(observers.player)
            NotificationCenter.default.removeObserver(observers.willEnterForeground)
            observers.bounds.invalidate()
        }
    }
}

demo

※デモ動画ではボタンとラベルのコードも追加しています。

まとめ

意外と簡単に実現することが出来ました。
細かい制御を入れると少しコード量が増えるので、実際にはUIViewController内に記述せず、別のクラスに実装するのが良いと思います。

サンプルコードはこちらに置いてあります。
https://github.com/tattn/BackgroundVideoSample

関連URL

デモに使用した動画
http://mazwai.com/#/videos/218

tattn
Yahoo! JAPANで乗換案内アプリの開発や社内のアプリの課題解決、新規技術の導入サポートなどをしています。 https://zenn.dev/tattn
https://twitter.com/tanakasan2525
yahoo-japan-corp
Yahoo! JAPAN を運営しています。
https://www.yahoo.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away