★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤微信公眾號:山青詠芝(shanqingyongzhi)
➤博客園地址:山青詠芝(https://www.cnblogs.com/strengthen/ )
➤GitHub地址:https://github.com/strengthen/LeetCode
➤原文地址: https://www.cnblogs.com/strengthen/p/15401882.html
➤如果鏈接不是山青詠芝的博客園地址,則可能是爬取作者的文章。
➤原文已修改更新!強烈建議點擊原文地址閱讀!支持作者!支持原創!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
「iPad 適配指南」 這個系列會介紹在 iPad 上的一些特殊能力,如何更好地適配 iPad,以及適配 iPad 時的一些注意點。
本文作為基礎篇,主要介紹 iPad 的轉屏分屏、模態,和 SplitVC 能力。
如何判斷 iPad 設備
如何判斷設備, iPad 的各種形態
if UIDevice.current.userInterfaceIdiom == .pad {}
復制代碼
在 M1 Mac 上運行的 iOS 應用取到的
userInterfaceIdiom
屬性為.pad
在 Mac Catalyst 上運行的應用取到的
userInterfaceIdiom
屬性為.mac
分屏適配篇
iPad 和 iPhone 最大的不同是,我們往往在 iPhone 上會限定 App 的方向恆定為 Portrait,但在 iPad 上,我們不僅要處理旋轉屏,還要處理各種分屏的情況。
分屏
iOS 上的分屏最早可以追溯到隨 iOS 10 推出的 SlideOver
、Split View
和畫中畫
功能。從 iOS 12 開始,應用分屏的概念和操作比較接近於現在的 iPadOS。
在 iPadOS 中,分屏下的應用主要有 8 種狀態:橫屏 1/3 屏
、橫屏 1/2 屏
、橫屏 2/3 屏
、橫屏全屏
、豎屏 1/3 屏
、豎屏 2/3 屏
、豎屏全屏
,以及懸浮窗
。
分屏可以通過多種操作喚起,最常見的是長按 Dock 中的圖標,然后拖動到屏幕的一側。
尺寸變化
UITraitCollection 是什么
View / VC 如何兼容大小的變化
viewWillTransition willTransition
無論是旋轉屏幕,還是分屏,我們都可以收斂到「尺寸變化」這個概念上一起處理。
在此之前,需要先介紹 UITraitCollection
的概念。
UITraitCollection 是什么
traitCollection
是UIView
,UIViewController
,UIWindow
,UIWindowScene
和UIScreen
等的屬性。
Transition
是指 vc 將會變化,變化的新屬性集合會在traitCollection
這個屬性集合中。traitCollection
屬性集合常用的屬性有:縱橫寬度的 sizeClass,是否是 darkMode 等屬性
除了UIWindowScene
是直接實現的屬性,其他列舉到的都是通過 UITraitEnvironment
協議來實現的:
public protocol UITraitEnvironment : NSObjectProtocol {
@available(iOS 8.0, *)
var traitCollection: UITraitCollection { get }
/** To be overridden as needed to provide custom behavior when the environment's traits change. */
@available(iOS 8.0, *)
func traitCollectionDidChange( _ previousTraitCollection: UITraitCollection?)
}
復制代碼
traitCollectionDidChange
一般會用在響應 iOS 界面環境的變化,對窗口大小變化的兼容會在接下來的一節中講到。
View / VC 兼容大小的變化
約束布局不必考慮尺寸的變化
- 對於 View,可以在
layoutSubviews
中進行 frame 布局或響應尺寸的變化。當窗口大小發生變化的時候,VC 會調用 View 的該方法。 - 對於 VC,有兩種策略:
- 在
viewWillLayoutSubviews
中進行布局 - 可以在以下兩個方法中進行布局的調整:
// UIViewController 實現了這個協議
public protocol UIContentContainer : NSObjectProtocol {
@available(iOS 8.0, *)
func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
@available(iOS 8.0, *)
func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator)
}
復制代碼
-
調用時機的區別在於:
- VC 出現和大小變化時都會調用
viewWillLayoutSubviews
和willTransition
- VC 出現時,如果不主動改變 view 大小,不會調用
viewWillTransition
,僅當 view 的大小變化時才會調用
- VC 出現和大小變化時都會調用
-
2 中的兩個函數的區別在於,窗口大小變化時:
-
willTransition
會先被調用。可以通過重寫該方法獲得即將變成的新 traitCollection- 注意:此時取 view/vc/window 的 traitCollection 仍為舊值
-
viewWillTransition
后被調用。可以通過重寫該方法獲得即將變成的新 size- 注意:此時取 view/vc/window 的 traitCollection 和 bounds.size 仍為舊值
- 最佳實踐:如果需要在 viewWillTransition 中獲取即將變成的新 traitCollection,可以考慮在 vc 持有一個
lastTraitCollection
,並且在willTransition
時更新其值。
-
這三種方法不僅僅會在上述情形中被調用。
App 在 iPad 退出后台或鎖屏時,因為要生成橫屏和豎屏的截圖以便在 App Switcher 中顯示,都會被多次調用。
詳見后文「鎖屏/退到后台時在 iPad 上的特殊情況」
UIScreen 的使用
大家可能早已習慣直接使用 UIScreen.main.bounds
。這在過去的一台設備只有唯一屏幕、一個屏幕只有唯一應用
情況下是沒有問題的。但事情正在發生改變:在 iPadOS 上,一個屏幕已經能顯示多個應用了,在 Apple Silicon Mac 上,一個設備也能有多個顯示內容不一樣的屏幕,應用並不一定會在 UIScreen.main
上顯示。
我們應該遵循的原則是:在每個 UIView 中,獲取自身的 bounds 屬性,或者利用元素間的相對關系 Auto Layout 進行布局。應該盡量避免獲取設備本身的寬高來進行布局。
SizeClass 介紹
介紹 sizeClass 概念,以及各種 iOS 窗口尺寸對應的 CR 值
概念
日常我們所說的Size Class,是UITraitCollection
中的兩個屬性:
@available(iOS 8.0, *)
open class UITraitCollection : NSObject, NSCopying, NSSecureCoding {
/// 水平 size class,最常用
open var horizontalSizeClass: UIUserInterfaceSizeClass { get }
/// 豎直 size class,用的少
open var verticalSizeClass: UIUserInterfaceSizeClass { get }
}
復制代碼
Size Class
將界面寬度分成了 Compact
和 Regular
兩種類型。
@available(iOS 8.0, *)
public enum UIUserInterfaceSizeClass : Int {
/// 未指定
case unspecified = 0
/// 緊致
case compact = 1
/// 正常(寬松)
case regular = 2
}
復制代碼
對於每個 View / VC / Window / WindowScene / Screen,都有 size class 的概念。
Size Class
對我們最重要的意義是:
響應式布局最重要的即是
斷點
。所謂斷點,就是一個分界線,在這個分界線的兩邊,我們會采取不同的布局策略。而Size Class
給我們提供了關於斷點
的指導。
系統水平方向 Size Class 規則
- 目前在 iPhone 豎屏時,
horizontalSizeClass
都是Compact
,其他情況比較復雜,參考官方文檔,不展開贅述; - 在 iPad 上,
全屏
和橫屏2/3分屏
都是Regular
; 橫屏1/2分屏
時,只有 12.9 寸的 iPad 是Regular
;- 除此之外的其他情況都是
Compact
。
詳見官方文檔:Size Classes - HIG
布局控件篇
模態控件
介紹 modalPresentationStyle 各種樣式的效果 以及着重介紹一下 popover 的概念
在 iPad 上,我們經常看到這樣的頁面。看起來兩者差異很大,似乎需要做很多的適配,但其實代碼很簡單,我們只需要兩行代碼,就能同時完成在 iPhone 上和 iPad 上的適配:
vc.modalPresentationStyle = .formSheet
self.present(vc, animated: true)
復制代碼
這里涉及到了 modalPresentationStyle 的概念。
我們知道,一個 VC 可以被 push,也可以被 present。
兩者在用法上的區別是,present 的頁面會阻擋用戶的其他操作,使其專注在當前頁面上。
Sheet
在 iPad 上有兩種種最常見的樣式:.formSheet
和.pageSheet
,這三種都是 present 前可以設置給 VC 的樣式。
在 iPhone 上,兩種 Sheet 的樣式沒有什么分別:
在 iPad 上 formSheet 和 pageSheet 的區別是:
pageSheet
的浮窗大小是系統根據系統字體大小確定的,不能修改大小formSheet
和接下來要提到的popover
的大小,都可以通過 vc 的preferredContentSize
來指定實際大小。
![]() |
![]() |
---|---|
pageSheet適合信息密度較高、閱讀寫作 | formSheet默認大小,適合信息密度較低或自定義大小的場景 |
iOS 13 對 formSheet 的窄屏樣式從 fullScreen 變成了現在的層疊卡片樣式
對於 formSheet
和 pageSheet
,在 iPad 上有手勢下滑返回的自帶功能。
如果希望介入手勢下滑事件,可在 UIAdaptivePresentationControllerDelegate 中進行處理。
Popover 氣泡
Popover 是 iPad 上非常常見的一種交互元素。
前面我們介紹到的 modalPresentationStyle
,還有一種取值即為 .popover
但與前面幾種我們提到的 Style 不同的是,除了簡單的指定 modalPresentationStyle
之外,我們還需要設置幾個屬性:
// 指定樣式
pushvc.modalPresentationStyle = .popover
// 指定 Popover 指向的矩形
pushvc.popoverPresentationController?.sourceRect = btn.frame
// 指定 Popover 指向的 View,必須指定,否則會崩潰
pushvc.popoverPresentationController?.sourceView = self.view
// 指定 Popover 允許的箭頭朝向
pushvc.popoverPresentationController?.permittedArrowDirections = .up
self.present(pushvc, animated: true)
復制代碼
modalPresentationStyle
我們以 iPad Pro 11-inch, iOS 14, SplitVC detailVC(yellow) present(purple 40% 透明度)VC 的 case 為例,簡單介紹一下所有 modalPresentationStyle 的取值區別:
橫屏全屏 | ![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
---|---|---|---|---|---|---|
豎屏全屏 | ![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
窄屏&iPhone | ![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
類型 | fullScreen | pageSheet | formSheet | currentContext | overFullScreen | overCurrentContext |
大小特點 | 覆蓋全屏 | 更大尺寸的模態 | 可自定義大小的模態,默認大小如圖 | 只覆蓋當前區域 | 覆蓋全屏 | 只覆蓋當前區域 |
當然,系統也提供了 custom 樣式,以提供自定義動畫和樣式的能力。
over*
與*
的區別是:
over*
不會將覆蓋的視圖從視圖層級撤下
iOS 15 | Customize and resize sheets in UIKit
Video: Customize and resize sheets in UIKit - WWDC 2021 - Videos - Apple Developer
在 iOS 15 中,Sheets 又有了一些新能力:
我們可以更精細化地控制 Sheets 的垂直高度了,比如創建一個半屏 Sheet,或者讓 Sheet 可以在半屏高度停靠(Dedents):
我們可以移除 Sheets 下的陰影遮罩,讓我們可以在展示 Sheet 的時候與下層 View 交互;
或者在 Compact 屏幕下展示非全屏 Sheet
所有的新特性都可以通過新 API:UISheetPresentationController 來進行行為的控制。
當 VC 的 modalPresentationStyle
為 formSheet / pageSheet (by default) 時,我們可以這樣取得 UISheetPresentationController
// Get a sheet
if let sheet = viewController.sheetPresentationController {
// Customize the sheet
}
present(viewController, animated: true)
復制代碼
路由跳轉
UISplitViewController
介紹 UISplitViewController 是什么
master detail 概念
showMaster / showDetail 的概念
各種 displayMode 代表什么
為了更好地利用 iPad 更大屏幕的尺寸,系統提供了 UISplitViewController
,以在寬屏情況下並列顯示多個視圖
上圖是 iOS 14 中 UISplitViewController
更新的新接口,允許三欄同時展示。我們可以在系統自帶的 郵件app 看到實際的效果。
iOS 14 更新了新的初始化接口:init(style:)
。通過這個接口我們可以在初始化時設置兩欄或者三欄的布局:
DisplayMode
規定術語:
Master / Primary:兩欄時,展示在左側的單欄
Detail / Secondary:兩欄時,展示在右側的詳細頁面
UISplitViewController
有多種顯示模式,我們稱之為 DisplayMode
。這里簡要介紹一下:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
|
---|---|---|---|---|---|---|
automatic | secondaryOnlyprimaryHidden | oneBesideSecondaryallVisable | oneOverSecondaryprimaryOverlay | twoBesideSecondary iOS 14 available | twoOverSecondary iOS 14 available | twoDisplaceSecondary iOS 14 available |
自動模式,根據屏幕大小自動切換 | 只展示 detail 頁 | Master 和 detail 並列展示 | Master 蓋住了 detail | 兩欄與 detail 並列 | 兩欄蓋住了 detail | 兩欄將 detail 向右擠開,參考 郵件.app |
簡單概括:Bseide 意為並列顯示,over 意為上層會覆蓋下層的一個部分,Displace 意為上層會擠開下層。
常用接口
路由行為
如果是使用 init(style:)
初始化的 iOS 14 列風格 的 SplitVC,一切會變得省心很多:
-
用
setViewController(_:for:)
來設置 VC 應該展示在哪一列 -
用
viewController(for:)
來獲取指定列的 VC -
SplitVC 會自動把所有的 childVC 用 navigationController 包住。
- 如果設置的時候沒有提供 navigationController,SplitVC 會自動創建一個。
- 通過 SplitVC 的 children 屬性可以找到 navigationController。
-
用
show(_:)
或者hide(_:)
來展示或隱藏指定列
如果是傳統風格的 SplitVC(只支持 master & detail 的顯示,不支持更多欄):
- 如果需要,應該手動為 master 和 detail 手動設置 navigationController 以實現路由跳轉。
- 直接設置
viewControllers
屬性,默認第一個為 master,第二個為 detail,會忽略更多(如果有) - 使用
show(_:sender:)
來在 master 中找到 navigationController 進行 push vc - 使用
showDetailViewController(_:sender:)
來 在 detail 中找到 navigationController 進行 push vc
尺寸變化
在 iPad 上,用戶可能進行的分屏操作會突然改變程序的視圖大小。當視圖較窄時,SplitVC 的分欄布局可能不再適合,我們可能需要將所有欄中的 viewControllers 進行合並。當視圖變寬時,我們又需要將 viewControllers 分配到不同的列當中。在這里我們稱之為 Collapse & Expand。
我們可以在 SplitVC 的 delegate 中控制上述行為:
public protocol UISplitViewControllerDelegate {
// Return the view controller which is to become the primary view controller after `splitViewController` is collapsed due to a transition to
// the horizontally-compact size class. If you return `nil`, then the argument will perform its default behavior (i.e. to use its current primary view
// controller).
@available(iOS 8.0, *)
optional func primaryViewController(forCollapsing splitViewController: UISplitViewController) -> UIViewController?
// Return the view controller which is to become the primary view controller after the `splitViewController` is expanded due to a transition
// to the horizontally-regular size class. If you return `nil`, then the argument will perform its default behavior (i.e. to use its current
// primary view controller.)
@available(iOS 8.0, *)
optional func primaryViewController(forExpanding splitViewController: UISplitViewController) -> UIViewController?
// This method is called when a split view controller is collapsing its children for a transition to a compact-width size class. Override this
// method to perform custom adjustments to the view controller hierarchy of the target controller. When you return from this method, you're
// expected to have modified the `primaryViewController` so as to be suitable for display in a compact-width split view controller, potentially
// using `secondaryViewController` to do so. Return YES to prevent UIKit from applying its default behavior; return NO to request that UIKit
// perform its default collapsing behavior.
@available(iOS 8.0, *)
optional func splitViewController( _ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool
// This method is called when a split view controller is separating its child into two children for a transition from a compact-width size
// class to a regular-width size class. Override this method to perform custom separation behavior. The controller returned from this method
// will be set as the secondary view controller of the split view controller. When you return from this method, `primaryViewController` should
// have been configured for display in a regular-width split view controller. If you return `nil`, then `UISplitViewController` will perform
// its default behavior.
@available(iOS 8.0, *)
optional func splitViewController( _ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController?
}
復制代碼
鎖屏/退到后台時在 iPad 上的特殊情況
在 iOS 上,因為需要在 App Switcher 中顯示各應用在橫屏、豎屏、分屏情況下的界面預覽,所以系統會提前在應用鎖屏或退到后台時,對應用進行模擬界面變化並截圖。
系統函數名為beginSnapshotSession。
在 iPad 上的整個模擬界面變化的過程中,一般會模擬橫屏、豎屏、分屏等幾種大小。處於最上層的 VC 可能會收到多次 willTransition / viewWillTransition / viewWillLayout 的調用。
在存在 SplitVC 的情況中,甚至因為模擬分屏,導致 mergeMasterAndDetail 時隱藏了 VC,調用到 VC 的 viewDidDisappear,也是有可能的。
作者:飛書技術
鏈接:https://juejin.cn/post/6986538271698321439
來源:稀土掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。