看起来在当前的工具/系统中,刚刚发布的 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
只需滚动到 id:
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
预览
Lac*_*rov 8
这是一个适用于 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
- 这是针对 SwiftUI 1.0 的最佳答案 (2认同)
谢谢 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
}
}