xcodeswiftuiscrollviewtvosbehavior

How to fix SwiftUI tvOS ScrollView jumpy / jittery / flaky / buggy / glitchy behavior?


look at this simple example of scroll on tvOS (ver 14.7 18M60):

struct TestView: View
{
    var body: some View {
            ScrollView (.vertical, showsIndicators: false, content: {
                ForEach(0..<600) { index in
                    Button(action: {}, label: {Text("Button - \(index)")})
                        .background(Color.blue)
                        .padding()
                }
            })
    }
}

this example will behave badly when scrolling continuously, the scroll will stop moving and wait for me to finish scrolling on the remote and only a half second after i have finished swipeing on the Apple TV remote then it will update the view and scroll to the requested position,

See a video with this behavior

obviously this is an unwanted behavior, since during the swipe i cannot see where i have reached, therefor i have to stop swipeing, allow the view to refresh and only after i see where the scroll has reached, i can decide wether to keep scrolling or scroll back up (if needed)

it looks like a SwiftUI (try to) optimize performance, but it destroy the whole ATV app experience

i tried fixing it using a ScrollViewReader like this:

import Foundation
import SwiftUI

struct FocusableButton: View
{
    @State var isFocused = false
    let index: Int
    var scrollView: ScrollViewProxy
    
    var body: some View
    {
        VStack
        {
            Text("Button - \(index)")
        }
            .id(index)
            .frame(width: 200, height: 100)
            .padding()
            .scaleEffect(isFocused ?  1.1 : 1)
            .background(isFocused ?  Color.white : Color.blue)
            .focusable(true, onFocusChange: { focused in
                isFocused = focused
                (focused ? {
                    scrollView.scrollTo(index)
                }() : { /* Lost Focus */ }())
            })
    }
}

struct TestView: View
{
    var body: some View {
        ScrollViewReader { scrollView in
            ScrollView (.vertical, showsIndicators: false, content: {
                ForEach(0..<600) { index in
                    FocusableButton(index: index, scrollView: scrollView)
                }
            })
        }
    }
}

see a video of how it behaves after the fix

but if scroll too fast - the auto - paging - scroll kicks in and crash the app and i get the error (in xcode):

Fatal error: ScrollViewProxy may not be accessed during view updates 

and the runtime issue:

Modifying state during view update, this will cause undefined behavior.

the xcode runtime error screenshot

can anyone help? any idea how to solve it? (the scroll issue or the error)

even if there is a way to bypass the auto paging scroll i would love to see a code example


Solution

  • strange fact: if you surround the ScrollView with a List (even of 1 dummy item) - the scrolling behaves correctly,

    so i implemented this test with List and it behaves correctly:

    struct TestView: View
    {
        var body: some View {
            VStack(spacing: 40)
            {
                List(0..<600) { index in
                    FocusableButton(index: index)
                }
            }
        }
    }
    

    so my current conclusion is that the combination of:

    wont work!


    if i remove the focusable and replace it with a button the ScrollView and ForEach also work correctly:

    struct TestView: View
    {
        var body: some View {
            VStack(spacing: 40)
            {
                List(0..<600) { index in
                    Button(action: {}, label: {
                        Text("Button - \(index)")
                            .frame(width: 200, height: 100)
                            .padding()
                            .background(Color.blue)
                    })
                }
            }
        }
    }
    

    but all 3 together (ScrollView + ForEach + focusable) behaves badly