¿Cómo desplazar la lista mediante programación en SwiftUI?
Parece que en las herramientas/sistema actuales, recién lanzado Xcode 11.4/iOS 13.4, no habrá soporte nativo de SwiftUI para la función "desplazarse hacia" en List
. Entonces, incluso si ellos, Apple, lo proporcionan en el próximo lanzamiento importante , necesitaré soporte retroactivo para iOS 13.x.
Entonces, ¿cómo lo haría de la forma más sencilla y ligera?
- desplazarse por la lista para finalizar
- desplazar la lista hacia arriba
- y otros
(No me gusta incluir UITableView
la infraestructura completa UIViewRepresentable/UIViewControllerRepresentable
como se propuso anteriormente en SO).
SWIFTUI 2.0
Aquí hay una posible solución alternativa en Xcode 12/iOS 14 (SwiftUI 2.0) que se puede usar en el mismo escenario cuando los controles de desplazamiento están fuera del área de desplazamiento (porque SwiftUI2 ScrollViewReader
solo se puede usar dentro ScrollView
)
Nota: El diseño del contenido de la fila está fuera del ámbito de consideración.
Probado con 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()
ScrollViewReader { sp in
ScrollView {
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+
Aquí hay una variante simplificada del enfoque que funciona, parece apropiada y requiere un par de códigos de pantalla.
Probado con Xcode 11.2+ / iOS 13.2+ (también con Xcode 12b / iOS 14)
Demostración de uso:
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
)
}
}
}
}
}
Solución:
La vista de luz representable que se inyecta List
da acceso a la jerarquía de vistas de UIKit. Como List
se reutilizan las filas, no hay más valores y luego se ajustan las filas en la pantalla.
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
}
}
Proxy simple que encuentra elementos adjuntos UIScrollView
(es necesario hacerlo una vez) y luego redirige las acciones de "desplazamiento" necesarias a esa vista de desplazamiento almacenada
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
}
}
Simplemente desplácese hasta la identificación:
scrollView.scrollTo(ROW-ID)
Dado que SwiftUI está estructurado y diseñado basado en datos, debe conocer los ID de todos sus elementos. Para que puedas desplazarte a cualquier id ScrollViewReader
desde iOS 14 y con 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!) } }
}
}
}
}
Tenga en cuenta que ScrollViewReader
debería admitir todo el contenido desplazable, pero ahora solo admiteScrollView