深入了解 SwiftUI 5 中 ScrollView 的新功能 | 肘子的 Swift 记事本
List
和 Form
也可以创建滚动视图效果,但有时如果想要滚动任意的数据(例如我们手动创建的一些视图),就需要用 ScrollView
ScrollView
是指定的滚动方向的扩张型视图,可以将滚动方向设置为:垂直、水平、或双向滚动ScrollView
就像子视图的容器。 当其中的子视图进入帧外时,用户可以滚动将帧外的子视图带入视图ScrollView
可以控制系统是否应在其旁边显示滚动指示器,这些小滚动条可以让用户了解内容有多大ScrollView
内包含 ForEach
视图,就类似列表 List
。 但注意这些行不可重复使用,出于内存和性能考虑最好限制行数<aside>
💡 ScrollView
对待安全区域就像 List
和 Form
一样。它们的内容位于主界面的指示器的下面,但它们添加了一些额外的填充间距,以便整个视图完全可见,不会被挡住一部分。
</aside>
// 例如,可以创建一个包含 100 个文本视图的滚动列表,如下所示:
ScrollView {
VStack(spacing: 10) {
ForEach(0..<100) {
Text("Item \\($0)")
.font(.title)
}
}
}
// 但由于 scrollView 是滚动方向的扩张型视图;上面例子中,它是垂直方向扩张,水平方向不扩张
// 因此在水平方向上,上面的滚动视图只占屏幕中间的一小块区域,这让滚动不方便,通常要让整个屏幕区域都支持滚动
// 为了获得这种行为,应该让 VStack 占用更多空间(scrollView 对滑动的响应区域,是其内部所有元素所占据的区域)
// 同时保持默认的居中对齐不变,修改如下:
ScrollView {
VStack(spacing: 10) {
ForEach(0..<100) {
Text("Item \\($0)")
.font(.title)
}
}
.frame(maxWidth: .infinity)
}
VStack
和 HStack
会预先加载所有内容。如果在 scrollView
中使用它们,里面子元素都是一开始就全部添加进视图的,即使在屏幕上看不见。SwiftUI 不会等到向下滚动才能看到它们,它只会立即创建它们。所以这可能会很慢。LazyVStack
和 LazyHStack
。它们的使用方式与常规堆栈完全相同,但会按需加载其内容(在实际显示之前它们不会创建视图)。因此最大限度地减少了所使用的系统资源量。<aside>
💡 这些惰性堆栈自动具有灵活的首选宽度,因此它们相比常规的 Stack,会自动占用更大的空间。使用惰性堆栈,在水平方向上可以使用文本周围的空白进行滚动;但如果切换到常规 Stack ,就只能在中间文本区域进行滚动,除非设置 .frame(maxWidth: .infinity)
</aside>
参见:‣
//垂直滚动
ScrollView(.vertical){ ... }
//水平滚动
ScrollView(.horizontal){ ... }
//支持两个方向滚动
ScrollView([.horizontal, .vertical]){ ... }
//隐藏滚动指示器
ScrollView(.horizontal, showsIndicators: false){...}
ScrollView
默认从顶部开始滚动,但如果您想创建像 Apple 的消息应用程序一样的 UI,您可以使用 defaultScrollAnchor()
修饰符要求滚动视图从底部开始初始锚点为 .bottom
。
ScrollView {
ForEach(0..<50) { i in
Text("Item \\(i)")
.frame(maxWidth: .infinity)
.padding()
.background(.blue)
.clipShape(.rect(cornerRadius: 25))
}
}
.defaultScrollAnchor(.bottom)
// 如果 UI 在用户不滚动的情况下发生某种变化(例如,如果出现键盘,或者您调整滚动视图的大小),使用了该修饰符的滚动位置仍将保持锚定在底部。但是,如果用户手动调整滚动位置,它将正常自由滚动。
<aside>
💡 传入的参数可以是任何 UnitPoint
,因此您可以使用 .trailing
从右边缘启动水平滚动视图,也可以使用 UI 所需的任何精确值。
</aside>
ScrollView
会自动剪辑其内容,例如子视图超出滚动视图后,子视图就看不见了。但是,如果使用 scrollClipDisabled()
修饰符,则可以覆盖此默认行为,从而允许滚动视图溢出(超出也可以看见)。
这不会影响滚动视图的触摸区域,也就是说可滚动的触摸区域还是原来那么大。(虽然有些超出区域的内容被看见,但如果用户点击这些超出滚动区域的视图,其点击实际上无法触发滚动,它只会被下面的视图接收到。)因此,最好稍微限制一下,例如允许阴影在滚动区域之外流动,而不会过多影响其他视图。
// 该示例显示了一个 VStack ,其顶部和底部具有固定文本,中间有一个滚动区域。滚动视图将在顶部文本下方整齐地对齐,但当您滚动时将溢出:
VStack {
Text("Fixed at the top")
.frame(maxWidth: .infinity)
.frame(height: 100)
.background(.green)
.foregroundStyle(.white)
ScrollView {
ForEach(0..<5) { i in
Text("Scrolling")
.frame(maxWidth: .infinity)
.frame(height: 200)
.background(.blue)
.foregroundStyle(.white)
}
}
.scrollClipDisabled()
Text("Fixed at the bottom")
.frame(maxWidth: .infinity)
.frame(height: 100)
.background(.green)
.foregroundStyle(.white)
}
// 使用 scrollClipDisabled() 时了解两件额外的事情会很有帮助:
// 您可以添加自定义剪辑形状来限制内容溢出的距离。例如,添加 padding() 然后 clipShape(.rect) 意味着您会得到一点溢出,但不是无限的。
// 由于滚动视图现在与其周围环境重叠,因此您可能需要使用 zIndex() 来调整其垂直位置。例如,如果其他视图具有默认的 Z 索引,则在滚动视图上使用 zIndex(1) 将使其子视图渲染在其他视图上。
除了通过设定参数来隐藏滚动指示器,还可以通过添加 scrollIndicators
修饰符来隐藏滚动指示器,这适用于 ScrollView
、List
等视图。
List(1..<100) { i in
Text("Row \\(i)")
}
.scrollIndicators(.hidden)
// 参数有4种选择,并且有细微的区别:
- .automatic:是在没有修饰符的情况下得到的结果 - SwiftUI 会做它认为最好的事情
- .visible:以在 iOS 上获取自动隐藏指示器,或在 macOS 上尊重用户的偏好
- .hidden:在 iOS 上隐藏指示器,并且在 macOS 上也大多隐藏它们(如果用户连接鼠标,滚动指示器将返回)
- .never:在 iOS 和 macOS 上隐藏指示器,无论用户使用什么触摸设备
使用 scrollIndicatorsFlash()
修饰符可以控制 ScrollView
或 List
的滚动指示器何时应该闪烁,这是通知用户某些部分数据已更改的好方法。该方法在 List
与 ScrollView
上都可以适用。此修饰符有两种形式:
// 1.当滚动视图指示器,首次出现时是否应该闪烁
ScrollView {
ForEach(0..<50) { i in
Text("Item \\(i)")
.frame(maxWidth: .infinity)
}
}
.scrollIndicatorsFlash(onAppear: true)
// 2.当某个观察值更改时指示器是否应该闪烁
// 您可以提供一个自定义值来跟踪指示器是否应闪烁。这可以是任何 Equatable 值,只要该值发生变化,SwiftUI 就会闪烁指示器
// 因此,您可以增加一个整数,生成一个随机 UUID ,或者只是提供布尔值在 true 和 false 之间切换都行
struct ContentView: View {
@State private var exampleState = false
var body: some View {
VStack {
ScrollView {
ForEach(0..<50) { i in
Text("Item \\(i)")
.frame(maxWidth: .infinity)
.background(.blue)
.foregroundStyle(.white)
}
}
.scrollIndicatorsFlash(trigger: exampleState)
Button("Flash!") {
exampleState.toggle()
}
}
}
}
ScrollView
的内容会在滚动方向上填充所有可用空间,而【滚动指示器】会贴着放置于屏幕边缘。使用 contentMargins()
修饰符,可以给【滚动内容】或【滚动指示器】,在指定的边上提供缩进
// 例如
ScrollView {
ForEach(0..<50) { i in
Text("Item \\(i)")
.frame(maxWidth: .infinity)
.foregroundStyle(.white)
.background(.blue)
}
}
// 这会将滚动视图的内容在每个边缘缩进 50 点,而不调整滚动指示器:
.contentMargins(50, for: .scrollContent)
// 如果只需要某些边缘,则可以指定单个值或您选择的选项集。例如,这仅在顶部边缘将内容缩进 150 点:
.contentMargins(.top, 150, for: .scrollContent)
// 这会向【滚动指示器】的顶部添加 100 点的边距,但其余部分保持不变
.contentMargins(.top, 100, for: .scrollIndicators)
// 滚动视图默认都会有一个弹跳效果,即就算内容太少不足以滚动时,也允许用户上下滑动,然后弹回来
// 如果想禁止弹跳效果,可以加上以下代码:根据内容决定是否支持弹跳
.scrollBounceBehavior(.basedOnSize)
想要实现 ScrollView 在滚动时的吸附效果(也就是停在和内部元素对齐的位置),需要两个修饰符:
.scrollTargetLayout
:应用在滚动视图的子元素上,例如 HStack
,使在 HStack
内定义的每个视图都成为【滚动目标】.scrollTargetBehavior(.viewAligned)
:应用在 ScrollView
上,它会使这个滚动视图在所有【滚动目标】之间平滑移动scrollTargetLayout()
和 .scrollTargetBehavior(.viewAligned)
两者需要一起使用.scrollTargetBehavior()
有两种滚动方式:
// 1. viewAligned 每当我们放开手时,SwiftUI 都会自动确保滑动区域,回弹在 一个(滚动目标)视图的左边缘上
ScrollView(.horizontal) {
LazyHStack {
ForEach(0..<10) { i in
RoundedRectangle(cornerRadius: 25)
.fill(Color(hue: Double(i) / 10, saturation: 1, brightness: 1).gradient)
.frame(width: 300, height: 100)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
.safeAreaPadding(.horizontal, 40)
// 2. 另一种滚动定位行为是 .paging ,它使 ScrollView 根据滚动方向恰好移动一个屏幕宽度或高度:
ScrollView {
ForEach(0..<50) { i in
Text("Item \\(i)")
.font(.largeTitle)
.frame(maxWidth: .infinity)
.frame(height: 200)
.background(.blue)
.foregroundStyle(.white)
.clipShape(.rect(cornerRadius: 20))
}
}
.scrollTargetBehavior(.paging)
scrollDismissesKeyboard
修饰符能够精确控制“当用户滚动”时,键盘应如何关闭。例如将 TextField
和 TextEditor
放入滚动视图中,并让它们都以交互方式关闭键盘,如下所示:
struct ContentView: View {
@State private var username = "Anonymous"
@State private var bio = ""
var body: some View {
ScrollView {
VStack {
TextField("Name", text: $username)
.textFieldStyle(.roundedBorder)
TextEditor(text: $bio)
.frame(height: 400)
.border(.quaternary, width: 1)
}
.padding(.horizontal)
}
.scrollDismissesKeyboard(.interactively)
}
}
可以为 scrollDismissesKeyboard()
修饰符指定4个值之一,所有这些值都有各自的用途:
.automatic
让 SwiftUI 根据当前滚动视图上下文,判断此刻最好的处理方法是什么.immediately
让键盘在发生任何滚动时,立即完全消失.interactively
使键盘与用户的手势一致地关闭(他们需要进一步滚动才能完全关闭).never
根据苹果的文档,文本编辑器应该建议使用 interactively
,而其他视图应该使用 immediately
,但目前情况似乎并非如此。