堆栈,网格,滚动视图

187 阅读6分钟

如何使用 VStack 和 HStack 创建堆栈?

我们的 SwiftUI 内容视图必须包含一个或多个视图,这是我们希望它们显示的布局。 当我们一次想要在屏幕上显示多个视图时,通常需要告诉 SwiftUI 如何安排它们,这就是堆栈的存放位置。

堆栈-等同于 UIKit 中的 UIStackView - 分为三种形式:水平(HStack),垂直(VStack)和基于深度(ZStack),当您要放置子视图以使其重叠时可以使用后者。

需要将其放置在垂直堆栈中,以便我们的文本视图彼此重叠放置:

VStack {
    Text("SwiftUI")
    Text("rocks")
}

水平并排放置标签,则将 VStack 替换为 HStack,如下所示:

HStack {
    Text("SwiftUI")
    Text("rocks")
}

如何使用对齐和间距自定义堆栈布局?

您可以通过在初始化程序中提供一个值来在 SwiftUI 堆栈内添加 spacing,如下所示:

VStack(spacing: 50) {
    Text("SwiftUI")
    Text("rocks")
}

您可以在项目之间创建分隔线,以便 SwiftUI 在堆栈中的每个项目之间进行小的视觉区分,如下所示:

VStack {
    Text("SwiftUI")
    Divider()
    Text("rocks")
}

堆栈中的项目在中心对齐。 对于 HStack,这意味着项目在中间垂直对齐,因此,如果您有两个高度不同的文本视图,则它们都将在其垂直中心对齐。 对于 VStack,这意味着项目在中间居中对齐,因此,如果您有两个不同长度的文本视图,则它们都将与其水平中心对齐。

VStack(alignment: .leading) {
    Text("SwiftUI")
    Text("rocks")
}

这样会将 SwiftUI 和 rocks 都对齐到其左边缘,但是它们最终仍将位于屏幕的中间,因为堆栈仅占用所需的空间。

当然,您可以同时使用对齐和间距,如下所示:

VStack(alignment: .leading, spacing: 20) {
    Text("SwiftUI")
    Text("rocks")
}

如何使用 Spacer 将视图强制移到堆栈中的一侧?

SwiftUI 默认情况下将其视图居中,这意味着如果在 VStack 中放置三个文本视图,则这三个视图将在屏幕上垂直居中。 如果要更改此设置(如果要强制视图朝屏幕的顶部,底部,左侧或右侧),则应使用 Spacer 视图。

要将文本视图推到父视图的顶部,我们需要在其下方放置一个空格,如下所示:

VStack {
    Text("Hello World")
    Spacer()
}

如果您希望在 HStack 的前端和后端有两段文字,可以使用这样的间隔符:

HStack {
    Text("Hello")
    Spacer()
    Text("World")
}

如何使用 zIndex 更改视图分层的顺序?

默认情况下,SwiftUI 的 ZStack 使用绘画者的算法来确定视图的深度,以决定视图的深度:首先绘制到 ZStack 中的所有内容都会被首先绘制,然后在其上分层放置随后的视图。

尽管这通常是您想要的,但有时您需要更多的控制权-例如,您可能希望在应用运行时将一个视图推到另一个视图的后面,或者在点击某个视图时将其移到最前面。

为此,您需要使用 zIndex() 修饰符,该修饰符允许您确切指定视图如何在单个 ZStack 中分层。 视图的默认Z索引为0,但是您可以提供正值或负值,分别将它们放置在其他视图的上方或下方。

例如,此 ZStack 包含两个重叠的矩形,但是绿色矩形仍然可见,因为它使用的 Z 索引值为 1

ZStack {
    Rectangle()
        .fill(Color.green)
        .frame(width: 50, height: 50)
        .zIndex(1)

    Rectangle()
        .fill(Color.red)
        .frame(width: 100, height: 100)
}

⚠️ 注意:zIndex() 修饰符仅影响当前 ZStack 内的绘制顺序。 这意味着,如果您有两个重叠的堆栈,则需要考虑堆栈的 Z 索引以及堆栈内部的视图。

如何使用尺寸类创建不同的布局?

SwiftUI 通过将大小型暴露在环境中供我们阅读来原生支持大小类。 要使用它们,首先创建一个 @Environment 对象,该对象将存储其值,然后在需要时检查该属性的值,查找 .compact 或 .regular 尺寸类。

struct ContentView: View {
    #if !os(macOS)
    @Environment(.horizontalSizeClass) var horizontalSizeClass
    #endif

