Skip to content

TodoListView 헤더 트러블슈팅 ‐ 스크롤 조정

최윤진 edited this page Mar 5, 2026 · 3 revisions

Note

Section의 header 파라미터의 한계를 체감하고 safeAreaInset(edge: .top)을 사용하여 UI를 개선하기 위한 시도를 작성하였습니다
해결 PR

문제 정의

  • TodoListView의 todoListContent에서 Section header:로 headerView를 사용 중이었으나, 헤더의 top/leading/trailing 인셋을 세밀하게 제어할 수 없는 한계가 존재.
  • 이를 해결하기 위해 safeAreaInset(edge: .top)으로 headerView를 분리하고, 스크롤 시 헤더가 연동되어 움직이는 기능을 구현하려 함.

해결 과정 및 시도 내용

Try 1: Section header에 listRowInsets 적용

  • Section의 header: 파라미터에 headerView를 넣고 .listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: -8, trailing: 16))를 적용.
  • 결과: 스크롤 시 navigationBar 밑으로 headerView가 들어가서 가려지는 현상 발생.

Try 2: safeAreaInset(edge: .top)으로 헤더 분리

  • Section header: 대신 List에 .safeAreaInset(edge: .top)으로 headerView를 배치.
  • 결과: headerView의 padding으로 인셋 제어는 가능해졌으나, 스크롤 연동 기능(오프셋 추적) 구현이 추가로 필요해짐.

Try 3: onScrollGeometryChange (iOS 18+)

  • iOS 18의 onScrollGeometryChange(for:of:action:)으로 스크롤 오프셋을 추적하여 headerOffset에 반영.
  • 결과: 정상 동작하나 iOS 17 미지원 문제 발생. View+ 확장에 onScrollOffsetChange 헬퍼를 만들어 내부에서 #available(iOS 18, *) 분기 처리하여 대응.

Try 4: PreferenceKey + GeometryReader (Section background)

  • Section의 .background에 GeometryReader를 넣고, named coordinate space에서 minY를 PreferenceKey로 전달.
  • 결과: onPreferenceChange가 최초 1회만 호출되고 스크롤 중에는 업데이트되지 않음. List 내부의 PreferenceKey는 스크롤 중 연속 전파가 안 되는 SwiftUI의 한계 확인.

Try 5: GeometryReader를 첫 번째 row의 background에 적용

  • Section이 아닌 개별 row의 .background에 GeometryReader를 배치.
  • Array(zip(todos.indices, todos))로 idx를 확인하고 idx == 0일 때만 적용 및 .onChange(of: minY)로 State 변수 직접 업데이트.
  • 결과: 최초 스크롤 시 일부 지점에서 위치 이동이 매끄럽지 않은 글리치 현상이 발생하여 폐기.

Try 6: UIScrollView KVO (UIViewRepresentable) - iOS 17 지원 및 최종 해결

iOS 17에서도 스크롤 오프셋을 추적하기 위해 UIViewRepresentable을 활용한 KVO(Key-Value Observing) 방식을 도입했습니다. (초기 시도 및 최종 개선안 통합)

  • 1차 시도 (단순 superview 탐색): ScrollViewOffsetTracker를 구현하여 superview를 탐색해 UIScrollView의 contentOffset을 관찰하려 함. 하지만 SwiftUI .background()로 추가된 UIView가 UIScrollView 외부에 배치되어 단순히 superview 체인만으로는 UIScrollView를 찾지 못함 (디버그 로그: superview exists: true, UIScrollView not found). 또한 contentOffset 변화값이 SwiftUI @State로 제대로 전달되지 않는 이슈가 있었음.
  • 2차 시도 (superview + sibling subview 재귀 탐색): superview 체인을 올라가면서 각 단계에서 sibling(형제) subview도 재귀적으로 탐색하도록 로직을 변경. (findScrollView: superview 방향 순회 + 각 superview의 sibling subview에서 findScrollViewInSubviews 호출)
  • 최종 결과: iOS 17에서 UIScrollView(List 내부의 UICollectionView)를 정상적으로 찾고, KVO로 contentOffset을 연속 추적하는 데 성공함.

최종 구현체 (View+.swift): onScrollOffsetChange 헬퍼 메서드 내부에서 버전에 따라 분기 처리:

  • iOS 18+: 기본 제공되는 onScrollGeometryChange 사용
  • iOS 17: ScrollViewOffsetTracker.background()로 추가하여 superview + sibling 탐색으로 UIScrollView를 찾고 KVO 관찰 적용

발견된 부수 이슈 및 해결 방법

1. safeAreaInset + searchable 전환 시 headerView 미표시

  • 현상: searchable 상태에서 복귀하면 safeAreaInset 내부의 headerView가 보이지 않음 (onAppear는 호출되나 화면 렌더링 실패).
  • 원인: headerView 내부의 ScrollView(.horizontal)의 레이아웃 충돌 문제.
  • 해결: ScrollView(.horizontal).frame(height: 36)으로 고정 높이를 지정하여 레이아웃을 안정화시켜 해결.

2. safeAreaInset 내부 ScrollView와 List의 스크롤뷰 충돌

  • 현상: 콘솔에 "Multiple scroll views were found. Picking the first one to compare." 경고 출력.
  • 해결: onScrollGeometryChange 모디파이어를 .safeAreaInset보다 앞에 배치하여 오직 List의 스크롤만 감지하도록 순서 변경.

3. searchable 복귀 후 headerView 드래그 가능 현상

  • 현상: searchable에서 돌아오면 가로형 headerView(ScrollView)가 드래그에 의해 통째로 이동 가능해지는 버그 발생.
  • 해결: headerView의 ScrollView에 .scrollDisabled(true/false)를 조건부 적용하여 초기 레이아웃 안정화 후 드래그를 방지함.

4. 초기 로드 시 네비게이션 large title 불안정

  • 현상: 뷰가 최초 표시될 때 네비게이션 타이틀(large title)이 위아래로 튀는 현상 발생.
  • 해결: isScrollTrackingEnabled State를 추가하고, onAppear 이후 0.3초 딜레이를 주어 스크롤 추적을 지연 활성화하도록 처리. (iOS 26은 해결되지 않음)

Clone this wiki locally