AppleMusicのような、TabBarにDockされたツールバーを構築する


AppleMusicのようなTabBarにDockされたツールバーを構築する

AppleMusicのiOSアプリには、TabBarの上に再生中の音楽を示したツールバーが表示されます。
今回はタブを切り替えたり、UINavigationControllerでpush遷移をしても単一のインスタンスを使用して表示ができるような実装をしてみます。

完成イメージ

実装概要

UITabBarControllerを継承したTabBarController側にPlayer用のUIViewをレイアウトし、公開させておきます。

UITabBarControllerに埋め込まれたUIViewControllerからはUITabBarControllerが取得できるので、それを経由してPlayerインスタンスにアクセスし、
レイアウトに使用することができます。

実装を簡易化するため、TabBarVisible protocolを作成し、protocol extensionでplayerを取得できるようにします。

注意点としてはtabBarControllerはviewDidLoad時点では取得することができないので、updateViewConstraints等でレイアウトをする必要があります。

実装

import UIKit

final class TabBarController: UITabBarController {
    enum Const {
        static let playerHeight: CGFloat = 64
    }

    let player: UILabel = {
        let label = UILabel()
        label.text = "This is Music Player"
        label.font = .boldSystemFont(ofSize: 18)
        label.backgroundColor = UIColor.purple
        return label
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        player.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(player)
        NSLayoutConstraint.activate([
            player.heightAnchor.constraint(equalToConstant: Const.playerHeight),
            player.leftAnchor.constraint(equalTo: view.leftAnchor),
            player.rightAnchor.constraint(equalTo: view.rightAnchor),
            player.bottomAnchor.constraint(equalTo: tabBar.topAnchor)
        ])

        viewControllers = [
            TopViewController(title: "Page1", color: .green),
            TopViewController(title: "Page2", color: .yellow)
        ]
    }
}

import UIKit

protocol TabBarVisible {}

extension TabBarVisible where Self: UIViewController {
    var player: UIView {
        guard let tabBarController = tabBarController as? TabBarController else {
            fatalError("Must to be embbeded in UITabBarController")
        }
        return tabBarController.player
    }
}
import UIKit

final class TopViewController: UIViewController, TabBarVisible {
    private let label: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.minimumScaleFactor = 0.1
        label.adjustsFontSizeToFitWidth = true
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    init(title: String, color: UIColor) {
        super.init(nibName: nil, bundle: nil)
        self.title = title
        label.text = Array(repeating: title, count: 100).joined()
        view.backgroundColor = color
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(label)
    }

    override func updateViewConstraints() {
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: view.topAnchor),
            label.rightAnchor.constraint(equalTo: view.rightAnchor),
            label.leftAnchor.constraint(equalTo: view.leftAnchor),
            label.bottomAnchor.constraint(equalTo: player.topAnchor)
        ])

        super.updateViewConstraints()
    }
}