[tvOS] ScrollView with Text does not scroll

I'm trying to do something so seemingly basic, yet I can't get it to work and I'm flummoxed.

In a basic, vanilla SwiftUI app for tvOS, embed a single Text element with a very long string (hundreds of lines) in it:

struct ContentView: View {
  var body: some View {
    ScrollView(.vertical) {
      Text(veryLargeString)
        .focusable()
    }
  }
}

Then fire up the app on tvOS, and it will not scroll. No matter what I do. Pressing arrow keys, swiping fast with my thumb, and nothing. It will not move. Ironically, in the Xcode SwiftUI Preview window—it does scroll, so that's always a fun tease.

What I do know is that the focus engine is throwing a few errors, so it's leading me to believe the issue is with how I have the focusable element attached. I'm using a combination of -UIFocusLoggingEnabled YES as well as listening for UIFocusSystem.movementDidFailNotification.

Unfortunately since this is SwiftUI, the notification failure and debugging logs aren't really all that actionable. Help appreciated!

Filed FB13691987

I have the same issue just now.


    public var body: some View {
        ScrollView {
            Text(attributedLicenseBody)
                .font(.caption)
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding()
                .onAppear {
                    attributedLicenseBody = attribute(library.licenseBody)
                }
        }
        .navigationBarTitle(library.name)
        ._licenseViewStyle(licenseViewStyle) {
            if let url = library.url {
                openURL(url)
            }
        }
        .background(licenseViewStyle.background)
    }

That codes scroll nicely on iOS.

It appears to me that it is possible to use UITextView instead of Text. And with that, you end up with a scrollable element (interestingly completely without using scrollview.

Note, in the example below I also used attributified text to render html elements for styling. So even that works. But it also works with just a regular text string!

    Rectangle()
        .fill(.ultraThinMaterial)
        .ignoresSafeArea()

    FocusableAttributedTextView(attributedString: viewModel.attributedString)
        .frame(width: 500,
                height: 800)
        .background(.ultraThinMaterial)
        .clipShape(RoundedRectangle(cornerRadius: Constants.cornerRadius))
        .focused($isFocused)
}
.onExitCommand {
    showExpanded = false
}
struct FocusableAttributedTextView: UIViewRepresentable {
    let attributedString: AttributedString

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()

        // Configure for tvOS focus and scrolling as per Cocoanetics/DTCoreText#1149
        textView.isSelectable = true
        textView.isUserInteractionEnabled = true
        textView.panGestureRecognizer.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.indirect.rawValue)]
        textView.font = UIFont.systemFont(ofSize: 90)

        return textView
    }

    func updateUIView(_ uiView: UITextView, context: Context) {
        // Convert SwiftUI AttributedString to NSAttributedString for UITextView
        let nsAttributedString = NSAttributedString(attributedString)
        uiView.attributedText = nsAttributedString
    }
}

Your focusable element is attached correctly. The problem is that there is only one of it.

On tvOS a ScrollView scrolls as a side effect of focus moving: the focus engine picks a new target, and if that target is off screen the container scrolls to bring it on. With a single focusable view there is no second target to move to, so nothing is ever revealed and the view never budges. One focusable view behaves exactly like none. That is also why your UIFocusSystem.movementDidFailNotification is firing and why -UIFocusLoggingEnabled YES is complaining — the movement really is failing, because there is nowhere to go.

Apple documents the mechanism, but in UIKit rather than SwiftUI, on UIFocusItemScrollableContainer — "A type of focus item container that supports automatic scrolling of focusable content", and "The focus engine scrolls the container to keep items onscreen as they become focused." You can write SwiftUI for a long time without meeting that protocol. https://developer.apple.com/documentation/uikit/uifocusitemscrollablecontainer

I don't know why the Xcode preview scrolls when the device won't — my assumption is that the preview isn't driving the focus engine, but I haven't confirmed that, so I wouldn't read much into the preview either way.

I should say plainly what this does not fix: it will not make a single long Text scroll. If you need one unbroken text view, the UITextView approach further down this thread is the way to do it. What works in SwiftUI is giving the focus engine more than one place to land, which means splitting the content:

ScrollView {
    VStack(alignment: .leading, spacing: 16) {
        ForEach(paragraphs, id: \.self) { para in
            Text(para).focusable(true)
        }
    }
}

Two traps I hit doing this in a shipping tvOS app, both of which cost me a round on the device.

Putting .focusable(true) on the enclosing VStack instead of the rows looks like the fix and causes the bug — it collapses the whole block to one target. I had written that version myself, having correctly worked out that a scroll view with nothing focusable cannot scroll, and then not noticed that one is the same as none.

And the scroll only ever travels to a view that takes focus, so anything above your topmost focusable view is unreachable once it scrolls off the top. If there is a title above the content, make it focusable too or you can go down and never come back up.

The cost is a focus ring on prose when the d-pad lands on it. On balance I kept it, since that ring is the only feedback that the remote did anything.

Observed on an Apple TV 4K (3rd generation) running tvOS 27, on an app that builds back to tvOS 17.

Longer write-up with the two traps and a pane whose scrollability turned out to depend on StoreKit returning products: https://mcmizzle.com/blog/tvos-scrollview-wont-scroll/

[tvOS] ScrollView with Text does not scroll
 
 
Q