在当前发布的Xcode 11.4 / iOS 13.4的当前工具/系统中,List
中不支持SwiftUI本地支持“ scroll-to”功能。因此,即使他们(苹果公司)将在下一个 major 版本中提供它,我也需要向后支持iOS 13.x。
那么我将如何以最简单,最轻松的方式来做到这一点?
(我不喜欢将SO UITableView
的完整基础结构包装到UIViewRepresentable/UIViewControllerRepresentable
中)。
答案 0 :(得分:31)
SWIFTUI 2.0
这里是Xcode 12 / iOS 14(SwiftUI 2.0)中的替代解决方案,当滚动控件位于滚动区域之外时,可以在相同情况下使用该解决方案(因为SwiftUI2 ScrollViewReader
仅在内部使用 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
的简单代理(需要执行一次),然后将所需的“ scroll-to”操作重定向到该存储的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
}
}
答案 1 :(得分:5)
非常感谢Asperi。当在视图外部添加新条目时,我需要向上滚动列表。重新设计以适合macOS。
我将state / proxy变量带到一个环境对象中,并在视图外部使用它来强制滚动。我发现我不得不更新两次,第二次以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
}
}
答案 2 :(得分:5)
由于SwiftUI采用结构化设计的“数据驱动”,因此您应该知道所有项目ID。因此,您可以使用 iOS 14 中的ScrollViewReader
和 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
答案 3 :(得分:2)
这是一个适用于iOS13&14的简单解决方案: 我的情况是初始滚动位置。
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
答案 4 :(得分:1)
现在可以使用Xcode 12中的所有新ScrollViewProxy
来简化此操作,就像这样:
struct ContentView: View {
let itemCount: Int = 100
var body: some View {
ScrollViewReader { value in
VStack {
Button("Scroll to top") {
value.scrollTo(0)
}
Button("Scroll to buttom") {
value.scrollTo(itemCount-1)
}
ScrollView {
LazyVStack {
ForEach(0 ..< itemCount) { i in
Text("Item \(i)")
.frame(height: 50)
.id(i)
}
}
}
}
}
}
}
答案 5 :(得分:0)
MacOS 11:如果您需要基于视图层次结构外部的输入来滚动列表。我已经使用新的scrollViewReader遵循了原始的滚动代理模式:
struct ScrollingHelperInjection: NSViewRepresentable {
let proxy: ScrollViewProxy
let helper: ScrollingHelper
func makeNSView(context: Context) -> NSView {
return NSView()
}
func updateNSView(_ nsView: NSView, context: Context) {
helper.catchProxy(for: proxy)
}
}
final class ScrollingHelper {
//updated for mac os v11
private var proxy: ScrollViewProxy?
func catchProxy(for proxy: ScrollViewProxy) {
self.proxy = proxy
}
func scrollTo(_ point: Int) {
if let scroller = proxy {
withAnimation() {
scroller.scrollTo(point)
}
} else {
//problem
}
}
}
环境对象:
@Published var scrollingHelper = ScrollingHelper()
在视图中:ScrollViewReader { reader in .....
视图中的注入:
.background(ScrollingHelperInjection(proxy: reader, helper: scrollingHelper)
视图层次结构之外的用法:scrollingHelper.scrollTo(3)
答案 6 :(得分:0)
如@ lachezar-todorov的回答Introspect中所述,它是一个不错的库,可以访问UIKit
中的SwiftUI
元素。但是请注意,您多次访问用于访问UIKit
元素的块。这确实会弄乱您的应用程序状态。在我的cas中,CPU使用率达到100%,应用程序变得无响应。我必须使用一些先决条件来避免它。
ScrollView() {
...
}.introspectScrollView { scrollView in
if aPreCondition {
//Your scrolling logic
}
}
答案 7 :(得分:0)
我的两分钱用于根据其他逻辑在任何时候删除和重新定位列表..例如在删除之后,敌人的例子会进入顶部。 (这是一个超简化的示例,我习惯在网络回调后重新定位:网络调用后我更改 previousIndex )
结构内容视图:视图{
@State private var previousIndex : Int? = nil
@State private var items = Array(0...100)
func removeRows(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
self.previousIndex = offsets.first
}
var body: some View {
ScrollViewReader { (proxy: ScrollViewProxy) in
List{
ForEach(items, id: \.self) { Text("\($0)")
}.onDelete(perform: removeRows)
}.onChange(of: previousIndex) { (e: Equatable) in
proxy.scrollTo(previousIndex!-4, anchor: .top)
//proxy.scrollTo(0, anchor: .top) // will display 1st cell
}
}
}
}