¿Cómo desplazar la lista mediante programación en SwiftUI?

Resuelto Asperi asked hace 54 años • 11 respuestas

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 UITableViewla infraestructura completa UIViewRepresentable/UIViewControllerRepresentablecomo se propuso anteriormente en SO).

Asperi avatar Jan 01 '70 08:01 Asperi
Aceptado

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 ScrollViewReadersolo 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

demostración2

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
                        )
                }
            }
        }
    }
}

manifestación

Solución:

La vista de luz representable que se inyecta Listda acceso a la jerarquía de vistas de UIKit. Como Listse 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
    }
}
Asperi avatar Mar 25 '2020 19:03 Asperi

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 ScrollViewReaderdesde 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


Avance

avance

Mojtaba Hosseini avatar Jun 28 '2020 14:06 Mojtaba Hosseini