はじめに
iOS 15 からついに標準で半モーダルが実装できるようになりました!
そもそも半モーダルてなんなんだ?という気持ちですが下記記事より抜粋。
iOSでの半モーダル/ハーフモーダルの実装についてのまとめ
・モードの完全遷移が起こらない
・モードを多重化しながらパラレルに対話可能
・モードを終了させずにモードからの一時退避が可能
・擬似的なマルチウインドウ・インターフェイスに応用可能
・スワイプなどインタラクションコストの低い操作方法によってモードの切り替えが可能
ということで半モーダルぽいものの実装方法について4つ紹介します。
こんな感じです。
ひさびさに記事書こうと思ったけど実装して力尽きた🙃
半モーダルぽいなにかの実装4パターン✌️✌️ pic.twitter.com/0FNm7g6j1q— am10 (@am103141592) November 15, 2021
sheetPresentationControllerを使う
iOS 15 から使えるようになった sheetPresentationController を使う方法です。
実装方法は ViewController の sheetPresentationController.detents
に設定してやるだけ。
1 2 3 4 5 |
let viewController = HogeViewController() if let sheet = viewController.sheetPresentationController { sheet.detents = [.medium(), .large()] } present(viewController, animated: true) |
UIModalPresentationStyle
は pageSheet
か formSheet
である必要があるみたいです。
使えるならこれが一番いいように思います。残念ながら iOS 15 以上でしか使えないので iOS 14 などもターゲットであれば他の方法を模索する必要があります。
inputViewを使う
半モーダルと言えるのか?とも思いますが inputView
を使った方法です(入力モードと考えるなら半モーダルなのかも??)。
実装方法は下記。CustomKeyboardView
は xib で作ってます。任意の画面のボタンのクラスに SelectButton
を設定するとボタン押下で表示されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
final class CustomKeyboardView: UIView { override init(frame: CGRect) { super.init(frame: frame) loadNib() } required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder)! loadNib() } private func loadNib() { let view = Bundle.main.loadNibNamed("CustomKeyboardView", owner: self, options: nil)?.first as! UIView view.frame = bounds view.translatesAutoresizingMaskIntoConstraints = true view.autoresizingMask = [.flexibleWidth, .flexibleHeight] addSubview(view) } } final class SelectButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } private func commonInit() { addTarget(self, action: #selector(toggleSelection(_:)), for: .touchUpInside) } override var canBecomeFirstResponder: Bool { return true } @discardableResult override func becomeFirstResponder() -> Bool { let value = super.becomeFirstResponder() isSelected = isFirstResponder return value } @discardableResult override func resignFirstResponder() -> Bool { let value = super.resignFirstResponder() isSelected = isFirstResponder return value } override var inputView: UIView? { let view = CustomKeyboardView() var frame = view.frame frame.size.height = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height view.frame = frame return view } override var inputAccessoryView: UIView? { let toolbar = UIToolbar() toolbar.items = [ UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil), UIBarButtonItem.init(title: "閉じる", style: .done, target: self, action: #selector(close(_:))) ] toolbar.sizeToFit() return toolbar } @objc private func close(_ sender: Any?) { resignFirstResponder() } @objc private func toggleSelection(_ sender: Any?) { isSelected.toggle() if isSelected { becomeFirstResponder() } else { resignFirstResponder() } } } |
OS のバージョンを気にする必要はありませんが inputView
のカスタマイズなので使える場面は限られるかもしれません(あとセーフエリア対応とかめんどくさい)。
作ってから思いましたが inputView
の上に UISearchBar
のっけてもキーボード表示できないので使えないですね。。。
UIActivityViewControllerの魔改造
下記でみかけた UIActivityViewController
を魔改造する方法です。
Recreate iOS 13' share sheet modal in swift (not the share sheet itself, but the way it's presented)
実装方法は下記。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
final class CustomActivityViewController: UIActivityViewController { private let controller: UIViewController! required init(controller: UIViewController) { self.controller = controller super.init(activityItems: [], applicationActivities: nil) } override func viewDidLoad() { super.viewDidLoad() view.subviews.forEach { $0.removeFromSuperview() } addChild(controller) view.addSubview(controller.view) } } // 任意のVCで表示 let controller = HogeViewController() let activityViewController = CustomActivityViewController(controller: controller) present(activityViewController, animated: true) |
こちらも iOS 15 未満でも動きますが OS のバージョンアップで動かなくなる可能性もあるのでおすすめはしません(面白そうだったので取り上げました)。
ライブラリ(FloatingPanel)を使う
SCENEE/FloatingPanelを使う方法です。今までだとこちらの方法が半モーダル実装のスタンダードだったのではないでしょうか。
実装方法(SPM でいれました)。
1 2 3 4 5 6 7 8 |
import FloatingPanel // 任意のVCで表示 let fpc = FloatingPanelController() let controller = HogeViewController() fpc.set(contentViewController: controller) fpc.isRemovalInteractionEnabled = true present(fpc, animated: true) |
FloatingPanelLayout
を設定することで高さ調整もできます。
Swift Package Manager にも対応していますし最近もメンテされているようなので iOS 15 以降でも利用できそうです。
おわりに
そういえば公式で半モーダルができるようになったなぁというのを思い出しました。似たようなことなら他にも色々方法がありそうだなということで4つ提案してみました。
公式が使えるなら公式が一番!
参考
- Customize and resize sheets in UIKit
- 公式「ハーフモーダル」がやってきた! #wwdc21
- Swift: iOS15から使える新しいハーフモーダル
- iOSでの半モーダル/ハーフモーダルの実装についてのまとめ
- iOSにおける半モーダルビューの解釈
- Recreate iOS 13' share sheet modal in swift (not the share sheet itself, but the way it's presented)
コメント