只顯示部分高度的 sheet 畫面

開發 iOS App 時,有時我們會看到新頁面只顯示部分的高度,這樣的 UI 從前我們必須自己開發或使用套件,在 iOS 15 內建就有這樣的功能,利用present 顯示 controller 時我們可以控制新頁面只顯示部分的高度。

iOS 15 之前的類似套件

在 iOS 15 我們可以讓新頁面只顯示一半的高度,在 iOS 16 則更厲害,我們可以指定任何想要的高度。接下來讓我們以伊坂幸太郎小說瓢蟲改編的好看電影子彈列車為例說明吧。

storyboard 畫面設計

storyboard 的畫面如下,我們想要點選 button 後顯示電影子彈列車的介紹。

從 button 拉 segue 時選擇 Present Modally,因為使用 Present Modally 切換畫面才能指定新頁面的高度。

執行 App ,點選 button 顯示的電影畫面幾乎佔滿整個螢幕,上方露出一點點前一頁的畫面。

設定 detents,顯示一半高度的畫面

透過設定 iOS 15 sheetPresentationController 的 detents,我們可以讓 present 顯示的畫面只有一半的高度,以下我們利用 segue 拉出 IBSegueAction,然後在裡面設定新頁面的高度。

@IBSegueAction func showMovie(_ coder: NSCoder) -> UIViewController? {
let controller = UIViewController(coder: coder)
if let sheetPresentationController = controller?.sheetPresentationController {
sheetPresentationController.detents = [.medium()]
}
return controller
}

controller 的 property sheetPresentationController 負責控制用 present 顯示的畫面,它的型別是 UISheetPresentationController。從 sheetPresentationController 的 detents 可設定新畫面的高度,目前有 .large(),.medium() & .custom 3 種選擇。.custom 是 iOS 16 的新功能,我們待會再介紹。.large() 是我們剛剛看到的效果,代表幾乎佔滿整個螢幕的高度,.medium() 代表螢幕一半的高度。

啟動 App 點選 button 後,電影介紹的畫面果然只有一半的高度。

剛剛的例子我們在 IBSegueAction 設定 detents,若是頁面的切換是直接從程式呼叫 present,則可參考以下寫法。

let movieViewController = MovieViewController()
if let sheetPresentationController = movieViewController.sheetPresentationController {
sheetPresentationController.detents = [.medium()]
}
present(movieViewController, animated: true)

設定 detents,指定新畫面為特定的高度

利用 iOS 16 的 .custom,我們可指定新畫面為特定的高度。以下程式將讓畫面的高度為 300。

@IBSegueAction func showMovie(_ coder: NSCoder) -> UIViewController? {
let controller = UIViewController(coder: coder)
if let sheetPresentationController = controller?.sheetPresentationController {
sheetPresentationController.detents = [.custom(resolver: { context in
300
})]
}
return controller
}

我們也可以指定比例,以下程式的 context.maximumDetentValue * 0.8 表示畫面的高度為最大高度的 80%。

@IBSegueAction func showMovie(_ coder: NSCoder) -> UIViewController? {
let controller = UIViewController(coder: coder)
if let sheetPresentationController = controller?.sheetPresentationController {
sheetPresentationController.detents = [.custom(resolver: { context in
context.maximumDetentValue * 0.8
})]
}
return controller
}

關閉畫面

關閉剛剛以 present 顯示的畫面,有以下兩種方法。

  • 方法1: 點選上方的空白區塊。
  • 方法2: 往下拖曳。

多種高度的 detents

detents 的型別是 array,因此我們可設定多種高度,讓使用者拖曳切換不同的高度。以下程式設定 detents 為 200,medium & large,因此電影畫面一開始顯示的高度為 200,使用者可往上拖曳,讓畫面變成一半的高度,然後再往上拖曳將變成幾乎佔滿整個螢幕。

