当尝试编写跨多种设备尺寸的代码时,(比如希望布局在水平空间受限时垂直排列,否则水平排列),SwiftUI 提供了一个简单的解决方案,称为尺寸类别 size classes,这是一种【完全模糊】的方式来告诉我们,视图有多少空间
SwiftUI 通过将尺寸类暴露在环境中供我们阅读来原生支持尺寸类。要使用它们,首先创建一个将存储其值的 @Environment
对象;然后可以声明的尺寸类别的环境值,只有“水平”和“垂直”两种:horizontalSizeClass
& verticalSizeClass
//首先要添加 @Environment 属性
@Environment(\\.horizontalSizeClass) var sizeClass
这将非常粗略地告诉我们:尺寸类别是【常规 regular】尺寸类别或【紧凑 compact】
判断 size class 的值属于常规还是紧凑,分别做处理:
HStack {
//如果横向空间 horizontalSizeClass == 紧凑,则
if sizeClass == .compact {
VStack(spacing: 10) { ResortDetailsView(resort: resort) }
VStack(spacing: 10) { SkiDetailsView(resort: resort) }
}
//如果横向空间不等于紧凑,则
else {
ResortDetailsView(resort: resort)
SkiDetailsView(resort: resort)
}
}
.padding(.vertical)
.background(Color.primary.opacity(0.1))
如果您使用的是 iOS 14 或更高版本,您会发现自定义字体会自动缩放,无需您进行进一步的操作。
但有时候就算是紧凑布局中,也会剩更多的空间,这意味着可以使用更大的【动态字型尺寸】而不会占满空间。虽然许多用户不会遇到这个问题,因为他们默认使用尺寸甚至更小的尺寸。为了更好地处理空间,我们可以将【应用程序的水平尺寸类别】与对【用户的动态类型设置】结合起来检查,以便更好地安排空间。
// 首先添加另一个环境属性来读取当前的动态类型设置:
@Environment(\\.dynamicTypeSize) var typeSize
// 获取动态字型后,我们可以结合屏幕大小进行判断
if sizeClass == .compact && typeSize > .large { ... }
// 如果您想禁用字体的动态类型
// 例如希望字体大小永远不会改变,无论动态类型设置如何。那么您应该在以下情况下将 size 替换为 fixedSize 创建自定义字体
VStack {
Text("This Scales")
.font(.custom("Georgia", size: 24))
Text("This is Fixed")
.font(.custom("Georgia", fixedSize: 24))
}
// 如果希望字体相对于特定动态类型字体进行缩放,则应使用 relativeTo 参数
// 这将使字体从 24pt 开始,但它会相对于 Headline Dynamic Type 字体进行缩放
Text("Scaling")
.font(.custom("Georgia", size: 24, relativeTo: .headline))
使用 dynamicTypeSize()
修饰符,还可以限制特定视图支持的动态类型大小的范围。例如,您可能已经努力支持尽可能广泛的尺寸,但发现任何大于“额外超大”设置的东西看起来都很糟糕。在这种情况下,您可以在视图上使用 dynamicTypeSize()
修饰符,如下所示:
// 与固定值一起使用,这意味着视图将忽略所有动态类型大小
Text("Hello").dynamicTypeSize(.xxLarge)
// 您还可以指定范围,例如允许任何大小,直到并包括大尺寸:
// 这是一个单方面的范围,意味着任何大小小于或等于 .large 都可以,但不能更大
Text("Hello").dynamicTypeSize(...DynamicTypeSize.large)
// 显然,最好尽可能避免设置这些限制,但如果您明智地使用它,这不是问题
// 例如, `TabView` 和 `NavigationView` 都限制其文本标签的大小,以便用户界面不会损坏。
ViewThatFits
视图可以根据可用屏幕空间,从几种可能的布局中选择一种进行使用。这使其成为确保应用程序从最大的 tvOS 屏幕到最小的 Apple Watch 都具有出色外观的绝佳方式。缺点是你无法控制 SwiftUI 具体选哪一种。
// 例如,默认情况下,这将尝试显示 500x200 的矩形,但如果无法容纳可用空间,它将显示 200x200 的圆形:
ViewThatFits {
Rectangle()
.frame(width: 500, height: 200)
Circle()
.frame(width: 200, height: 200)
}
由于没有控制权,所以您应该从【最首选】到【最不首选】列出想要的所有布局替代方案,SwiftUI 将逐个尝试,直到找到适合的一个:
ViewThatFits {
Label("Welcome to AwesomeApp", systemImage: "bolt.shield")
.font(.largeTitle)
Label("Welcome", systemImage: "bolt.shield")
.font(.largeTitle)
Label("Welcome", systemImage: "bolt.shield")
}
当您处理可以根据空间垂直或水平排列的视图时,这特别有用。例如,这创建了一个具有四个不同按钮的视图,然后根据空间大小决定水平或垂直排列它们:
struct OptionsView: View {
var body: some View {
Button("Log in") { }
.buttonStyle(.borderedProminent)
Button("Create Account") { }
.buttonStyle(.bordered)
Button("Settings") { }
.buttonStyle(.bordered)
Spacer().frame(width: 50, height: 50)
Button("Need Help?") { }
}
}
struct ContentView: View {
var body: some View {
ViewThatFits {
HStack(content: OptionsView.init)
VStack(content: OptionsView.init)
}
}
}
SwiftUI 处理文本布局时,文本更喜欢位于同一行。因此默认情况下 ViewThatFits
更喜欢避免导致文本换行的布局。因此,当空间有限时,以下代码将默认为 VStack
而不是使用 HStack
来换行文本:
ViewThatFits {
HStack {
Text("The rain")
Text("in Spain")
Text("falls mainly")
Text("on the Spaniards")
}
// 首选是 VStack
VStack {
Text("The rain")
Text("in Spain")
Text("falls mainly")
Text("on the Spaniards")
}
}
.font(.title)
这里发生的事情是 ViewThatFits
正在水平和垂直测量我们的文本,并试图找到适合这两个维度的文本的东西。文本全部在一行的东西,而不会被垂直截断。这有时会导致问题,但幸运的是我们可以告诉 ViewThatFits
仅关心一维,以便我们可以获得更多控制。例如,您想向用户显示一些条款和条件,如果空间适合,则将其设置为固定文本,否则滚动文本。以下代码不会像你期望的那样工作:
struct ContentView: View {
let terms = String(repeating: "abcde ", count: 100)
var body:some View {
ViewThatFits {
Text(terms)
ScrollView {
Text(terms)
}
}
}
}
除非您有一个巨大的屏幕,否则以上代码将始终选择 ScrollView
版本,因为我们要求 ViewThatFits
关心文本的水平轴和垂直轴。这意味着一旦文本跨行超过一行,SwiftUI 将倾向于避免这种布局。为了解决这个问题,我们可以限制 ViewThatFits
仅测量垂直轴,如下所示:
var body:some View {
ViewThatFits(in: .vertical) {
Text(terms)
ScrollView {
Text(terms)
}
}
}
// 这将允许文本水平换行,但一旦用完垂直空间,SwiftUI 将移至 ScrollView
AnyLayout
结构允许我们根据任何环境上下文,自由地在 HStack
和 VStack
之间切换。
除了 VStackLayout
和 HStackLayout
之外,您还可以使用 ZStackLayout
和 GridLayout
。
非网格布局中使用的任何网格行都不会执行任何操作 - 它们与使用 Group
相同。
// 例如,当我们处于常规水平尺寸类别时,我们可能希望水平显示一组图像,否则希望垂直显示一组图像,如下所示:
struct ContentView: View {
// 获取设备宽度进行后续比较
@Environment(\\.horizontalSizeClass) var horizontalSizeClass
var body: some View {
// 如果设备视图尺寸等于 regular,则使用 AnyLayout(HStackLayout()),否则使用 AnyLayout(VStackLayout()
let layout = horizontalSizeClass == .regular ? AnyLayout(HStackLayout()) : AnyLayout(VStackLayout())
// AnyLayout属性
layout {
Image(systemName: "1.circle")
Image(systemName: "2.circle")
Image(systemName: "3.circle")
}
.font(.largeTitle)
}
}
<aside> 💡 提示:与使用 AnyView 不同, AnyLayout 不会产生任何性能影响,并且不会丢弃其子视图的任何状态。
</aside>
如果希望将应用程序仅支持在设备的横向上运行 —— 即强制设备使用 “横向模式” ,需要: