介绍
ScrollView 即滚动视图,在 iOS 开发中扮演着非常重要的角色。但在 SwiftUI 的发展史上,ScrollView 一直处于“残废”的状态,直到 SwiftUI 6.0 才逐渐补齐短板。下面详细讲解 SwiftUI 中 ScrollView 的进化史。
SwiftUI 1.0
可以垂直与水平滚动。
import SwiftUI
struct ContentView: View {
var body: some View {
// 参数1:滚动方向,参数2:是否显示滚动条,参数3:滚动内容
ScrollView(.vertical, showsIndicators: false) {
Text("SwiftUI").padding(20)
Divider()
Rectangle()
.foregroundColor(.orange)
.frame(width: UIScreen.main.bounds.midX, height: 1500, alignment: .center)
Divider()
Text("Example")
}
}
}
SwiftUI 2.0
支持同时横向与纵向滚动。
import SwiftUI
struct ContentView: View {
var body: some View {
ScrollView([.vertical, .horizontal], showsIndicators: false) {
Text("SwiftUI").padding(20)
Divider()
Rectangle()
.foregroundColor(.orange)
.frame(width: 1500, height: 1500, alignment: .center)
Divider()
Text("Example")
}
}
}
SwiftUI 3.0
支持设置safeAreaInset
。
import SwiftUI
struct ContentView: View {
var body: some View {
ScrollView {
Color
.orange
.frame(width: UIScreen.main.bounds.width, height: 1500)
}
.safeAreaInset(edge: .top) { // 顶部contentInset
VStack {
Text("顶部")
Divider()
.frame(height: 5)
.background(.blue)
}
.background(.bar) // 选择bar
}
.safeAreaInset(edge: .bottom) { // 底部contentInset
VStack {
Divider()
.frame(height: 5)
.background(.blue)
Text("底部")
}
.background(.green) // 选择颜色
}
}
}
SwiftUI 4.0
- 可以控制是否可以滚动。
- 可以控制滚动时键盘的隐藏方式。
- 可以控制滚动条的显隐。
import SwiftUI
struct ContentView: View {
@State private var name: String = "ZhangSan"
var body: some View {
ScrollView {
TextField("请输入用户名", text: $name)
.textFieldStyle(.roundedBorder)
.padding()
Color
.orange
.frame(height: 1500)
}
.scrollDisabled(false)
.scrollDismissesKeyboard(.immediately)
.scrollIndicators(.visible)
}
}
SwiftUI 5.0
- 新增
scrollTargetBehavior
修饰符,用于实现滚动时的行为,如对齐、分页。 - 新增
scrollTargetLayout
修饰符,用于告知 ScrollView 偏移的参照者,常用于修饰 ScrollView 中的各种 Stack。
import SwiftUI
struct ContentView: View {
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(0 ..< 10) { i in
RoundedRectangle(cornerRadius: 25)
.fill(Color(hue: Double(i) / 10, saturation: 1, brightness: 1).gradient)
.frame(width: 300, height: 300)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging) // 分页
.padding()
}
}
- 新增
scrollPosition
修饰符,用于设置内容的偏移与滚动后内容停留的位置。
import SwiftUI
struct ContentView: View {
@State private var scrolledID: Int? = 0
var body: some View {
VStack {
Button("Scroll") {
withAnimation(.smooth) {
scrolledID = Int.random(in: 1 ..< 100)
}
}
ScrollView {
ForEach(1 ..< 100, id: \.self) { number in
Text(number.formatted())
.font(.title)
.foregroundColor(.white)
.frame(width: 300, height: 180)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.purple))
}
}
.scrollTargetLayout()
}
.scrollPosition(id: $scrolledID)
}
}
- 新增
scrollTransition
修饰符,用于设置滚动时的效果。
import SwiftUI
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(1 ..< 100, id: \.self) { number in
Text(number.formatted())
.font(.title)
.foregroundColor(.white)
.frame(width: 300, height: 300)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.purple))
.scrollTransition(topLeading: .animated, bottomTrailing: .interactive) { view, phase in
view
.scaleEffect(phase != .identity ? 0.6 : 1)
.opacity(phase != .identity ? 0.2 : 1)
}
}
}
}
}
- 新增
scrollClipDisabled
修饰符,防止超出范围的内容被裁切。
import SwiftUI
struct ContentView: View {
let colors: [Color] = [.red, .orange, .yellow, .green, .teal, .blue, .purple]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
ForEach(colors, id: \.self) { color in
Rectangle()
.frame(width: 100, height: 100)
.foregroundStyle(color)
.shadow(color: .black.opacity(0.6), radius: 20)
}
}
}
.scrollClipDisabled(true)
.padding()
}
}
- 新增
scrollIndicatorsFlash
修饰符,用于使滚动指示器闪现一下。
import SwiftUI
struct ContentView: View {
@State private var showFlash = false
var body: some View {
HStack {
Button("Flash") {
withAnimation(.smooth) {
showFlash.toggle()
}
}
ScrollView {
ForEach(1 ..< 100, id: \.self) { number in
Text(number.formatted())
.font(.title)
.foregroundColor(.white)
.frame(width: 300, height: 180)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.purple))
}
}
.scrollIndicatorsFlash(trigger: showFlash)
}
}
}
- 新增
defaultScrollAnchor
修饰符,用于设置内容的默认显示位置。
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
// 默认从1开始显示
ScrollView(.horizontal) {
HStack {
ForEach(1 ..< 100, id: \.self) { number in
Text(number.formatted())
.font(.title)
.foregroundColor(.white)
.frame(width: 60, height: 60)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.purple))
}
}
}
// 默认从99开始显示
ScrollView(.horizontal) {
HStack {
ForEach(1 ..< 100, id: \.self) { number in
Text(number.formatted())
.font(.title)
.foregroundColor(.white)
.frame(width: 60, height: 60)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.green))
}
}
}
.defaultScrollAnchor(.trailing)
}
}
SwiftUI 6.0
- 新增 ScrollPosition 类型,配合
scrollPosition
修饰符,可以设置滚动后内容停留的位置。
import SwiftUI
struct ContentView: View {
@State private var position: ScrollPosition = .init(idType: Int.self)
var body: some View {
VStack {
HStack(spacing: 20) {
Button("Scroll") {
withAnimation(.smooth) {
position.scrollTo(id: 66, anchor: .center) // 改变ScrollPosition即可滚动到指定的内容位置
}
}
Button("Scroll Offset") {
withAnimation(.smooth) {
position.scrollTo(point: CGPoint(x: 0, y: 1000)) // 跳转到某个位置点
position.scrollTo(x: 0) // 也可以单独指定x与y
position.scrollTo(y: 10000) // 也可以单独指定x与y
}
}
Button("Top") {
withAnimation(.smooth) {
position.scrollTo(edge: .top) // 回到顶部
}
}
Button("Bottom") {
withAnimation(.smooth) {
position.scrollTo(edge: .bottom) // 回到底部
}
}
}
ScrollView {
ForEach(1 ..< 100, id: \.self) { number in
Text(number.formatted())
.font(.title)
.foregroundColor(.white)
.frame(width: 300, height: 180)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.purple))
}
}
.scrollPosition($position) // 传入ScrollPosition类型
.animation(.default, value: position)
}
}
}
- 新增
onScrollPhaseChange
修饰符,用于监听 ScrollView 状态的改变。其中状态的类型为ScrollPhase
,共有 5 种状态,分别为idle
、tracking
、interacting
、decelerating
与animating
。
import SwiftUI
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(1 ..< 100, id: \.self) { number in
Text(number.formatted())
.font(.title)
.foregroundColor(.white)
.frame(width: 300, height: 180)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.purple))
}
}
.onScrollPhaseChange { oldPhase, newPhase, context in
switch newPhase {
case .idle:
print("newPhase idle") // 未移动或者未交互
case .tracking:
print("newPhase tracking") // 滚动事件即将发生
case .interacting:
print("newPhase interacting") // 滚动但手指未离开
case .decelerating:
print("newPhase decelerating") // 加速松开手指,滚动减速
case .animating:
print("newPhase animating") // 向一个特定View移动
}
if newPhase.isScrolling {
print("scrolling")
}
}
}
}
- 新增
onScrollGeometryChange
修饰符,用于监听 ScrollGeometry 的改变。通过 ScrollGeometry 可以获取 ScrollView 当前的bounds
、containerSize
、contentInsets
、contentOffset
、contentSize
以及visibleRect
。
import SwiftUI
struct ContentView: View {
@State private var offset: CGFloat = 0
var body: some View {
ZStack(alignment: .top) {
Rectangle()
.fill(Color.red)
.frame(height: 300 + max(0, -offset))
.transformEffect(.init(translationX: 0, y: -max(0, offset)))
ScrollView {
Rectangle()
.fill(Color.clear)
.frame(height: 300)
Text("\(offset)")
ForEach(1 ..< 100, id: \.self) { number in
Text(number.formatted())
.font(.title)
}
}
.onScrollGeometryChange(for: CGFloat.self, of: { geometry in
geometry.contentOffset.y + geometry.contentInsets.top
}, action: { _, newValue in
offset = newValue
})
}
}
}
- 新增
onScrollVisibilityChange
修饰符,用于监听 ScrollView 可视内容的改变。
import SwiftUI
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(1 ..< 100, id: \.self) { number in
Text(number.formatted())
.font(.title)
.foregroundColor(.white)
.frame(width: 300, height: 180)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.purple))
.onScrollVisibilityChange(threshold: 0.5) { status in
if status {
print("\(number) is Visible")
}
}
}
}
}
}