size classes 尺寸类别

当尝试编写跨多种设备尺寸的代码时,(比如希望布局在水平空间受限时垂直排列,否则水平排列),SwiftUI 提供了一个简单的解决方案,称为尺寸类别 size classes,这是一种【完全模糊】的方式来告诉我们,视图有多少空间

1. 设定环境值

SwiftUI 通过将尺寸类暴露在环境中供我们阅读来原生支持尺寸类。要使用它们,首先创建一个将存储其值的 @Environment 对象;然后可以声明的尺寸类别的环境值,只有“水平”和“垂直”两种:horizontalSizeClass & verticalSizeClass

//首先要添加 @Environment 属性
@Environment(\\.horizontalSizeClass) var sizeClass

这将非常粗略地告诉我们:尺寸类别是【常规 regular】尺寸类别或【紧凑 compact】

2. 获取环境值进行判断

判断 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))

Dynamic Type sizes 动态字型

如果您使用的是 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 自动选择视图

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 动态切换堆栈视图

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>


强制程序仅支持横向屏幕

如果希望将应用程序仅支持在设备的横向上运行 —— 即强制设备使用 “横向模式” ,需要: