その辺にいるITインフラ屋さん

気が向いた時に適当な記事を書いたりする個人の備忘録です。個人で開発しているアプリの事やインフラについてなどの記事を書いたりすると思います。更新頻度は少なめ

FloatingPanel を利用したセミモーダルビュー を作る

MoveLogというiOSアプリで、複数の位置情報が保存されている場合に、保存したデータ毎の移動時間と距離を表示出来るようにしました。移動履歴だとピンをタップした時にしか距離と時間を表示する事が出来なかったのですが、セミモーダルビューを表示する事で一覧として見れるようにしました。

f:id:mayuki123:20200622223537g:plain

この表示方法が半モーダルビューとかハーフモーダルビューとかどれが正式名称なのかが未だに分かっていないのですが、この記事ではセミモーダルビューとします。個人的にはひょっこりビューとよんでいます。今回はFloatingPanelを利用して実装をしましたので、利用方法を備忘録として残します。



FloatingPanelについて

セミモーダルビューはSwiftの標準ライブラリとしては用意されておりません。そのため、画面からアニメーションの処理を全て自前で設定する必要となりますが、本業でアプリを作っていない個人開発者にはとても荷が重いです。そのため、 FloatingPanel というOSSのライブラリを利用する事にしました。

github.com

利用方法

README にインストール手順が記載されています。

GitHub - SCENEE/FloatingPanel: A clean and easy-to-use floating panel UI component for iOS

私は Cocoa Pods を利用しているので、Podfile に下記の設定を追加しました。

pod 'FloatingPanel'

ViewControllerFloatingPanelControllerセミモーダルとして追加します。今回はCustomFloatingPanelControllerを作る事にしました。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let fpc = CustomFloatingPanelController()
        fpc.addPanel(toParent: self)
    }
}

下記のような CustomFloatingPanelController を作成します。surfaceView.cornerRadius を設定すればセミモーダルビューを角丸にする事が出来ます。set(contentViewController: xxxx) で実際に描画するViewControllerを指定します。

import UIKit
import FloatingPanel

class CustomFloatingPanelController: FloatingPanelController {
    init() {
        super.init()
        self.delegate = self
        self.isRemovalInteractionEnabled = true
        self.surfaceView.cornerRadius = 24.0

        // セミモーダルビュー上に表示するContent を設定する
        let content = UIViewController()
        content.view.backgroundColor = .cyan
        self.set(contentViewController: content)
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

セミモーダルビューのレイアウトについては、FloatingPanelLayout で設定する事が出来ます。今回はCustomFloatingPanelLayoutを作成しています。セミモーダルビューの表示は .full, .half, .tip, .hidden の4パターンの表示サイズが設定出来ます。

class CustomFloatingPanelLayout: FloatingPanelLayout {
    // セミモーダルビューの表示の際の初期ポジション
    public var initialPosition: FloatingPanelPosition {
        return .tip
    }
    // 対応するポジションの一覧
    public var supportedPositions: Set<FloatingPanelPosition> {
        return [.full, .half, .tip, .hidden]
    }
    // ポジション毎のサイズ設定
    public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
        switch position {
        case .full:
            return 50.0 // A top inset from safe area
        case .half:
            return 300.0 // A bottom inset from the safe area
        case .tip:
            return 100.0 // A bottom inset from the safe area
        default:
            return nil // Or `case .hidden: return nil`
        }
    }
}

FloatingPanelControllerDelegateセミモーダルビューのイベントを検知する事が出来ます。もし、セミモーダルビュー上にTableViewを利用する場合は、floatingPanelDidEndDragging のイベントが起きた時に Viewのサイズを可変にするようにしていないと、セルが最後まで表示されないといった事が発生するかと思います。FloatingPanelControllerに track 設定があるのでそれを使えばよいのかも?試してないですが…。

extension CustomFloatingPanelController: FloatingPanelControllerDelegate {
    func floatingPanel(_ vc: FloatingPanelController,
                       layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
        return CustomFloatingPanelLayout()
    }
    func floatingPanelDidEndDragging(_ vc: FloatingPanelController,
                                     withVelocity velocity: CGPoint,
                                     targetPosition: FloatingPanelPosition) {
        //targetPositionに応じて Viewのサイズを変更する

    }
}

最終的にはこんな感じとなります。比較的簡単にセミモーダルビューが実現出来ていいですね。

f:id:mayuki123:20200622233840g:plain

おわりに

MoveLogのようなマップを使うアプリは画面のほとんどを地図で埋め尽くす必要があるので、どういう風に画面表示をしようかとわりと悩みます。セミモーダルビューも独自実装となると作れる気がしませんでしたが、FloatingPanel を利用する事で簡単に実装する事が出来てよかったです。コード関連の記事はあまり書く事ないので、どこまで書くべきなのか難しいですね。苦手意識しかない