SwiftUI 5.0(iOS 17)滚动视图的滚动目标行为(Target Behavior)解惑和实战

概览

在 SwiftUI 的开发过程中我们常说:"屏幕不够,滚动来凑"。可见滚动视图对于超长内容的呈现有着多么秉轴持钧的重要作用。

这不,从 SwiftUI 5.0(iOS 17)开始苹果又为滚动视图增加了全新的功能。但是官方的示例可能会让小伙伴们"雾里看花"、不求甚解。所以,本篇博文存在的真谛就尽在于此了!

在本篇博文中,您将学到如下内容:

  • 概览
  • [1. 什么是滚动目标行为(Scroll Target Behavior)?](#1. 什么是滚动目标行为(Scroll Target Behavior)?)
  • [2. scrollTargetLayout 视图修改器到底是干嘛用的?](#2. scrollTargetLayout 视图修改器到底是干嘛用的?)
  • [3. 定制我们自己的 ScrollTargetBehavior 滚动目标行为](#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)在滚动停止时以何种方式对齐。

swift 复制代码
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()
}

在上面的代码中,我们尝试用三种不同方式来对齐滚动视图中的滚动目标,它们分别是:

  1. 无(滚到哪是哪)
  2. 按页面对齐
  3. 按视图对齐

运行可以发现:前两种滚动对齐效果和我们的想象不谋而合,不过最后一种以视图为基准的对齐却貌似没起到什么作用,这是怎么回事呢?

2. scrollTargetLayout 视图修改器到底是干嘛用的?

原来,要想以滚动视图内部独立子元素为基准应用滚动目标行为,我们必须明确设置滚动目标(Scroll Targets),这是通过调用 scrollTargetLayout 视图修改器来实现的:

我们也可以理解为 scrollTargetLayout 方法将最外层的布局配置成了滚动目标布局。所以上面的代码我们需要做如下修正:

swift 复制代码
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() 修改器方法:

swift 复制代码
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 就"水到渠成"啦:

swift 复制代码
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)的位置等信息:

swift 复制代码
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 的代码逻辑不难看到:我们自定义创建的这种新滚动模式,它在滚动时的阻尼感特别强。

现在,可以非常方便轻松的在滚动视图中应用我们自己的滚动目标行为啦:

swift 复制代码
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),并且介绍了如何游刃有余应用它们,我们在最后还创建了定制的滚动目标行为让自由度更加"出谷迁乔"。

感谢观赏,再会啦!😎

相关推荐
大熊猫侯佩4 个月前
SwiftUI 6.0(iOS 18.0)滚动视图新增的滚动阶段(Scroll Phase)监听功能趣谈
scrollview·ios 18·xcode 16·swiftui 6.0·滚动视图·scroll phase·监听滚动状态
大熊猫侯佩4 个月前
SwiftUI 6.0(iOS 18)ScrollView 全新的滚动位置(ScrollPosition)揭秘
scrollview·ios 18·swiftui 6.0·ipados 18·滚动视图·wwdc24·scrollposition
_大猪4 个月前
Unity的ScrollView滚动视图复用
unity·优化·scrollview·复用·滚动视图
大熊猫侯佩5 个月前
SwiftUI 5.0(iOS 17)进一步定制 TipKit 外观让撸码如虎添翼
swiftui 5.0·ios 17.0·tipkit·tip·tipviewstyle·自定义 tip 样式·全局调控 tip 外观
大熊猫侯佩7 个月前
SwiftUI 5.0(iOS 17.0)触摸反馈“震荡波”与触发器模式趣谈
触发器·swiftui 5.0·trigger·ios 17.0·haptic·震动反馈·sensoryfeedback
吕氏春秋i10 个月前
Android NestedScrollView悬浮固定顶部
android·悬浮·scrollview·android滑动·吸顶
大漠知秋1 年前
ScrollView 与 SliverPadding 的关系
flutter·scrollview·listview
Songlcy1 年前
react-native 0.63 适配 Xcode 15 iOS 17.0+
react native·simulator·ios 17·xcode 15
大熊猫侯佩1 年前
Swift 5.9 与 SwiftUI 5.0 中新 Observation 框架应用之深入浅出
swift 5.9·swiftui 5.0·observation·observable·bindable·可观察对象·可变对象