Skip to content

Fix iOS scroll locked to bottom during incoming data#468

Open
alvarolb wants to merge 1 commit intomigueldeicaza:mainfrom
alvarolb:ios-scroll-fix
Open

Fix iOS scroll locked to bottom during incoming data#468
alvarolb wants to merge 1 commit intomigueldeicaza:mainfrom
alvarolb:ios-scroll-fix

Conversation

@alvarolb
Copy link
Contributor

@alvarolb alvarolb commented Feb 12, 2026

Problem

On iOS, TerminalView (a UIScrollView subclass) always forces contentOffset to the bottom in updateScroller(). This means the user cannot scroll up to read previous output while the terminal is actively receiving data — any new content immediately snaps the view back to the bottom.

Approach

This follows the same pattern already used on macOS (where updateScroller() only updates the NSScroller widget without forcing content position), adapted for UIScrollView:

  1. Derive contentOffset from yDispupdateScroller() now uses displayBuffer.yDisp (already managed by Terminal.scroll(isWrapped:)) instead of always forcing to the bottom. When userScrolling is true, yDisp stays stable so the view won't snap.

  2. Track scroll via panGestureRecognizer — Uses addTarget on the existing pan gesture instead of setting delegate = self, preserving delegate semantics for consumers of TerminalView.

  3. Synchronize yDisp from contentOffset — After the user finishes scrolling (including deceleration), yDisp is updated to match the visual scroll position, keeping buffer state in sync.

  4. Detect deceleration end via CADisplayLink — Uses the existing display link to check when deceleration ends, avoiding contentOffset didSet overrides that conflict with UIKit bounce animations.

  5. Reset on buffer switchbufferActivated() clears userScrolling so alternate screen transitions always snap to bottom.

  6. Reset on keyboard inputcommitTextInput() clears userScrolling and scrolls to bottom, since typing implies the user wants to see current output.

  7. Conditional ensureCaretIsVisible — Only scrolls to bottom if the caret is actually off-screen, matching macOS behavior.

Changes

  • Sources/SwiftTerm/iOS/iOSTerminalView.swift: Guard contentOffset in updateScroller(), add pan gesture tracking, sync yDisp from scroll position, reset on buffer switch and keyboard input, conditional ensureCaretIsVisible.

@migueldeicaza
Copy link
Owner

I like the idea, but there are a few challenges with this patch

  1. The patch toggles terminal.userScrolling but never synchronizes yDisp from the actual UIScrollView position. I think this will get the data out of sync.
  2. I think that the state reset needs to cover other scenarios, like buffer switching
  3. Hijacking the delegate is a change in the semantics, I am not sure how to solve this.
  4. I wonder what happens if you are just a little off, it seems like this requires one full line to be scrolled before the "freeze" effect takes place.

@migueldeicaza
Copy link
Owner

Oh, and I think that keyboard input needs to also auto-reset this.

On iOS, updateScroller() unconditionally forced contentOffset to
the bottom on every new line of output, preventing the user from
scrolling up while the terminal was receiving data.

Changes:

1. Derive contentOffset from yDisp — updateScroller() now uses the
   terminal's yDisp (already managed by Terminal.scroll) instead of
   always forcing to bottom. When userScrolling is true, yDisp stays
   stable so the view won't snap.

2. Track scroll via panGestureRecognizer — uses addTarget on the
   existing pan gesture instead of setting delegate = self, preserving
   delegate semantics for consumers of TerminalView.

3. Synchronize yDisp from contentOffset — after the user finishes
   scrolling (including deceleration), yDisp is updated to match the
   visual position, keeping buffer state in sync.

4. Detect deceleration end via CADisplayLink — uses the existing
   display link to check when deceleration ends, avoiding contentOffset
   didSet overrides that conflict with UIKit bounce animations.

5. Reset on buffer switch — bufferActivated() clears userScrolling
   so alternate screen transitions always snap to bottom.

6. Reset on keyboard input — commitTextInput() clears userScrolling
   and scrolls to bottom, since typing implies the user wants to see
   current output.

7. Conditional ensureCaretIsVisible — only scrolls to bottom if the
   caret is actually off-screen, matching macOS behavior.
@migueldeicaza
Copy link
Owner

I got a GitHub notification for this, but when I click on it, it says nothing is there - would you mind posting your message again?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants