Repository navigation
refactor: View/ViewModel 책임 분리 + PullToRefresh 성능 개선 - #106
Conversation
GeometryReader + 매 프레임 DispatchQueue.main.async @State 난타 제거(버벅임 원인). contentOffset 기반 안정적 오버스크롤 측정 + scrollBounceBehavior(.always)로 짧은 콘텐츠도 동작 + 인디케이터 페이드 전환.
- BrandDetail: View의 모델 직접 mutate 제거(pause/resume VM 인텐트), 매직넘버 321 → VM requiresSpecialSubscribeFlow - Subscribe: subscribeState·filteredSubscriptions·새로고침 throttle → VM - Explore: 6개 트리거의 isGuest 분기+fetch → VM 인텐트 메서드, isLoaded → VM isInitialLoaded - MyPage: View의 store 직접 접근(userInfo @State) 제거 → VM displayNickname/displaySubscribeEmail - Search: 에러 채널 중복(errorMessage/popularErrorMessage String) → AppError 영역 상태로 정리 - Bookmark: sortedCategories·빈상태 판정 → VM computed - Subscribe/Explore VM에 appState 주입(isGuest 판정용, default .shared)
There was a problem hiding this comment.
Code Review
This pull request refactors several views and view models across multiple modules (DesignSystem, Bookmark, Detail, Explore, MyPage, Search, and Subscribe) to improve separation of concerns by moving UI state and business logic into the view models. Key feedback points out that using onScrollGeometryChange in PullToRefreshView requires iOS 18.0+, which may cause compilation issues on older deployment targets. Additionally, it is recommended to mark handleOverscroll with @MainActor for concurrency safety, and to remove duplicated cooldown logic and the redundant canRefresh method from SubscribeViewModel.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| .onScrollGeometryChange(for: CGFloat.self) { geometry in | ||
| geometry.contentOffset.y + geometry.contentInsets.top | ||
| } action: { _, offset in | ||
| handleOverscroll(-offset) | ||
| } |
There was a problem hiding this comment.
onScrollGeometryChange(for:action:) is only available in iOS 18.0 and later. If the project's deployment target is lower than iOS 18.0, this will cause a compilation error.\n\nIf the app supports iOS 17 or earlier, you should wrap this modifier in an #available check or provide a fallback implementation (such as the previous GeometryReader approach) for older iOS versions.
| .accessibilityHint("당겨서 새로고침") | ||
| } | ||
|
|
||
| private func handleOverscroll(_ distance: CGFloat) { |
There was a problem hiding this comment.
Mark handleOverscroll with @MainActor to ensure that state mutations (such as isRefreshing, hasTriggered, and lastRefreshTime) are safely performed on the main thread. This prevents potential data races and adheres to Swift 6 strict concurrency guidelines.
@MainActor\n private func handleOverscroll(_ distance: CGFloat) {| private var lastRefreshTime = Date.distantPast | ||
| private let refreshCooldown: TimeInterval = 2.0 |
There was a problem hiding this comment.
The cooldown logic is now duplicated between PullToRefreshView (which has its own cooldownInterval and lastRefreshTime tracking) and SubscribeViewModel. This redundancy can cause UI bugs where the pull-to-refresh animation triggers but the actual data refresh is silently ignored because the ViewModel's cooldown (2.0s) is longer than the View's cooldown (1.5s).\n\nWe should remove the cooldown state and logic from SubscribeViewModel and instead pass cooldownInterval: 2.0 directly to PullToRefreshView in SubscribeView.
| public func canRefresh(referenceDate: Date = Date()) -> Bool { | ||
| guard referenceDate.timeIntervalSince(lastRefreshTime) >= refreshCooldown else { | ||
| return false | ||
| } | ||
| lastRefreshTime = referenceDate | ||
| return true | ||
| } |
Summary
PullToRefreshView를GeometryReader+ 매 프레임DispatchQueue.main.async패턴에서onScrollGeometryChange기반으로 전환 — 버벅임 제거 및 짧은 콘텐츠 동작 보장Details
View → ViewModel 이전
321→requiresSpecialSubscribeFlowsubscribeState·filteredSubscriptions·새로고침 throttle → VMisGuest분기 + fetch → VM 인텐트 메서드,isLoaded→ VMisInitialLoadeduserInfo@State) 제거 → VMdisplayNickname/displaySubscribeEmailerrorMessage/popularErrorMessageString) →AppError영역 상태로 정리sortedCategories·빈 상태 판정 → VM computedappState주입 (isGuest 판정용, default.shared)PullToRefresh 성능
contentOffset기반 안정적 오버스크롤 측정scrollBounceBehavior(.always)로 짧은 콘텐츠도 동작Test plan
🤖 Generated with Claude Code