SwiftUI:如何控制safeAreaInset

WWDC21已經(jīng)結(jié)束墓贿,safeAreaInset()是一個全新的SwiftUI視圖修飾符距潘,它允許我們定義成為觀察安全區(qū)的一部分的視圖孕暇。讓我們深入研究這個新的仑撞、強大的特性。

滾動視圖

最常見的safeAreaInset用例可能是滾動視圖妖滔。以下面的屏幕為例隧哮,我們有一個帶有一些內(nèi)容的ScrollView和一個按鈕:

button.png
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的視圖修飾符

因為按鈕只是一個覆蓋,滾動視圖不受它的影響座舍,當我們滾動底部時沮翔,這就成為一個問題:

no.gif

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)在可見了:

yes.gif

接下來曲秉,讓我們看看它是如何工作的采蚀。

定義

這個修飾符有兩種變體,每個軸上有一個(水平/垂直):

/// 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開始,它總是占用所有可用空間:

base.png

struct ContentView: View {
  var body: some View {
    LinearGradient(
      colors: [.mint, .teal, .cyan, .indigo],
      startPoint: .leading,
      endPoint: .trailing
    )
  }
}

假設(shè)我們想要擴展頂部安全區(qū)域盖桥,這現(xiàn)在是可能的灾螃,感謝新的safeAreaInset視圖修改器:

red.png
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:

  1. 取觀察區(qū)域
  2. 將其內(nèi)容(上面的紅色)放置在該區(qū)域(根據(jù)其參數(shù))
  3. 基于content大小和參數(shù)揩徊,減少可用區(qū)域腰鬼,并將其傳遞給LinearGradient

這是一個很大的區(qū)別與overlay視圖修改器,其中:

  1. overlay應(yīng)用于放置自身在觀察區(qū)域
  2. overlay繼承視圖位置和大小
  3. overlay被放置在該空間的頂部

事物擺放的方式基本上是相反的塑荒。

Size

因為safeAreaInset只關(guān)心觀察到的區(qū)域熄赡,它的content可以超過它應(yīng)用到的視圖的大小:

size.png
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:

gap.png
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.redLinearContent之間有50個點的差距:這個間距總是減少我們原始視圖(例子中的LinearGradient)提供的區(qū)域拧篮,并且只針對我們的目標邊緣词渤。

如果我們傳遞一個負間距,那么我們將減少安全區(qū)域:

overlap.png
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.red10個像素點,因為safeAreaInsetspacing-10礁凡。

Alignment

alignment參數(shù)的工作原理類似于它在overlay上的做法高氮,當safeAreaInset內(nèi)容不完全適合可用空間時,將其定位在正確的位置顷牌。

使用Color.red.frame(height: 30),safeAreaInset內(nèi)容總是占用所有的水平可用空間纫溃,讓我們將其寬度限制為30,并聲明一個.trailing對齊:

align.png
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的三個步驟:

  1. 取觀察區(qū)域
  2. 將其內(nèi)容(上面的紅色)放置在該區(qū)域(根據(jù)其參數(shù))
  3. 基于content大小和參數(shù)疗锐,減少可用區(qū)域,并將其傳遞給LinearGradient

第一個應(yīng)用的視圖修改器是最外面的一個费彼,帶有Color.blue那個滑臊,它執(zhí)行上面的三個步驟,并將減少的可用區(qū)域向下傳遞到倒數(shù)第二個safeAreaInset箍铲,即Color.green雇卷,其他的也一樣。

這是最終的結(jié)果:

stack.png

多個邊緣

我們已經(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)
    }
  }
}
multiple.png

同樣的邏輯仍然有效,不管每個safeAreaInset修飾符的目標是什么邊緣:

  • 首先我們應(yīng)用/放置(最外面的)Color.yellow``safeAreaInset翘瓮,它將占用所有需要的空間贮折,并向下傳遞縮小的區(qū)域
  • 然后我們轉(zhuǎn)到Color.blue``safeAreaInset也會做同樣的事情

ignoresSafeArea

先前的ignoresSafeArea意味著讓我們的視圖被放置在Home指示符、鍵盤或狀態(tài)欄下:
在iOS15中资盅,ignoresSafeArea也意味著重置任何safeAreaInset调榄。

在下面的例子中,我們首先放置safeAreaInset呵扛,然后在放置最終視圖之前忽略它:

ignore.png
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)
  }
}

使用案例如下:

stackSafeAreaInset.gif
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。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末利诺,一起剝皮案震驚了整個濱河市富蓄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌慢逾,老刑警劉巖立倍,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異侣滩,居然都是意外死亡口注,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門君珠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寝志,“玉大人,你說我怎么就攤上這事策添〔牟浚” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵唯竹,是天一觀的道長乐导。 經(jīng)常有香客問我,道長浸颓,這世上最難降的妖魔是什么物臂? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮产上,結(jié)果婚禮上棵磷,老公的妹妹穿的比我還像新娘。我一直安慰自己晋涣,他們只是感情好泽本,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著姻僧,像睡著了一般规丽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上撇贺,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天赌莺,我揣著相機與錄音,去河邊找鬼松嘶。 笑死艘狭,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播巢音,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼遵倦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了官撼?” 一聲冷哼從身側(cè)響起梧躺,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎傲绣,沒想到半個月后掠哥,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡秃诵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年续搀,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片菠净。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡禁舷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出毅往,到底是詐尸還是另有隱情榛了,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布煞抬,位于F島的核電站,受9級特大地震影響构哺,放射性物質(zhì)發(fā)生泄漏革答。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一曙强、第九天 我趴在偏房一處隱蔽的房頂上張望残拐。 院中可真熱鬧,春花似錦碟嘴、人聲如沸溪食。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽错沃。三九已至,卻和暖如春雀瓢,著一層夾襖步出監(jiān)牢的瞬間枢析,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工刃麸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留醒叁,地道東北人。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像把沼,于是被迫代替她去往敵國和親啊易。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容