WWDC21已經(jīng)結(jié)束墓贿,safeAreaInset()
是一個全新的SwiftUI視圖修飾符距潘,它允許我們定義成為觀察安全區(qū)的一部分的視圖孕暇。讓我們深入研究這個新的仑撞、強大的特性。
滾動視圖
最常見的safeAreaInset
用例可能是滾動視圖妖滔。以下面的屏幕為例隧哮,我們有一個帶有一些內(nèi)容的ScrollView
和一個按鈕:
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(1..<30) { _ in
Text("Five Stars")
.font(.largeTitle)
}
.frame(maxWidth: .infinity)
}
.overlay(alignment: .bottom) {
Button {
...
} label: {
Text("Continue")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.controlSize(.large)
.controlProminence(.increased)
.padding(.horizontal)
}
}
}
注意:
.buttonStyle(.bordered)
.controlSize(.large)
.controlProminence(.increased)
是iOS15的視圖修飾符
因為按鈕只是一個覆蓋,滾動視圖不受它的影響座舍,當我們滾動底部時沮翔,這就成為一個問題:
ScrollView
中的最后一個元素被遮擋在按鈕下面!
現(xiàn)在我們把.overlay(alignment: .bottom)
和.safeAreaInset(edge: .bottom)
交換:
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(1..<30) { _ in
Text("Five Stars")
.font(.largeTitle)
}
.frame(maxWidth: .infinity)
}
.safeAreaInset(edge: .bottom) { // ????
Button {
...
} label: {
Text("Continue")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.controlSize(.large)
.controlProminence(.increased)
.padding(.horizontal)
}
}
}
ScrollView
觀察通過safeAreaInset
傳遞下來的新區(qū)域,最后的元素現(xiàn)在可見了:
接下來曲秉,讓我們看看它是如何工作的采蚀。
定義
這個修飾符有兩種變體,每個軸上有一個(水平/垂直):
/// Horizontal axis.
func safeAreaInset<V: View>(
edge: HorizontalEdge,
alignment: VerticalAlignment = .center,
spacing: CGFloat? = nil,
@ViewBuilder content: () -> V
) -> some View
/// Vertical axis.
func safeAreaInset<V: View>(
edge: VerticalEdge,
alignment: HorizontalAlignment = .center,
spacing: CGFloat? = nil,
@ViewBuilder content: () -> V
) -> some View
它們有四個參數(shù):
-
edge
-指定目標區(qū)域的邊緣承二,垂直方向上.top
或.bottom
,水平方向.leading
或.trailing
-
alignment
- 當safeAreaInset
內(nèi)容不適合可用空間時榆鼠,我們指定如何對齊 -
spacing
- 在那里我們可以進一步移動安全區(qū)超出safeAreaInset
內(nèi)容的邊界,默認情況下矢洲,這個參數(shù)有一個非零值璧眠,基于我們的目標平臺約定 -
content
- 在這里定義safeAreaInset
的內(nèi)容
讓我們在實踐中使用它來理解這是怎么回事。
案例
默認情況下读虏,SwiftUI將我們的內(nèi)容放在安全區(qū)域责静,我們將從一個LinearGradient
開始,它總是占用所有可用空間:
struct ContentView: View {
var body: some View {
LinearGradient(
colors: [.mint, .teal, .cyan, .indigo],
startPoint: .leading,
endPoint: .trailing
)
}
}
假設(shè)我們想要擴展頂部安全區(qū)域盖桥,這現(xiàn)在是可能的灾螃,感謝新的safeAreaInset
視圖修改器:
struct ContentView: View {
var body: some View {
LinearGradient(
colors: [.mint, .teal, .cyan, .indigo],
startPoint: .leading,
endPoint: .trailing
)
.safeAreaInset(edge: .top, spacing: 0) {
Color.red.frame(height: 30).opacity(0.5)
}
}
}
我們傳遞了一個透明的視圖作為視圖修改器內(nèi)容:注意LinearGradient
是如何在它下面擴展的。
這是因為我們的safeAreaInset
:
- 取觀察區(qū)域
- 將其內(nèi)容(上面的紅色)放置在該區(qū)域(根據(jù)其參數(shù))
- 基于
content
大小和參數(shù)揩徊,減少可用區(qū)域腰鬼,并將其傳遞給LinearGradient
這是一個很大的區(qū)別與overlay
視圖修改器,其中:
-
overlay
應(yīng)用于放置自身在觀察區(qū)域 -
overlay
繼承視圖位置和大小 -
overlay
被放置在該空間的頂部
事物擺放的方式基本上是相反的塑荒。
Size
因為safeAreaInset
只關(guān)心觀察到的區(qū)域熄赡,它的content
可以超過它應(yīng)用到的視圖的大小:
struct ContentView: View {
var body: some View {
LinearGradient(
colors: [.mint, .teal, .cyan, .indigo],
startPoint: .leading,
endPoint: .trailing
)
.frame(width: 50)
.safeAreaInset(edge: .top, spacing: 0) {
Color.red.frame(height: 30).opacity(0.5)
}
}
}
在這個例子中,受到.frame(width: 50)
修飾符的影響,這個視圖被safeAreaInset
作用的區(qū)域只有50像素齿税。然而彼硫,safeAreaInset
的內(nèi)容仍然占用了它所需要的觀測區(qū)域的所有空間。
間距Spacing
spacing參數(shù)將進一步改變安全區(qū)域safeAreaInset
內(nèi)容的邊界,在上面的例子中我們都是把它設(shè)置為0,這次我們把它設(shè)置為50:
struct ContentView: View {
var body: some View {
LinearGradient(
colors: [.mint, .teal, .cyan, .indigo],
startPoint: .leading,
endPoint: .trailing
)
.safeAreaInset(edge: .top, spacing: 50) {
Color.red.frame(height: 30).opacity(0.5)
}
}
}
現(xiàn)在在我們的Color.red
和LinearContent
之間有50個點的差距:這個間距總是減少我們原始視圖(例子中的LinearGradient
)提供的區(qū)域拧篮,并且只針對我們的目標邊緣词渤。
如果我們傳遞一個負間距,那么我們將減少安全區(qū)域:
struct ContentView: View {
var body: some View {
LinearGradient(
colors: [.mint, .teal, .cyan, .indigo],
startPoint: .leading,
endPoint: .trailing
)
.safeAreaInset(edge: .top, spacing: -10) {
Color.red.frame(height: 30).opacity(0.5)
}
}
}
正如所料,safeAreaInset
內(nèi)容沒有移動串绩,然而缺虐,LinearGradient
現(xiàn)在重疊Color.red
10個像素點,因為safeAreaInset
的spacing
為-10
礁凡。
Alignment
alignment
參數(shù)的工作原理類似于它在overlay
上的做法高氮,當safeAreaInset
內(nèi)容不完全適合可用空間時,將其定位在正確的位置顷牌。
使用Color.red.frame(height: 30)
,safeAreaInset
內(nèi)容總是占用所有的水平可用空間纫溃,讓我們將其寬度限制為30,并聲明一個.trailing
對齊:
struct ContentView: View {
var body: some View {
LinearGradient(
colors: [.mint, .teal, .cyan, .indigo],
startPoint: .leading,
endPoint: .trailing
)
.safeAreaInset(edge: .top, alignment: .trailing, spacing: 0) {
Color.red.frame(width: 30, height: 30)
}
}
}
在介紹完了之后韧掩,讓我們嘗試用我們的新修改器做更多的實驗。
累積視圖修飾符
當我們將多個safeAreaInset
應(yīng)用到同一個視圖時會發(fā)生什么?
struct ContentView: View {
var body: some View {
LinearGradient(
colors: [.mint, .teal, .cyan, .indigo],
startPoint: .leading,
endPoint: .trailing
)
.safeAreaInset(edge: .bottom, spacing: 0) {
Color.red.frame(height: 30).opacity(0.5)
}
.safeAreaInset(edge: .bottom, spacing: 0) {
Color.green.frame(height: 30).opacity(0.5)
}
.safeAreaInset(edge: .bottom, spacing: 0) {
Color.blue.frame(height: 30).opacity(0.5)
}
}
}
讓我們回到文章的開頭窖铡,我們描述了safeAreaInset
的三個步驟:
- 取觀察區(qū)域
- 將其內(nèi)容(上面的紅色)放置在該區(qū)域(根據(jù)其參數(shù))
- 基于
content
大小和參數(shù)疗锐,減少可用區(qū)域,并將其傳遞給LinearGradient
第一個應(yīng)用的視圖修改器是最外面的一個费彼,帶有Color.blue
那個滑臊,它執(zhí)行上面的三個步驟,并將減少的可用區(qū)域向下傳遞到倒數(shù)第二個safeAreaInset
箍铲,即Color.green
雇卷,其他的也一樣。
這是最終的結(jié)果:
多個邊緣
我們已經(jīng)看到了如何“堆疊”多個safeAreaInsets
,然而颠猴,我們不需要在一條邊停止:完全可以應(yīng)用多個safeAreaInset
修改器关划,應(yīng)用到不同的邊:
struct ContentView: View {
var body: some View {
LinearGradient(
colors: [.mint, .teal, .cyan, .indigo],
startPoint: .leading,
endPoint: .trailing
)
.safeAreaInset(edge: .top, spacing: 0) {
Color.red.frame(height: 30).opacity(0.5)
}
.safeAreaInset(edge: .trailing, spacing: 0) {
Color.green.frame(width: 30).opacity(0.5)
}
.safeAreaInset(edge: .bottom, spacing: 0) {
Color.blue.frame(height: 30).opacity(0.5)
}
.safeAreaInset(edge: .leading, spacing: 0) {
Color.yellow.frame(width: 30).opacity(0.5)
}
}
}
同樣的邏輯仍然有效,不管每個safeAreaInset
修飾符的目標是什么邊緣:
- 首先我們應(yīng)用/放置(最外面的)
Color.yellow``safeAreaInset
翘瓮,它將占用所有需要的空間贮折,并向下傳遞縮小的區(qū)域 - 然后我們轉(zhuǎn)到
Color.blue``safeAreaInset
也會做同樣的事情
ignoresSafeArea
先前的ignoresSafeArea
意味著讓我們的視圖被放置在Home指示符、鍵盤或狀態(tài)欄下:
在iOS15中资盅,ignoresSafeArea
也意味著重置任何safeAreaInset
调榄。
在下面的例子中,我們首先放置safeAreaInset
呵扛,然后在放置最終視圖之前忽略它:
struct ContentView: View {
var body: some View {
LinearGradient(
colors: [.mint, .teal, .cyan, .indigo],
startPoint: .leading,
endPoint: .trailing
)
.ignoresSafeArea(edges: .bottom)
.safeAreaInset(edge: .bottom, spacing: 0) {
Color.red.frame(height: 30).opacity(0.5)
}
}
}
在Xcode 13b1每庆,只有
ScrollView
正確地遵守了safeAreaInsets
:希望列表和表單將在即將到來的Xcode種子中被修復(fù)
兼容iOS15之前的版本
safeAreaInset
是iOS15才開始支持的API,那么如何在iOS13和14中使用相同的功能呢今穿?
@available(iOS, introduced: 13, deprecated: 15, message: "Use .safeAreaInset() directly") // ???? 2
extension View {
@ViewBuilder
func bottomSafeAreaInset<OverlayContent: View>(_ overlayContent: OverlayContent) -> some View {
if #available(iOS 15.0, *) {
self.safeAreaInset(edge: .bottom, spacing: 0, content: { overlayContent }) // ???? 1
} else {
self.modifier(BottomInsetViewModifier(overlayContent: overlayContent))
}
}
}
我們希望在我們放棄對舊iOS版本的支持后缤灵,能夠更容易地轉(zhuǎn)移到SwiftUI的safeAreaInset
。
struct BottomInsetViewModifier<OverlayContent: View>: ViewModifier {
@Environment(\.bottomSafeAreaInset) var ancestorBottomSafeAreaInset: CGFloat
var overlayContent: OverlayContent
@State var overlayContentHeight: CGFloat = 0
func body(content: Self.Content) -> some View {
content
.environment(\.bottomSafeAreaInset, overlayContentHeight + ancestorBottomSafeAreaInset)
.overlay(
overlayContent
.readHeight {
overlayContentHeight = $0
}
.padding(.bottom, ancestorBottomSafeAreaInset)
,
alignment: .bottom
)
}
}
extension View {
func readHeight(onChange: @escaping (CGFloat) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Spacer()
.preference(
key: HeightPreferenceKey.self,
value: geometryProxy.size.height
)
}
)
.onPreferenceChange(HeightPreferenceKey.self, perform: onChange)
}
}
private struct HeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}
struct BottomSafeAreaInsetKey: EnvironmentKey {
static var defaultValue: CGFloat = 0
}
extension EnvironmentValues {
var bottomSafeAreaInset: CGFloat {
get { self[BottomSafeAreaInsetKey.self] }
set { self[BottomSafeAreaInsetKey.self] = newValue }
}
}
struct ExtraBottomSafeAreaInset: View {
@Environment(\.bottomSafeAreaInset) var bottomSafeAreaInset: CGFloat
var body: some View {
Spacer(minLength: bottomSafeAreaInset)
}
}
使用案例如下:
struct ContentView: View {
var body: some View {
ScrollView {
scrollViewContent
ExtraBottomSafeAreaInset()
}
.bottomSafeAreaInset(overlayContent)
.bottomSafeAreaInset(overlayContent)
.bottomSafeAreaInset(overlayContent)
.bottomSafeAreaInset(overlayContent)
.bottomSafeAreaInset(overlayContent)
}
var scrollViewContent: some View {
ForEach(1..<60) { _ in
Text("Five Stars")
.font(.title)
.frame(maxWidth: .infinity)
}
}
var overlayContent: some View {
Button {
// ...
} label: {
Text("Continue")
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.accentColor.cornerRadius(8))
.padding(.horizontal)
}
}
}
結(jié)論
WWDC21給我們帶來了很多新的SwiftUI功能,讓我們可以將我們的應(yīng)用程序推向下一個層次:safeAreaInset
是那些你不知道你需要的視圖修改器之一凤价,它有一個偉大的鸽斟,簡單的API。