    var body: some View {
      #if !os(macOS)
      if horizontalSizeClass == .compact {
          Text("Compact")
      } else {
          Text("Regular")
      }
      #else
          Text("Regular2")
      #endif
    }
}

如何使用 ScrollView 添加水平和垂直滚动?

SwiftUI 的 ScrollView 允许我们相对轻松地创建视图的滚动容器,因为它会自动调整自身大小以适合我们放置在其中的内容,并且还会自动添加额外的插图以避免出现安全区域。

例如,我们可以创建一个包含十个文本视图的滚动列表,如下所示:

ScrollView {
    VStack(spacing: 20) {
        ForEach(0..<10) {
            Text("Item ($0)")
                .foregroundColor(.white)
                .font(.largeTitle)
                .frame(width: 200, height: 200)
                .background(Color.red)
        }
    }
}
.frame(height: 350)    

滚动视图默认情况下是垂直的,但是您可以通过传入 .horizontal 作为第一个参数来控制轴。 因此,我们可以将前面的示例翻转为水平,如下所示:

ScrollView(.horizontal) {
    HStack(spacing: 20) {
        ForEach(0..<10) {
            Text("Item ($0)")
                .foregroundColor(.white)
                .font(.largeTitle)
                .frame(width: 200, height: 200)
                .background(Color.red)
        }
    }
}

您可以使用 [.horizontal,.vertical] 同时指定两个轴。

最后,您可以决定是否在滚动动作发生时显示滚动指示器,如下所示:

ScrollView(.horizontal, showsIndicators: false) {
    HStack(spacing: 20) {
        ForEach(0..<10) {
            Text("Item ($0)")
                .foregroundColor(.white)
                .font(.largeTitle)
                .frame(width: 200, height: 200)
                .background(Color.red)
        }
    }
}

如何使用 ScrollViewReader 使滚动视图移动到某个位置?

如果您想以编程方式使 SwiftUI 的 ScrollView 移至特定位置,则应在其中嵌入 ScrollViewReader。 这提供了一个 scrollTo() 方法,仅通过提供其锚即可将其移动到父 scrollview 内部的任何视图。

例如,这将在垂直滚动视图中创建 100 个彩色框,并且当您按下按钮时,它将直接滚动到 ID 为 8 的框:

image.png

struct ContentView: View {
    let colors: [Color] = [.red, .green, .blue]

    var body: some View {
        ScrollView {
            ScrollViewReader { value in
                Button("Jump to #8") {
                    value.scrollTo(8)
                }
                .padding()

                ForEach(0..<100) { i in
                    Text("Example (i)")
                        .font(.title)
                        .frame(width: 200, height: 200)
                        .background(colors[i % colors.count])
                        .id(i)
                }
            }
        }
        .frame(height: 350)
    }
}

为了更好地控制滚动,可以指定另一个参数称为 anchor ,以控制滚动完成后应将目标视图放置在何处。

例如,这将滚动到与以前相同的视图,只是这次将视图置于顶部:

struct ContentView: View {
    let colors: [Color] = [.red, .green, .blue]

    var body: some View {
        ScrollView {
            ScrollViewReader { value in
                Button("Jump to #8") {
                    value.scrollTo(8, anchor: .top)
                }
                .padding()

                ForEach(0..<100) { i in
                    Text("Example (i)")
                        .font(.title)
                        .frame(width: 200, height: 200)
                        .background(colors[i % colors.count])
                        .id(i)
                }
            }
        }
        .frame(height: 350)
    }
}

如果在 withAnimation() 中调用 scrollTo() ,则将对动画进行动画处理。

提示:scrollTo() 也适用于列表!

如何使用 ScrollView 和 GeometryReader 创建3D效果(如Cover Flow)?

如果将 GeometryReader 与任何可以改变位置的视图(例如具有拖动手势或位于 List 内的视图)结合使用,我们可以创建在屏幕上看起来很棒的3D效果。GeometryReader 允许我们读取视图的坐标,并将这些值直接输入到 rotation3DEffect() 修饰符中。

例如,我们可以通过在滚动视图中水平堆叠许多文本视图,然后应用 rotation3DEffect() 来创建 Cover Flow 样式的滚动效果,这样当它们在滚动视图中移动时,它们会缓慢地旋转,如下所示:

