如何在SwiftUI中以编程方式滚动列表? [英] How to scroll List programmatically in SwiftUI?
问题描述
在当前的工具/系统中,就像刚刚发布的Xcode 11.4/iOS 13.4一样,List
中将没有SwiftUI原生支持"scroll-to"功能.因此,即使他们(苹果公司)将在下一个 major 版本中提供它,我也需要向后支持iOS 13.x.
It looks like in current tools/system, just released Xcode 11.4 / iOS 13.4, there will be no SwiftUI-native support for "scroll-to" feature in List
. So even if they, Apple, will provide it in next major released, I will need backward support for iOS 13.x.
那么,在最简单的&中,我将如何做?光明的方式?
So how would I do it in most simple & light way?
- 滚动列表到结束
- 将列表滚动到顶部
- 和其他人
(我不喜欢像之前在SO上所提出的那样将完整的UITableView
基础结构包装到UIViewRepresentable/UIViewControllerRepresentable
中.)
(I don't like wrapping full UITableView
infrastructure into UIViewRepresentable/UIViewControllerRepresentable
as was proposed earlier on SO).
推荐答案
SWIFTUI 2.0
这里是Xcode 12/iOS 14(SwiftUI 2.0)中的替代解决方案,当滚动控件不在滚动区域之外时,可以在同一场景中使用(因为SwiftUI2 ScrollViewReader
只能在内部使用 ScrollView
)
Here is possible alternate solution in Xcode 12 / iOS 14 (SwiftUI 2.0) that can be used in same scenario when controls for scrolling is outside of scrolling area (because SwiftUI2 ScrollViewReader
can be used only inside ScrollView
)
注意:行内容设计不在考虑范围之内
通过Xcode 12b/iOS 14测试
Tested with 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 +
这是该方法的简化变体,可以起作用,看起来合适并且需要几个屏幕代码.
Here is simplified variant of approach that works, looks appropriate, and takes a couple of screens code.
已在Xcode 11.2 +/iOS 13.2+(也已在Xcode 12b/iOS 14)上进行了测试
Tested with Xcode 11.2+ / iOS 13.2+ (also with 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
重用行时,没有更多的值可将行放入屏幕.
Light view representable being injected into List
gives access to UIKit's view hierarchy. As List
reuses rows there are no more values then fit rows into screen.
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
的封闭的简单代理(需要执行一次),然后将所需的滚动到"重定向到该存储的滚动视图的操作
Simple proxy that finds enclosing UIScrollView
(needed to do once) and then redirects needed "scroll-to" actions to that stored scrollview
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
}
}
这篇关于如何在SwiftUI中以编程方式滚动列表?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!