SwiftUI中ScrollViewReader的使用(自动滚动ScrollView)

发布于:2024-06-07 ⋅ 阅读:(209) ⋅ 点赞:(0)

ScrollViewReader是我最喜欢的SwiftUI新版本的新功能之一。在iOS 14发布之前,控制ScrollView的滚动位置并不容易。如果希望滚动视图滚动到特定位置,我们必须找到自己的解决方案。

使用ScrollViewReader,只需几行代码,就可以使滚动视图滚动到特定位置。本篇文章,我们将探究一下ScrollViewReader的使用。

关于ScrollView的使用,想必大家都不陌生了,先看一下下面这个示例:

struct ScrollViewReaderDemo: View {
  var body: some View {
    ScrollView {
      ForEach(0..<50) { index in
        Text("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 in
      ScrollView {
        Button("Scroll to specific item") {
          withAnimation{
            proxy.scrollTo(30, anchor: .bottom)
          }
        }

        ForEach(0..<50) { index in
          Text("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 in
        ScrollView {
          ForEach(0..<50) { index in
            Text("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 in
          if 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 in
  if let newValue {
    withAnimation {
      proxy.scrollTo(newValue, anchor: .top)
    }
  }
}

Button的事件里面修改scrollToIndex的值即可。

.toolbar(content: {
  ToolbarItem(placement: .topBarTrailing) {
    Button("Scroll") {
      scrollToIndex = 30
    }
  }
})

通过这种方法就实现了外部点击,内部ScrollView滚动的效果了。当然后,上面代码只是提供了一个实现外部触发滚动的参考,具体还得看大家的业务和设计需求了。

另外ScrollViewReader也可以和List组合使用,滚动List

写在最后

ScrollViewReaderSwiftUI框架的一个很好的补充。现在无需开发自己的解决方案,就可以轻松地指示任何滚动视图滚动到特定位置。

最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。