SwiftUI最強(qiáng)大的一個(gè)方面是它如何根據(jù)上下文進(jìn)行調(diào)整,這是SwiftUI的承諾,適用于所有蘋(píng)果設(shè)備渔肩,從38mm的Apple Watch到27英寸的iMac(不考慮外部顯示器!)
雖然這可以節(jié)省很多時(shí)間,但有時(shí)我們想讓UI聲明更具適應(yīng)性:在本文中,讓我們看看如何做到這一點(diǎn)厨相。
示例
在我們的應(yīng)用中,我們希望根據(jù)可用空間調(diào)整視圖鸥鹉。
我們定義了兩種布局蛮穿,一種是內(nèi)容垂直堆疊,另一種是內(nèi)容水平堆疊:
在考慮如何選擇布局之前,讓我們定義一個(gè)通用的可重用視圖AdaptiveView
:
struct AdaptiveView<Content: View>: View {
var content: Content
public init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
if /* condition here */ {
HStack {
content
}
} else {
VStack {
content
}
}
}
}
我們稍后再填寫(xiě)這個(gè)條件
我們的示例視圖將能夠使用這個(gè)新的定義蒜焊,其中它所需要聲明的只是內(nèi)容梆暮,其他一切都由AdaptiveView
管理:
var body: some View {
AdaptiveView {
RoundedRectangle(...)
.fill(...)
.frame(maxHeight: 400)
VStack {
Text("Title")
.bold()
.font(.title)
Text(...)
.fixedSize(horizontal: false, vertical: true)
}
}
}
下面讓我們看看如何填充AdaptiveView
條件。
為了簡(jiǎn)單起見(jiàn)府适,我們將重點(diǎn)討論基于水平空間可用的條件:同樣的概念也適用于垂直空間羔飞。
Size classes
所有大尺寸的iphone在橫屏?xí)r都有一個(gè)標(biāo)準(zhǔn)的水平尺寸類。
每個(gè)SwiftUI視圖都可以通過(guò)兩個(gè)environment
環(huán)境值來(lái)觀察屏幕尺寸的變化:horizontalSizeClass
和verticalSizeClass
檐春。
在SwiftUI中逻淌,它們都返回一個(gè)UserInterfaceSizeClass
實(shí)例,在UIKit中UIUserInterfaceSizeClass
是和它相對(duì)應(yīng)的疟暖。
public enum UserInterfaceSizeClass {
case compact
case regular
}
在這個(gè)例子中卡儒,我們可以使AdaptiveView
切換布局基于environment環(huán)境的horizontalSizeClass
:
struct AdaptiveView<Content: View>: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
var content: Content
init(...) { ... }
var body: some View {
if horizontalSizeClass == .regular {
// We have a "regular" horizontal screen estate:
// we lay the content horizontally.
HStack {
content
}
} else {
VStack {
content
}
}
}
}
動(dòng)態(tài)類型
AdaptiveView
可以使用的另一種方法是基于environment的ContentSizeCategory
:
UserInterfaceSizeClass
告訴我們?cè)诋?dāng)前方向上設(shè)備屏幕的compact/regular大小,而ContentSizeCategory
告訴我們用戶首選的內(nèi)容大小(也就是動(dòng)態(tài)類型)俐巴。
public enum ContentSizeCategory: Hashable, CaseIterable {
case extraSmall
case small
case medium
case large
case extraLarge
case extraExtraLarge
case extraExtraExtraLarge
case accessibilityMedium
case accessibilityLarge
case accessibilityExtraLarge
case accessibilityExtraExtraLarge
case accessibilityExtraExtraExtraLarge
}
我們可以在AdaptiveView
中使用這些情況中的任何一種作為條件閾值骨望,例如,我們可以切換比.large
更大的布局:
struct AdaptiveView<Content: View>: View {
@Environment(\.sizeCategory) var sizeCategory: ContentSizeCategory
var content: Content
init(...) { ... }
var body: some View {
if sizeCategory > .large {
VStack {
content
}
} else {
HStack {
content
}
}
}
}
SwiftUI也為ContentSizeCategory
提供了isAccessibilityCategory
屬性窜骄,我們也可以這么使用:
struct AdaptiveView<Content: View>: View {
@Environment(\.sizeCategory) var sizeCategory: ContentSizeCategory
var content: Content
init(...) { ... }
var body: some View {
if sizeCategory.isAccessibilityCategory {
// When the user prefers an accessibility category, lay the content vertically.
VStack {
content
}
} else {
HStack {
content
}
}
}
}
當(dāng)ContentSizeCategory
實(shí)例以"accessibility
"開(kāi)頭時(shí)锦募,isAccessibilityCategory
返回true
,這似乎是一個(gè)很好的默認(rèn)閾值:
當(dāng)然邻遏,我們應(yīng)該測(cè)試一下我們的實(shí)現(xiàn)糠亩,看看是否適合我們,如果不行准验,我們可以回到另一個(gè)閾值赎线。
自定義閾值
到目前為止提供的方法適用于大多數(shù)視圖,但是糊饱,它們也有一個(gè)很大的缺點(diǎn):它們依賴于全局值垂寥。
當(dāng)一個(gè)單一的AdaptiveView
是屏幕的主要內(nèi)容,但如果我們有多個(gè)視圖另锋,應(yīng)該適應(yīng)?
如果我們?cè)谶@種情況下滞项,我們可能不能依賴這些全局環(huán)境屬性:相反,我們應(yīng)該為每個(gè)視圖分別做出決定夭坪。
這樣文判,兩個(gè)或更多的視圖可以根據(jù)自己的空間和閾值進(jìn)行不同的布局。
為了做到這一點(diǎn)室梅,我們需要采取兩步:
- 獲取每個(gè)
AdaptiveView
的可用水平空間 - 基于這個(gè)空間創(chuàng)建一個(gè)條件
1. 獲取可用的水平空間
幸運(yùn)的是戏仓,我們已經(jīng)在SwiftUI:靈活的布局中遇到了這個(gè)問(wèn)題,并取得了以下結(jié)果:
struct FlexibleView: View {
@State private var availableWidth: CGFloat = 0
var body: some View {
ZStack {
Color.clear
.frame(height: 1)
.readSize { size in
availableWidth = size.width
}
// 我們要實(shí)現(xiàn)的部分
}
}
}
我們可以在我們的通用AdaptiveView
中實(shí)現(xiàn)它:
struct AdaptiveView<Content: View>: View {
@State private var availableWidth: CGFloat = 0
var content: Content
public init(...) { ... }
var body: some View {
ZStack {
Color.clear
.frame(height: 1)
.readSize { size in
availableWidth = size.width
}
if /* condition */ {
HStack {
content
}
} else {
VStack {
content
}
}
}
}
}
extension View {
func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
第一步完成了
基于這個(gè)空間創(chuàng)建一個(gè)條件
一旦我們有了可用的空間亡鼠,剩下要決定的就是如何使用它赏殃。
在構(gòu)建通用視圖時(shí),最好將閾值決定留給實(shí)現(xiàn)者间涵,他們對(duì)視圖的使用位置和實(shí)際內(nèi)容有更多的了解仁热。
由于這些原因,我們可以添加一個(gè)新的屬性threshold
勾哩,它將在AdaptiveView
條件中使用:
struct AdaptiveView<Content: View>: View {
@State private var availableWidth: CGFloat = 0
var threshold: CGFloat
var content: Content
public init(
threshold: CGFloat,
@ViewBuilder content: () -> Content
) {
self.threshold = threshold
self.content = content()
}
var body: some View {
ZStack {
Color.clear
.frame(height: 1)
.readSize { size in
availableWidth = size.width
}
if availableWidth > threshold {
HStack {
content
}
} else {
VStack {
content
}
}
}
}
}
到這里股耽,我們的自定義AdaptiveView
就完成了根盒。
驗(yàn)證
因?yàn)槲覀儸F(xiàn)在擁有了閾值,所以測(cè)試不同的閾值/布局/設(shè)備也很容易物蝙,事例如下
struct ContentView: View {
@State var currentWidth: CGFloat = 0
@State var padding: CGFloat = 8
@State var threshold: CGFloat = 100
var body: some View {
VStack {
AdaptiveView(threshold: threshold) {
RoundedRectangle(cornerRadius: 40.0, style: .continuous)
.fill(
Color(red: 224 / 255.0, green: 21 / 255.0, blue: 90 / 255.0, opacity: 1)
)
RoundedRectangle(cornerRadius: 40.0, style: .continuous)
.fill(
Color.pink
)
}
.readSize { size in
currentWidth = size.width
}
.overlay(
Rectangle()
.stroke(lineWidth: 2)
.frame(width: threshold)
)
.padding(.horizontal, padding)
Text("Current width: \(Int(currentWidth))")
HStack {
Text("Threshold: \(Int(threshold))")
Slider(value: $threshold, in: 0...500, step: 1) { Text("") }
}
HStack {
Text("Padding:")
Slider(value: $padding, in: 0...500, step: 1) { Text("") }
}
}
.padding()
}
}
完整的AdaptiveView
代碼:
多種布局
到目前為止,我們看到的例子根據(jù)我們的條件調(diào)整布局方向敢艰,但是這不是唯一的用例诬乞,例如,我們可以使用類似的方法來(lái)顯示/隱藏UI的一部分:
事例代碼:
struct SocialSignInView: View {
@State private var availableWidth: CGFloat = 0
private var buttonMode: SignInButton.Mode {
availableWidth > 500 ? .regular : .compact
}
var body: some View {
ZStack {
Color.clear
.frame(height: 1)
.readSize { size in
availableWidth = size.width
}
HStack {
SignInButton(action: {}, tintColor: .appleTint, imageName: "apple", mode: buttonMode)
SignInButton(action: {}, tintColor: .googleTint, imageName: "google", mode: buttonMode)
SignInButton(action: {}, tintColor: .twitterTint, imageName: "twitter", mode: buttonMode)
}
}
}
}
struct SocialSignInView_Previews: PreviewProvider {
static var previews: some View {
Group {
SocialSignInView()
.previewLayout(.fixed(width: 568, height: 320))
SocialSignInView()
.previewLayout(.fixed(width: 320, height: 528))
}
}
}
SignInButton:
extension Color {
static let appleTint = Color.black
static let googleTint = Color(red: 222 / 255.0, green: 82 / 255.0, blue: 70 / 255.0)
static let twitterTint = Color(red: 29 / 255.0, green: 161 / 255.0, blue: 242 / 255.0)
}
struct SignInButton: View {
enum Mode {
case regular
case compact
}
var action: () -> Void
var tintColor: Color
var imageName: String
var mode: Mode
var body: some View {
Button(action: action) {
switch mode {
case .compact:
Circle()
.fill(tintColor)
.overlay(Image(imageName))
.frame(width: 44, height: 44)
case .regular:
HStack {
Text("Sign in with")
Image(imageName)
}
.padding()
.background(
Capsule()
.fill(tintColor)
)
}
}
.foregroundColor(.white)
}
}
struct SignInButton_Previews: PreviewProvider {
static var previews: some View {
Group {
SignInButton(action: {}, tintColor: .appleTint, imageName: "apple", mode: .regular)
SignInButton(action: {}, tintColor: .appleTint, imageName: "apple", mode: .compact)
SignInButton(action: {}, tintColor: .googleTint, imageName: "google", mode: .regular)
SignInButton(action: {}, tintColor: .googleTint, imageName: "google", mode: .compact)
SignInButton(action: {}, tintColor: .twitterTint, imageName: "twitter", mode: .regular)
SignInButton(action: {}, tintColor: .twitterTint, imageName: "twitter", mode: .compact)
}
.previewLayout(.sizeThatFits)
}
}
總結(jié)
SwiftUI盡其所能地適應(yīng)每一個(gè)給定的場(chǎng)景:讓框架來(lái)做所有繁重的工作是完全沒(méi)問(wèn)題的钠导,但是如果我們多做一點(diǎn)工作震嫉,就可以幫助我們提供更好的用戶體驗(yàn)。