当前位置:   article > 正文

如何在 SwiftUI 中以编程方式滚动列表?_swiftui scrollviewreader scrollto

swiftui scrollviewreader scrollto

看起来在当前的工具/系统中,刚刚发布的 Xcode 11.4/iOS 13.4 中将没有 SwiftUI 原生支持“滚动到”功能List。因此,即使他们,Apple,将在下一个主要版本中提供它,我也需要对 iOS 13.x 的向后支持。

那么我将如何以最简单和轻松的方式做到这一点?

  • 滚动列表结束
  • 滚动列表到顶部
  • 和别的

(我不喜欢像之前在 SO 上提出的那样将完整的UITableView基础设施包装起来UIViewRepresentable/UIViewControllerRepresentable)。

SWIFTUI 2.0

这是 Xcode 12 / iOS 14 (SwiftUI 2.0) 中可能的替代解决方案,当滚动控件位于滚动区域之外时,可以在同一场景中使用(因为 SwiftUI2ScrollViewReader只能在内部使用 ScrollView

注意:行内容设计不在考虑范围内

使用 Xcode 12b / iOS 14 测试

  1. class ScrollToModel: ObservableObject {
  2. enum Action {
  3. case end
  4. case top
  5. }
  6. @Published var direction: Action? = nil
  7. }
  8. struct ContentView: View {
  9. @StateObject var vm = ScrollToModel()
  10. let items = (0..<200).map { $0 }
  11. var body: some View {
  12. VStack {
  13. HStack {
  14. Button(action: { vm.direction = .top }) { // < here
  15. Image(systemName: "arrow.up.to.line")
  16. .padding(.horizontal)
  17. }
  18. Button(action: { vm.direction = .end }) { // << here
  19. Image(systemName: "arrow.down.to.line")
  20. .padding(.horizontal)
  21. }
  22. }
  23. Divider()
  24. ScrollView {
  25. ScrollViewReader { sp in
  26. LazyVStack {
  27. ForEach(items, id: \.self) { item in
  28. VStack(alignment: .leading) {
  29. Text("Item \(item)").id(item)
  30. Divider()
  31. }.frame(maxWidth: .infinity).padding(.horizontal)
  32. }
  33. }.onReceive(vm.$direction) { action in
  34. guard !items.isEmpty else { return }
  35. withAnimation {
  36. switch action {
  37. case .top:
  38. sp.scrollTo(items.first!, anchor: .top)
  39. case .end:
  40. sp.scrollTo(items.last!, anchor: .bottom)
  41. default:
  42. return
  43. }
  44. }
  45. }
  46. }
  47. }
  48. }
  49. }
  50. }

SWIFTUI 1.0+

这是一种有效的方法的简化变体,看起来很合适,并且需要几个屏幕代码。

使用 Xcode 11.2+ / iOS 13.2+ 测试(也使用 Xcode 12b / iOS 14)

使用演示:

  1. struct ContentView: View {
  2. private let scrollingProxy = ListScrollingProxy() // proxy helper
  3. var body: some View {
  4. VStack {
  5. HStack {
  6. Button(action: { self.scrollingProxy.scrollTo(.top) }) { // < here
  7. Image(systemName: "arrow.up.to.line")
  8. .padding(.horizontal)
  9. }
  10. Button(action: { self.scrollingProxy.scrollTo(.end) }) { // << here
  11. Image(systemName: "arrow.down.to.line")
  12. .padding(.horizontal)
  13. }
  14. }
  15. Divider()
  16. List {
  17. ForEach(0 ..< 200) { i in
  18. Text("Item \(i)")
  19. .background(
  20. ListScrollingHelper(proxy: self.scrollingProxy) // injection
  21. )
  22. }
  23. }
  24. }
  25. }
  26. }

解决方案:

注入List可表示的轻视图可以访问 UIKit 的视图层次结构。当List重用行时,没有更多的值然后将行放入屏幕。

  1. struct ListScrollingHelper: UIViewRepresentable {
  2. let proxy: ListScrollingProxy // reference type
  3. func makeUIView(context: Context) -> UIView {
  4. return UIView() // managed by SwiftUI, no overloads
  5. }
  6. func updateUIView(_ uiView: UIView, context: Context) {
  7. proxy.catchScrollView(for: uiView) // here UIView is in view hierarchy
  8. }
  9. }

找到封闭UIScrollView(需要做一次)然后将所需的“滚动到”操作重定向到存储的滚动视图的简单代理

  1. class ListScrollingProxy {
  2. enum Action {
  3. case end
  4. case top
  5. case point(point: CGPoint) // << bonus !!
  6. }
  7. private var scrollView: UIScrollView?
  8. func catchScrollView(for view: UIView) {
  9. if nil == scrollView {
  10. scrollView = view.enclosingScrollView()
  11. }
  12. }
  13. func scrollTo(_ action: Action) {
  14. if let scroller = scrollView {
  15. var rect = CGRect(origin: .zero, size: CGSize(width: 1, height: 1))
  16. switch action {
  17. case .end:
  18. rect.origin.y = scroller.contentSize.height +
  19. scroller.contentInset.bottom + scroller.contentInset.top - 1
  20. case .point(let point):
  21. rect.origin.y = point.y
  22. default: {
  23. // default goes to top
  24. }()
  25. }
  26. scroller.scrollRectToVisible(rect, animated: true)
  27. }
  28. }
  29. }
  30. extension UIView {
  31. func enclosingScrollView() -> UIScrollView? {
  32. var next: UIView? = self
  33. repeat {
  34. next = next?.superview
  35. if let scrollview = next as? UIScrollView {
  36. return scrollview
  37. }
  38. } while next != nil
  39. return nil
  40. }
  41. }


Moj*_*ini  10

只需滚动到 id:

scrollView.scrollTo(ROW-ID)

由于 SwiftUI 结构化设计的数据驱动,你应该知道你所有的项目 ID。所以,你可以滚动到任何ID与ScrollViewReader来自iOS的14,并用Xcode的12

  1. struct ContentView: View {
  2. let items = (1...100)
  3. var body: some View {
  4. ScrollViewReader { scrollProxy in
  5. ScrollView {
  6. ForEach(items, id: \.self) { Text("\($0)"); Divider() }
  7. }
  8. HStack {
  9. Button("First!") { withAnimation { scrollProxy.scrollTo(items.first!) } }
  10. Button("Any!") { withAnimation { scrollProxy.scrollTo(50) } }
  11. Button("Last!") { withAnimation { scrollProxy.scrollTo(items.last!) } }
  12. }
  13. }
  14. }
  15. }

注意 ScrollViewReader应该支持所有可滚动的内容,但现在只支持ScrollView


预览


Lac*_*rov  8

这是一个适用于 iOS13&14 的简单解决方案:
使用Introspect
我的情况是初始滚动位置。

  1. ScrollView(.vertical, showsIndicators: false, content: {
  2. ...
  3. })
  4. .introspectScrollView(customize: { scrollView in
  5. scrollView.scrollRectToVisible(CGRect(x: 0, y: offset, width: 100, height: 300), animated: false)
  6. })

如果需要,可以根据屏幕大小或元素本身计算高度。此解决方案适用于垂直滚动。对于水平,您应该指定 x 并将 y 保留为 0

  • 这是针对 SwiftUI 1.0 的最佳答案 (2认同)

小智  7

谢谢 Asperi,很棒的提示。当在视图之外添加新条目时,我需要向上滚动列表。重新设计以适应 macOS。

我将状态/代理变量带到环境对象并在视图外使用它来强制滚动。我发现我必须更新它两次,第二次有 0.5 秒的延迟才能获得最佳结果。第一次更新可防止视图在添加行时滚动回顶部。第二次更新滚动到最后一行。我是新手,这是我的第一个 stackoverflow 帖子:o

为 MacOS 更新:

  1. struct ListScrollingHelper: NSViewRepresentable {
  2. let proxy: ListScrollingProxy // reference type
  3. func makeNSView(context: Context) -> NSView {
  4. return NSView() // managed by SwiftUI, no overloads
  5. }
  6. func updateNSView(_ nsView: NSView, context: Context) {
  7. proxy.catchScrollView(for: nsView) // here NSView is in view hierarchy
  8. }
  9. }
  10. class ListScrollingProxy {
  11. //updated for mac osx
  12. enum Action {
  13. case end
  14. case top
  15. case point(point: CGPoint) // << bonus !!
  16. }
  17. private var scrollView: NSScrollView?
  18. func catchScrollView(for view: NSView) {
  19. //if nil == scrollView { //unB - seems to lose original view when list is emptied
  20. scrollView = view.enclosingScrollView()
  21. //}
  22. }
  23. func scrollTo(_ action: Action) {
  24. if let scroller = scrollView {
  25. var rect = CGRect(origin: .zero, size: CGSize(width: 1, height: 1))
  26. switch action {
  27. case .end:
  28. rect.origin.y = scroller.contentView.frame.minY
  29. if let documentHeight = scroller.documentView?.frame.height {
  30. rect.origin.y = documentHeight - scroller.contentSize.height
  31. }
  32. case .point(let point):
  33. rect.origin.y = point.y
  34. default: {
  35. // default goes to top
  36. }()
  37. }
  38. //tried animations without success :(
  39. scroller.contentView.scroll(to: NSPoint(x: rect.minX, y: rect.minY))
  40. scroller.reflectScrolledClipView(scroller.contentView)
  41. }
  42. }
  43. }
  44. extension NSView {
  45. func enclosingScrollView() -> NSScrollView? {
  46. var next: NSView? = self
  47. repeat {
  48. next = next?.superview
  49. if let scrollview = next as? NSScrollView {
  50. return scrollview
  51. }
  52. } while next != nil
  53. return nil
  54. }
  55. }

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/一键难忘520/article/detail/780910
推荐阅读
相关标签
  

闽ICP备14008679号