ScrollViewReader mit List und scrollTo

Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

Aufgrund der Corona-Krise: Die Veröffentlichung von Stellenangeboten und -gesuchen ist bis 31.3.2023 kostenfrei. Das beinhaltet auch Angebote und Gesuche von und für Freischaffende und Selbstständige.

  • ScrollViewReader mit List und scrollTo

    Da das Thema vielleicht etwas umfangreicher wird, mache ich hierfür mal einen extra Thread auf. Folgendes Problem: Im angehängten minimalen Testcase wird eine Liste erzeugt und beim Hinzufügen eines Eintrags, soll jeweils zum Ende der Liste gescrollt werden. In iOS 15 klappt das wunderbar, in iOS 16 bekommen ich beim scrollTo einen Laufzeitfehler. Wenn ich die Doku zu ScrollViewReader richtig verstehe, sollte aber ein Aufruf im onChange durchaus möglich sein.

    Um der Frage vorzugreifen: Ja, ich möchte hier lieber List statt ScrollView nutzen, da ich die Swipe-Gesten verwende.

    Hier der Quellcode. Vielleicht hat ja einer noch eine Idee, wo das Problem liegen können und wie man es in iOS 16 umgehen kann.

    Quellcode: ScrollListView.swift

    1. import SwiftUI
    2. struct ScrollListView: View {
    3. @State private var data: [String] = []
    4. var body: some View {
    5. VStack {
    6. Button(action: {
    7. data.append("ScrollItem #\(data.count+1)")
    8. }) {
    9. Label("Add Row", systemImage: "plus")
    10. }
    11. .buttonStyle(.borderedProminent)
    12. ScrollViewReader { proxy in
    13. List(0..<data.count, id: \.self) { index in
    14. Text(data[index]).id(index)
    15. }
    16. .listStyle(.grouped)
    17. .navigationTitle("ScrollList Test")
    18. .onChange(of: data.count) { _ in
    19. proxy.scrollTo(data.count-1)
    20. }
    21. }
    22. }
    23. .onAppear() {
    24. for _ in 0..<20 {
    25. data.append("ScrollItem #\(data.count+1)")
    26. }
    27. }
    28. }
    29. }
    Alles anzeigen
    PS: Mit einem Button in einer festen Liste zu einer Zeile zu springen, wie es hier beschrieben ist, funktioniert auch in iOS 16. Nützt mir aber leider nichts.
    So Long, and Thanks for All the Fish.
  • Ich denke schon, dass onChange() mit Main Thread läuft, zumindest kommt der Fehler im Main-Thread:



    Aber auch wenn ich den Aufrufe explizit in den Main packe, kommt der selbe Fehler:

    Quellcode

    1. .onChange(of: data.count) { count in
    2. DispatchQueue.main.async {
    3. proxy.scrollTo(count-1, anchor: .bottom)
    4. }
    5. }
    Ich habe echt keine Idee mehr und bei Apple im Forum gibt es schon ähnliche Meldungen. Ist halt wirklich ein blöder Fehler, da das Springen zum Ende einer dynamischen Liste jetzt keine so exotische Funktion ist.
    So Long, and Thanks for All the Fish.
  • Der ScrollViewReader ist ja dazu da den ScrollViewProxy zur Verfügung zu stellen. Das kann eigentlich nur so funktionieren, zumindest wird es so in der Doku beschrieben und ich wüsste auch keinen anderen Weg.

    Und scrollTo(0) ist kein Problem, das funktioniert. Es tritt nur auf, wenn man zum Ende einer dynamischen Liste springen will. Dabei ist aber auch data und count nicht das eigentliche Problem. Hier habe ich zum Beispiel am Ende immer ein Bottom-Element:

    Quellcode

    1. import SwiftUI
    2. struct ScrollListView: View {
    3. @State private var data: [String] = []
    4. @Namespace var bottomID
    5. var body: some View {
    6. VStack {
    7. Button(action: {
    8. data.append("ScrollItem #\(data.count+1)")
    9. }) {
    10. Label("Add Row", systemImage: "plus")
    11. }
    12. .buttonStyle(.borderedProminent)
    13. ScrollViewReader { proxy in
    14. VStack {
    15. List {
    16. ForEach (0..<data.count, id: \.self) { index in
    17. Text(data[index]).id(index)
    18. }
    19. Text("").id(bottomID)
    20. }
    21. .listStyle(.grouped)
    22. }
    23. .navigationTitle("ScrollList Test")
    24. .onChange(of: data.count) { _ in
    25. proxy.scrollTo(bottomID, anchor: .bottom)
    26. }
    27. }
    28. }
    29. .onAppear() {
    30. for _ in 0..<30 {
    31. data.append("ScrollItem #\(data.count+1)")
    32. }
    33. }
    34. }
    35. }
    Alles anzeigen
    Gleicher Fehler, obwohl ich data.count gar nicht nutze. Ich bin echt ratlos.
    So Long, and Thanks for All the Fish.
  • So in dem Fall nicht, weil man dann den ScrollViewProxy nicht hat. Ich habe das in einem anderen Test aber schon versucht und das macht keinen Unterschied. Das Problem liegt nicht am onChange-Handler, sondern irgendwo tiefer.

    Allerdings brauche ich es für mein aktuelles Projekt (in Calculy den Rechenverlauf) so oder so im onChange. Dort habe ich ein ObservableObject und muss da auf Änderungen reagieren.
    So Long, and Thanks for All the Fish.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von Babelfisch ()

  • Ohne es mir jetzt genau angeschaut zuhaben, würde ich in Richtung ‚willChange‘ vs ‚didChange‘ forschen.

    Der Button löst einen redraw der View aus, genauso wie das Neuzeichnen der Liste das auch tut, in der selben View. Fakt jedoch ist, dass die List neu gezeichnet wird, jedoch nicht mit dem aktuellen letzten Wert.

    Ich könnte mir vorstellen, wenn bttn und List nicht in der selben View den sitzen und jeweils den redraw in dieser triggern können, dass es dann funktioniert. Aber das ist jetzt nur eine Annahme, die ich nicht überprüft habe.

    Tipp:

    - vergebe doch nur für das letzte Element in der Liste eine feste .id / die anderen bekommen keine. So hast du die Fehlerquelle ‚id nicht korrekt gesetzt‘ weg.

    - erstelle dir ein Objekt was identifiable und hashable ist mit einer uuid random als id.

    - etatisiere über das Array data und nicht über data.count
  • 322 schrieb:

    Ohne es mir jetzt genau angeschaut zuhaben, würde ich in Richtung ‚willChange‘ vs ‚didChange‘ forschen.
    An willChange und didChange komme ich so leider nicht ran bzw. sehe ich nicht, wie ich das sinnvoll nutzen kann, da ich ja trotzdem innerhalb vom ScrollViewReader zu der Zeile springen muss und das nur mit onChange geht.

    322 schrieb:

    Der Button löst einen redraw der View aus, genauso wie das Neuzeichnen der Liste das auch tut, in der selben View. Fakt jedoch ist, dass die List neu gezeichnet wird, jedoch nicht mit dem aktuellen letzten Wert.
    Na ja, der Button selbst löst es nicht aus. Die Änderung der State-Variable bewirkt das Neuzeichnen der Liste und das ist ja so gewollt. Sehr merkwürdig ist aber, warum das letzte Element doppelt gerendert wird.

    322 schrieb:

    Ich könnte mir vorstellen, wenn bttn und List nicht in der selben View den sitzen und jeweils den redraw in dieser triggern können, dass es dann funktioniert. Aber das ist jetzt nur eine Annahme, die ich nicht überprüft habe.
    Wie meinst du das? Die müssen ja in der selben View sein, sonst kann ich den Button ja nicht anzeigen.

    322 schrieb:

    - vergebe doch nur für das letzte Element in der Liste eine feste .id / die anderen bekommen keine. So hast du die Fehlerquelle ‚id nicht korrekt gesetzt‘ weg.
    Im 5. Beitrag hier im Thread hatte ich auch mal ein Beispiel, wo ich ein extra bottom-Element mit einer eigenen ID einfüge, um immer nach unten zu springen. Bringt auch nichts.

    322 schrieb:

    - erstelle dir ein Objekt was identifiable und hashable ist mit einer uuid random als id.
    Das Beispiel hier ist extra so weit minimiert und auf das nötigste runtergebrochen. Da, wo ich es anwende, wird mit einem ObservableObject gearbeitet, das Hashable und über eine UUID Identifiable ist.

    Hier aber noch mal ein Beispiel, wo nur das letzte Element eine feste ID hat. Geht trotzdem nicht. Auch mit einer UUID als id geht es nicht.

    Quellcode

    1. ScrollViewReader { proxy in
    2. List {
    3. ForEach (0..<data.count, id: \.self) { index in
    4. Text(data[index])
    5. }
    6. Text("Bottom").id(bottomID)
    7. }
    8. .onChange(of: data.count) { count in
    9. proxy.scrollTo(bottomID, anchor: .bottom)
    10. }
    11. }
    Alles anzeigen

    322 schrieb:

    - etatisiere über das Array data und nicht über data.count
    Macht keinen Unterschied. Der Fehler tritt auch da genauso auf.

    Meine Vermutung ist, dass Apple hier wirklich einen ganz fiesen Bug in SwiftUI 4 und List hat und es wohl auch keinen Workaround dafür gibt. Der einzige Weg wäre, die List wegzulassen und das mit eine ScrollView nachzubilden. Das gleiche Beispiel ohne List mit ScrollView funktioniert nämlich:


    Quellcode: ScrollViewTest.swift

    1. import SwiftUI
    2. struct ScrollViewTest: View {
    3. @State private var data: [String] = []
    4. fileprivate func TextRowView(_ index: Int) -> some View {
    5. HStack {
    6. Text("#\(index)")
    7. Spacer()
    8. Text(data[index]).id(index)
    9. }
    10. .padding(.horizontal, 16)
    11. .padding(.vertical, 4)
    12. }
    13. var body: some View {
    14. VStack {
    15. Button(action: {
    16. data.append("ScrollItem #\(data.count+1)")
    17. }) {
    18. Label("Add Row", systemImage: "plus")
    19. }
    20. .buttonStyle(.borderedProminent)
    21. ScrollViewReader { proxy in
    22. ScrollView(.vertical) {
    23. VStack(alignment: .leading) {
    24. ForEach (0..<data.count, id: \.self) { index in
    25. TextRowView(index)
    26. }
    27. }
    28. .listStyle(.grouped)
    29. }
    30. .navigationTitle("ScrollView Test")
    31. .onChange(of: data.count) { count in
    32. withAnimation {
    33. proxy.scrollTo(count-1, anchor: .bottom)
    34. }
    35. }
    36. }
    37. }
    38. .onAppear() {
    39. for _ in 0..<30 {
    40. data.append("ScrollItem #\(data.count+1)")
    41. }
    42. }
    43. }
    44. }
    Alles anzeigen
    So Long, and Thanks for All the Fish.
  • Nein, leider nicht. Der Fehler tritt weiterhin auch in Xcode 14.1 RC und iOS 16.1 RC auf und von Apple gab es bisher noch keine Reaktion. Wie ich im Internet rausfinden konnte, liegt es wohl daran, dass List bisher UITableView genutzt hat und nun auf UICollectionView basiert. Das funktioniert dann mit ScrollViewReader offenbar nicht mehr. Warum Apple das aber nicht fixed und komplett ignoriert, ist mir ein Rätsel. Die verfügbaren Workarounds sind leider auch alle unbefriedigend.
    So Long, and Thanks for All the Fish.