ScrollView(.horizontal, showsIndicators: false) {
    HStack(spacing: 0) {
        ForEach(1..<20) { num in
            VStack {
                GeometryReader { geo in
                    Text("Number (num)")
                        .font(.largeTitle)
                        .padding()
                        .background(Color.red)
                        .rotation3DEffect(.degrees(-Double(geo.frame(in: .global).minX) / 8), axis: (x: 0, y: 1, z: 0))
                        .frame(width: 200, height: 200)
                }
                .frame(width: 200, height: 200)
            }
        }
    }
}

image.png

如何使用 LazyVGrid 和 LazyHGrid 在网格中放置视图?

SwiftUI 的 LazyVGrid 和 LazyHGrid 为我们提供了相当多的灵活性的网格布局。 最简单的网格由三部分组成:原始数据,描述所需布局的 GridItem 数组以及将数据和布局组合在一起的 LazyVGrid 或 LazyHGrid

例如,这将使用大小为80点的像元创建垂直网格布局:

struct ContentView: View {
    let data = (1...100).map { "Item ($0)" }

    let columns = [        GridItem(.adaptive(minimum: 80))    ]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 20) {
                ForEach(data, id: .self) { item in
                    Text(item)
                }
            }
            .padding(.horizontal)
        }
        .frame(maxHeight: 300)
    }
}

image.png

使用 GridItem(.adaptive(minimum:80) 意味着我们希望网格可以容纳每行尽可能多的项目,每个网格的最小大小为 80点。

如果要控制列数,可以改用 .flexible(),它还可以让您指定每个项目的大小,但现在可以控制有多少列。 例如,这将创建五列:

struct ContentView: View {
    let data = (1...100).map { "Item ($0)" }

    let columns = [        GridItem(.flexible()),        GridItem(.flexible()),        GridItem(.flexible()),        GridItem(.flexible()),        GridItem(.flexible())    ]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 20) {
                ForEach(data, id: .self) { item in
                    Text(item)
                }
            }
            .padding(.horizontal)
        }
        .frame(maxHeight: 300)
    }
}

image.png

第三种选择是使用固定大小。 例如,这将使第一列的宽度恰好为 100 点,并允许第二列填充所有剩余空间:

struct ContentView: View {
    let data = (1...100).map { "Item ($0)" }

    let columns = [        GridItem(.fixed(100)),        GridItem(.flexible()),    ]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 20) {
                ForEach(data, id: .self) { item in
                    Text(item)
                }
            }
            .padding(.horizontal)
        }
        .frame(maxHeight: 300)
    }
}

image.png

您还可以使用 LazyHGrid 制作水平滚动的网格,除了在初始化程序中接受行之外,其工作方式几乎相同。

例如,我们可以创建 10 个并排的标题图像,它们像这样水平滚动:

struct ContentView: View {
    let items = 1...50

    let rows = [        GridItem(.fixed(50)),        GridItem(.fixed(50))    ]

    var body: some View {
        ScrollView(.horizontal) {
            LazyHGrid(rows: rows, alignment: .center) {
                ForEach(items, id: .self) { item in
                    Image(systemName: "(item).circle.fill")
                        .font(.largeTitle)
                }
            }
            .frame(height: 150)
        }
    }
}

image.png

如何使用 LazyVStack 和 LazyHStack 延迟加载视图?

默认情况下,SwiftUI的VStack和HStack会预先加载其所有内容,如果在滚动视图中使用它们,可能会很慢。 如果您要延迟加载内容(即,仅当内容滚动到视图中时),则应适当使用LazyVStack和LazyHStack。

struct ContentView: View {
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(1...100, id: .self) { value in
                    Text("Row (value)")
                }
            }
        }
        .frame(height: 300)
    }
}

警告:这些惰性堆栈自动具有灵活的首选宽度,因此它们将以常规堆栈不会占用的方式占用可用空间。 要查看它们之间的区别,请尝试上面的代码,您会发现可以使用文本周围的空白来拖拉,但是如果您切换到常规 VStack,则会看到需要使用文本本身进行滚动。

使用惰性堆栈时,SwiftUI将在首次显示时自动创建视图。 之后,视图将保留在内存中,因此请注意显示的内容。

如果您想了解延迟加载的工作原理,请在iOS应用中尝试以下示例:

struct SampleRow: View {
    let id: Int

    var body: some View {
        Text("Row (id)")
    }

    init(id: Int) {
        print("Loading row (id)")
        self.id = id
    }
}

struct ContentView: View {
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(1...100, id: .self, content: SampleRow.init)
            }
        }
        .frame(height: 300)
    }
}