ScrollViewReader
是我最喜欢的SwiftUI
新版本的新功能之一。在iOS 14发布之前,控制ScrollView
的滚动位置并不容易。如果希望滚动视图滚动到特定位置,我们必须找到自己的解决方案。
使用ScrollViewReader
,只需几行代码,就可以使滚动视图滚动到特定位置。本篇文章,我们将探究一下ScrollViewReader
的使用。
关于ScrollView
的使用,想必大家都不陌生了,先看一下下面这个示例:
struct ScrollViewReaderDemo: View {var body: some View {ScrollView {ForEach(0..<50) { index inText("This is item \(index)").font(.headline).frame(height: 150).frame(maxWidth: .infinity).background(Color.white).cornerRadius(10).shadow(radius: 10).padding()}}}
}
上面是一个比较简单的ScrollView
使用方法,我们可以手动滑动界面。如果我们想要代码控制滚到哪里,或者是满足某个条件后,自动滚动,那如何处理呢,以前在UIKit中,我们能持有UIScrollView
的实例变量,然后调用相关方法,那么在SwiftUI
中有这么一个实例变量吗?
答案是肯定有的,那就是使用ScrollViewReader
。
ScrollViewReader
是通过使用代理来滚动到已知的子视图,从而提供程序化滚动的视图。
ScrollViewReader
的构造闭包里面返回了一个ScrollViewProxy
类型的实例对象,通过这个对象调用scrollTo(_:anchor:)
方法实现滚动。
func scrollTo<ID>(_ id: ID,anchor: UnitPoint? = nil
) where ID : Hashable
id
: 子视图的唯一标识。
anchor
: 滚动动作的对齐行为。
如果anchor
为nil,则此方法会找到已标识视图,并滚动最小值以使已标识视图完全可见。
如果anchor
非nil,它将定义已标识视图和滚动视图中要对齐的点。例如,将anchor
设置为top
将标识视图的顶部与滚动视图的顶部对齐。类似地,将anchor
设置为bottom
将标识视图的底部与滚动视图的底部对齐,依此类推。
下面的代码中,添加了一个Button
,点击后将ScrollView
滚动指定的位置,尤其要注意的是记得给每个子视图添加id
修饰符,要不然滚动的时候就找不到指定视图了。
struct ScrollViewReaderDemo: View {var body: some View {ScrollViewReader { proxy inScrollView {Button("Scroll to specific item") {withAnimation{proxy.scrollTo(30, anchor: .bottom)}}ForEach(0..<50) { index inText("This is item \(index)").font(.headline).frame(height: 150).frame(maxWidth: .infinity).background(Color.white).cornerRadius(10).shadow(radius: 10).padding().id(index)}}}}
}
上面的代码能够轻松的实现ScrollView
的滚动,ScrollViewReader闭包返回的proxy
代理了ScrollView
,进而操作滚动,问题来了,proxy
的作用域只在ScrollViewReader
闭包内,如果点击滚动的按钮在外面,或者在导航栏上,那该如何实现呢?
struct ScrollViewReaderDemo: View {@State private var scrollToIndex: Int?var body: some View {NavigationStack {ScrollViewReader { proxy inScrollView {ForEach(0..<50) { index inText("This is item \(index)").font(.headline).frame(height: 150).frame(maxWidth: .infinity).background(Color.white).cornerRadius(10).shadow(radius: 10).padding().id(index)}}.onChange(of: scrollToIndex) { newValue inif let newValue {withAnimation {proxy.scrollTo(newValue, anchor: .top)}}}}.navigationTitle("Title").navigationBarTitleDisplayMode(.inline).toolbar(content: {ToolbarItem(placement: .topBarTrailing) {Button("Scroll") {scrollToIndex = 30}}})}}
}
上面代码中将点击滚动的按钮放到了导航栏的右侧,这个时候在Button
的点击事件内已经访问不到proxy
代理了。
虽然访问不到了,但是我们可以通过一个状态变量(@State
修饰的变量)的变化来触发滚动。
@State private var scrollToIndex: Int?
然后在能访问到proxy
代理的区域内监听scrollToIndex
的变化,然后滚动视图。代码中给ScrollView
添加了onChange
修饰符,并添加观察对象scrollToIndex
,当scrollToIndex
变化的时候onChange
修饰符闭包会被触发,并返回最新的scrollToIndex
的值。
.onChange(of: scrollToIndex) { newValue inif let newValue {withAnimation {proxy.scrollTo(newValue, anchor: .top)}}
}
在Button
的事件里面修改scrollToIndex
的值即可。
.toolbar(content: {ToolbarItem(placement: .topBarTrailing) {Button("Scroll") {scrollToIndex = 30}}
})
通过这种方法就实现了外部点击,内部ScrollView
滚动的效果了。当然后,上面代码只是提供了一个实现外部触发滚动的参考,具体还得看大家的业务和设计需求了。
另外ScrollViewReader
也可以和List
组合使用,滚动List
。
写在最后
ScrollViewReader
是SwiftUI
框架的一个很好的补充。现在无需开发自己的解决方案,就可以轻松地指示任何滚动视图滚动到特定位置。
最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。