概览
在 SwiftUI 的开发过程中我们常说:“屏幕不够,滚动来凑”。可见滚动视图对于超长内容的呈现有着多么秉轴持钧的重要作用。
这不,从 SwiftUI 5.0(iOS 17)开始苹果又为滚动视图增加了全新的功能。但是官方的示例可能会让小伙伴们“雾里看花”、不求甚解。所以,本篇博文存在的真谛就尽在于此了!
在本篇博文中,您将学到如下内容:
- 概览
- 1. 什么是滚动目标行为(Scroll Target Behavior)?
- 2. scrollTargetLayout 视图修改器到底是干嘛用的?
- 3. 定制我们自己的 ScrollTargetBehavior 滚动目标行为
- 总结
相信学完本课后,小伙伴们一定会对 SwiftUI 5.0 中新的 scrollTargetLayout 以及 scrollTargetBehavior 修改器的含义和使用“醍醐灌顶”、如梦初醒!
那还等什么呢?让我们马上进入滚动的世界吧!
Let’s rolling!!!😉
本文对应的视频课在此,欢迎小伙伴们恣意观赏:
SwiftUI 5.0 滚动视图的滚动目标行为解惑和实战
1. 什么是滚动目标行为(Scroll Target Behavior)?
从 SwiftUI 5.0 开始,苹果为滚动视图特地新增了 scrollTargetBehavior 修改器方法:
使用它我们可以根据滚动轴来设置滚动视图的滚动目标行为(Scroll Target Behavior)。那么,什么是滚动目标行为呢?简单来说,它表示滚动视图中的滚动目标(Scroll Targets)在滚动停止时以何种方式对齐。
import SwiftUI
enum ScrollAlignType: Identifiable, CaseIterable {
case none, paging, view
var align: AnyScrollTargetBehavior {
switch self {
case .none:
.init(.viewAligned(limitBehavior: .never))
case .paging:
.init(.paging)
case .view:
.init(.viewAligned)
}
}
var title: String {
switch self {
case .none:
"无"
case .paging:
"按页面"
case .view:
"按视图"
}
}
var id: Int {
title.hashValue
}
}
struct ContentView: View {
@State private var scrollAlignType = ScrollAlignType.none
var body: some View {
ScrollView(.vertical) {
ForEach(1...100, id: \.self) { i in
Text("Item \(i)")
.font(.largeTitle.weight(.heavy))
.foregroundStyle(.white)
.frame(width: 300, height: 200)
.background {
Capsule()
.foregroundStyle(.blue.gradient)
}
}
}
.scrollTargetBehavior(scrollAlignType.align)
.padding(.vertical, 20.0)
.ignoresSafeArea()
.safeAreaInset(edge: .top) {
Picker("滚动目标行为", selection: $scrollAlignType) {
ForEach(ScrollAlignType.allCases) { alignType in
Text(alignType.title)
.tag(alignType)
}
}
.pickerStyle(.segmented)
.padding()
}
}
}
#Preview {
ContentView()
}
在上面的代码中,我们尝试用三种不同方式来对齐滚动视图中的滚动目标,它们分别是:
- 无(滚到哪是哪)
- 按页面对齐
- 按视图对齐
运行可以发现:前两种滚动对齐效果和我们的想象不谋而合,不过最后一种以视图为基准的对齐却貌似没起到什么作用,这是怎么回事呢?
2. scrollTargetLayout 视图修改器到底是干嘛用的?
原来,要想以滚动视图内部独立子元素为基准应用滚动目标行为,我们必须明确设置滚动目标(Scroll Targets),这是通过调用 scrollTargetLayout 视图修改器来实现的:
我们也可以理解为 scrollTargetLayout 方法将最外层的布局配置成了滚动目标布局。所以上面的代码我们需要做如下修正:
ScrollView(.vertical) {
ForEach(1...100, id: \.self) { i in
Text("Item \(i)")
.font(.largeTitle.weight(.heavy))
.foregroundStyle(.white)
.frame(width: 300, height: 200)
.background {
Capsule()
.foregroundStyle(.blue.gradient)
}
}
// 明确设置滚动目标
.scrollTargetLayout()
}
.scrollTargetBehavior(scrollAlignType.align)
.padding(.vertical, 20.0)
重新运行可以看到,以视图为基准的滚动对齐已然生效了:
另外,如果滚动视图中动态生成的内容需要放在额外惰性容器(比如 LazyVStack 或 LazyHStack)中,我们需要在这些容器外层应用 scrollTargetLayout() 修改器方法:
ScrollView(.vertical) {
LazyVStack {
ForEach(1...100, id: \.self) { i in
Text("Item \(i)")
.font(.largeTitle.weight(.heavy))
.foregroundStyle(.white)
.frame(width: 300, height: 200)
.background {
Capsule()
.foregroundStyle(.blue.gradient)
}
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(scrollAlignType.align)
.padding(.vertical, 20.0)
有些小伙伴们可能会问:为什么要做这种看似“多此一举”的事呢?
考虑下面这个例子,我们不希望滚动目标行为应用在滚动的头和尾视图上,所以只要在中间滚动内容上启用 scrollTargetLayout 就“水到渠成”啦:
struct AnotherExampleScrollView: View {
var body: some View {
ScrollView {
CustomHeaderView()
LazyVStack {
// 实际的滚动内容
}
.scrollTargetLayout()
CustomFooterView()
}
.scrollTargetBehavior(.viewAligned)
}
}
到目前为止(iOS 18 beta3),所有滚动目标行为相关的修改器方法都只能直接用在滚动视图(ScrollView)上,而不能用在 List 或 Form 这种内部“间接”使用滚动视图的容器上。
3. 定制我们自己的 ScrollTargetBehavior 滚动目标行为
除了使用 SwiftUI 系统默认的滚动目标行为(Scroll Target Behavior)以外,我们还可以按照实际需求创建特定的滚动对齐行为,这是通过遵循 ScrollTargetBehavior 协议来实现的:
遵循该协议只需完成一个 updateTarget 方法,在该方法传入的实参中我们可以根据当前滚动目标上下文(TargetContext)来恣意修改滚动目标(ScrollTarget)的位置等信息:
struct CustomScrollTargetBehavior: ScrollTargetBehavior {
func updateTarget(_ target: inout ScrollTarget, context: TargetContext) {
if context.velocity.dy > 0 {
target.rect.origin.y = context.originalTarget.rect.maxY
} else if context.velocity.dy < 0 {
target.rect.origin.y = context.originalTarget.rect.minY
}
}
}
extension ScrollTargetBehavior where Self == CustomScrollTargetBehavior {
static var custom: CustomScrollTargetBehavior { .init() }
}
如上代码所示,我们创建了一个定制的 CustomScrollTargetBehavior 滚动目标行为,在其中:
- 当滚动内容向上滚动时,context.velocity.dy 为正值;
- 当滚动内容向下滚动时,context.velocity.dy 为负值;
- 滚动速度越快,context.velocity.dy 绝对值越大;
从 updateTarget 的代码逻辑不难看到:我们自定义创建的这种新滚动模式,它在滚动时的阻尼感特别强。
现在,可以非常方便轻松的在滚动视图中应用我们自己的滚动目标行为啦:
ScrollView(.vertical) {
LazyVStack {
ForEach(1...100, id: \.self) { i in
Text("Item \(i)")
.font(.largeTitle.weight(.heavy))
.foregroundStyle(.white)
.frame(width: 300, height: 200)
.background {
Capsule()
.foregroundStyle(.blue.gradient)
}
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.custom)
.padding(.vertical, 20.0)
最后运行一下代码,看看新的滚动效果吧:
利用 SwiftUI 5.0(iOS 17.0)中新的滚动目标行为机制,我们可以逍遥物外的自由定制滚动视图的滚动对齐模式啦!棒棒哒!💯
总结
在本篇博文中,我们讨论了什么是 SwiftUI 5.0(iOS 17.0)中新增的滚动目标行为(Target Behavior),并且介绍了如何游刃有余应用它们,我们在最后还创建了定制的滚动目标行为让自由度更加“出谷迁乔”。
感谢观赏,再会啦!😎