赞
踩
看起来在当前的工具/系统中,刚刚发布的 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 测试
- class ScrollToModel: ObservableObject {
- enum Action {
- case end
- case top
- }
- @Published var direction: Action? = nil
- }
-
- struct ContentView: View {
- @StateObject var vm = ScrollToModel()
-
- let items = (0..<200).map { $0 }
- var body: some View {
- VStack {
- HStack {
- Button(action: { vm.direction = .top }) { // < here
- Image(systemName: "arrow.up.to.line")
- .padding(.horizontal)
- }
- Button(action: { vm.direction = .end }) { // << here
- Image(systemName: "arrow.down.to.line")
- .padding(.horizontal)
- }
- }
- Divider()
-
- ScrollView {
- ScrollViewReader { sp in
- LazyVStack {
- ForEach(items, id: \.self) { item in
- VStack(alignment: .leading) {
- Text("Item \(item)").id(item)
- Divider()
- }.frame(maxWidth: .infinity).padding(.horizontal)
- }
- }.onReceive(vm.$direction) { action in
- guard !items.isEmpty else { return }
- withAnimation {
- switch action {
- case .top:
- sp.scrollTo(items.first!, anchor: .top)
- case .end:
- sp.scrollTo(items.last!, anchor: .bottom)
- default:
- return
- }
- }
- }
- }
- }
- }
- }
- }

SWIFTUI 1.0+
这是一种有效的方法的简化变体,看起来很合适,并且需要几个屏幕代码。
使用 Xcode 11.2+ / iOS 13.2+ 测试(也使用 Xcode 12b / iOS 14)
使用演示:
- struct ContentView: View {
- private let scrollingProxy = ListScrollingProxy() // proxy helper
-
- var body: some View {
- VStack {
- HStack {
- Button(action: { self.scrollingProxy.scrollTo(.top) }) { // < here
- Image(systemName: "arrow.up.to.line")
- .padding(.horizontal)
- }
- Button(action: { self.scrollingProxy.scrollTo(.end) }) { // << here
- Image(systemName: "arrow.down.to.line")
- .padding(.horizontal)
- }
- }
- Divider()
- List {
- ForEach(0 ..< 200) { i in
- Text("Item \(i)")
- .background(
- ListScrollingHelper(proxy: self.scrollingProxy) // injection
- )
- }
- }
- }
- }
- }

解决方案:
注入List
可表示的轻视图可以访问 UIKit 的视图层次结构。当List
重用行时,没有更多的值然后将行放入屏幕。
- struct ListScrollingHelper: UIViewRepresentable {
- let proxy: ListScrollingProxy // reference type
-
- func makeUIView(context: Context) -> UIView {
- return UIView() // managed by SwiftUI, no overloads
- }
-
- func updateUIView(_ uiView: UIView, context: Context) {
- proxy.catchScrollView(for: uiView) // here UIView is in view hierarchy
- }
- }
找到封闭UIScrollView
(需要做一次)然后将所需的“滚动到”操作重定向到存储的滚动视图的简单代理
- class ListScrollingProxy {
- enum Action {
- case end
- case top
- case point(point: CGPoint) // << bonus !!
- }
-
- private var scrollView: UIScrollView?
-
- func catchScrollView(for view: UIView) {
- if nil == scrollView {
- scrollView = view.enclosingScrollView()
- }
- }
-
- func scrollTo(_ action: Action) {
- if let scroller = scrollView {
- var rect = CGRect(origin: .zero, size: CGSize(width: 1, height: 1))
- switch action {
- case .end:
- rect.origin.y = scroller.contentSize.height +
- scroller.contentInset.bottom + scroller.contentInset.top - 1
- case .point(let point):
- rect.origin.y = point.y
- default: {
- // default goes to top
- }()
- }
- scroller.scrollRectToVisible(rect, animated: true)
- }
- }
- }
-
- extension UIView {
- func enclosingScrollView() -> UIScrollView? {
- var next: UIView? = self
- repeat {
- next = next?.superview
- if let scrollview = next as? UIScrollView {
- return scrollview
- }
- } while next != nil
- return nil
- }
- }

Moj*_*ini 10
scrollView.scrollTo(ROW-ID)
由于 SwiftUI 结构化设计的数据驱动,你应该知道你所有的项目 ID。所以,你可以滚动到任何ID与ScrollViewReader
来自iOS的14,并用Xcode的12
- struct ContentView: View {
- let items = (1...100)
-
- var body: some View {
- ScrollViewReader { scrollProxy in
- ScrollView {
- ForEach(items, id: \.self) { Text("\($0)"); Divider() }
- }
-
- HStack {
- Button("First!") { withAnimation { scrollProxy.scrollTo(items.first!) } }
- Button("Any!") { withAnimation { scrollProxy.scrollTo(50) } }
- Button("Last!") { withAnimation { scrollProxy.scrollTo(items.last!) } }
- }
- }
- }
- }

注意 ScrollViewReader
应该支持所有可滚动的内容,但现在只支持ScrollView
这是一个适用于 iOS13&14 的简单解决方案:
使用Introspect。
我的情况是初始滚动位置。
- ScrollView(.vertical, showsIndicators: false, content: {
- ...
- })
- .introspectScrollView(customize: { scrollView in
- scrollView.scrollRectToVisible(CGRect(x: 0, y: offset, width: 100, height: 300), animated: false)
- })
如果需要,可以根据屏幕大小或元素本身计算高度。此解决方案适用于垂直滚动。对于水平,您应该指定 x 并将 y 保留为 0
小智 7
谢谢 Asperi,很棒的提示。当在视图之外添加新条目时,我需要向上滚动列表。重新设计以适应 macOS。
我将状态/代理变量带到环境对象并在视图外使用它来强制滚动。我发现我必须更新它两次,第二次有 0.5 秒的延迟才能获得最佳结果。第一次更新可防止视图在添加行时滚动回顶部。第二次更新滚动到最后一行。我是新手,这是我的第一个 stackoverflow 帖子:o
为 MacOS 更新:
- struct ListScrollingHelper: NSViewRepresentable {
-
- let proxy: ListScrollingProxy // reference type
-
- func makeNSView(context: Context) -> NSView {
- return NSView() // managed by SwiftUI, no overloads
- }
-
- func updateNSView(_ nsView: NSView, context: Context) {
- proxy.catchScrollView(for: nsView) // here NSView is in view hierarchy
- }
- }
-
- class ListScrollingProxy {
- //updated for mac osx
- enum Action {
- case end
- case top
- case point(point: CGPoint) // << bonus !!
- }
-
- private var scrollView: NSScrollView?
-
- func catchScrollView(for view: NSView) {
- //if nil == scrollView { //unB - seems to lose original view when list is emptied
- scrollView = view.enclosingScrollView()
- //}
- }
-
- func scrollTo(_ action: Action) {
- if let scroller = scrollView {
- var rect = CGRect(origin: .zero, size: CGSize(width: 1, height: 1))
- switch action {
- case .end:
- rect.origin.y = scroller.contentView.frame.minY
- if let documentHeight = scroller.documentView?.frame.height {
- rect.origin.y = documentHeight - scroller.contentSize.height
- }
- case .point(let point):
- rect.origin.y = point.y
- default: {
- // default goes to top
- }()
- }
- //tried animations without success :(
- scroller.contentView.scroll(to: NSPoint(x: rect.minX, y: rect.minY))
- scroller.reflectScrolledClipView(scroller.contentView)
- }
- }
- }
- extension NSView {
- func enclosingScrollView() -> NSScrollView? {
- var next: NSView? = self
- repeat {
- next = next?.superview
- if let scrollview = next as? NSScrollView {
- return scrollview
- }
- } while next != nil
- return nil
- }
- }

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。