@IBSegueAction func showMovie(_ coder: NSCoder) -> UIViewController? {
let controller = UIViewController(coder: coder)
if let sheetPresentationController = controller?.sheetPresentationController {
sheetPresentationController.detents = [
.custom(resolver: { context in
200
}), .medium(), .large()
]
}
return controller
}

顯示一半時仍可操作前一頁的畫面

通常我們 present 新的畫面時前一頁是不能操作的,如下圖所示,上方被透明的黑色區塊覆蓋,提醒使用者此區塊不能操作,因此綠色的開關無法點選。

拿掉黑色區塊,讓前一頁可以操作也是做得到的,只要設定 largestUndimmedDetentIdentifier。

@IBSegueAction func showMovie(_ coder: NSCoder) -> UIViewController? {
let controller = UIViewController(coder: coder)
if let sheetPresentationController = controller?.sheetPresentationController {
sheetPresentationController.largestUndimmedDetentIdentifier = .medium
sheetPresentationController.detents = [
.medium(),
.custom(resolver: { context in
context.maximumDetentValue * 0.8
}), .large()
]
}
return controller
}

以上程式設定 largestUndimmedDetentIdentifier = .medium,因此只有畫面高度超過 medium 時才會加上黑色區塊。

當畫面在一半的高度時,沒有加上黑色區塊,因此背後的開關可以點選。

當畫面在 80% 的高度時,加了黑色區塊,因此背後的開關不能點選。

設定頁面的圓角弧度

設定 preferredCornerRadius。

sheetPresentationController .preferredCornerRadius = 100

顯示下拉的灰色長條在頂部 & IG 新增 po 文列表

UISheetPresentationController 的 prefersGrabberVisible 控制新頁面出現時上方是否顯示提醒使用者下拉的灰色長條,以下例子模仿 IG 的新增 po 文列表。

新增 po 文列表以 Static Cells 實現,navigation controller 的 storyboard id 為 createNavigationController
@IBAction func add(_ sender: Any) {

if let createNavigationController = storyboard?.instantiateViewController(withIdentifier: "createNavigationController"),
let sheetPresentationController = createNavigationController.sheetPresentationController {
sheetPresentationController.detents = [.medium()]
sheetPresentationController.prefersGrabberVisible = true
present(createNavigationController, animated: true, completion: nil)
}
}

讓一半高度裡的 scroll view 捲動的 prefersScrollingExpandsWhenScrolledToEdge

prefersScrollingExpandsWhenScrolledToEdge 可以幫我們解決一個特別的捲動問題,讓我們看看以下的例子。

@IBAction func add(_ sender: Any) {

if let createNavigationController = storyboard?.instantiateViewController(withIdentifier: "createNavigationController"),
let sheetPresentationController = createNavigationController.sheetPresentationController {
sheetPresentationController.detents = [.medium(), .large()]
sheetPresentationController.prefersGrabberVisible = true
present(createNavigationController, animated: true, completion: nil)

}
}

我們將 detents 設為 medium & large,因此新頁面可以一半高度,也可以全螢幕。如下圖所示,新頁面是個有很多 cell 的表格,它一開始顯示一半的高度,當我們向上捲動時,它將自動展開成全螢幕。

有沒有方法能讓它在一半高度向上捲動時,捲動頁面裡的內容,而不是展開成全螢幕呢 ? 答案就在 prefersScrollingExpandsWhenScrolledToEdge。只要將它設為 false 即可。此時若要變成全螢幕,則要從新頁面的頂部向上拖曳。

sheetPresentationController.prefersScrollingExpandsWhenScrolledToEdge = false

參考連結

Apple 官方還有介紹一些特別的用法和範例,比方 prefersScrollingExpandsWhenScrolledToEdge & adaptiveSheetPresentationController,有興趣的朋友可進一步參考以下連結。

官方範例: Customize and Resize Sheets in UIKit

WWDC

UISheetPresentationController

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com