Skip to content

Comments

Feat/#109 피커 실시간 변동 적용#119

Merged
soseoyo12 merged 1 commit intodevelopfrom
feat/#109-pickerRealtime
Jan 20, 2026
Merged

Feat/#109 피커 실시간 변동 적용#119
soseoyo12 merged 1 commit intodevelopfrom
feat/#109-pickerRealtime

Conversation

@soseoyo12
Copy link
Contributor

🔗 연결된 이슈

📄 작업 내용

  • 피커 실시간 동기화 적용
구현 내용 IPhone 16 pro IPhone 13 mini
GIF Simulator Screen Recording - iPhone 17 Pro - 2026-01-20 at 23 22 46 Simulator Screen Recording - iPhone 13 mini - 2026-01-20 at 23 26 50

💻 주요 코드 설명

실시간 감지를 위해 내부 스크롤 뷰를 찾아내서 델리게이트를 가로채는 과정이 추가함으로써 해결했습니다.

func makeUIView(context: Context) -> UIPickerView {
    let picker = UIPickerView()
    picker.delegate = context.coordinator
    picker.dataSource = context.coordinator
    
    // 피커 내부의 스크롤 뷰를 찾아서 델리게이트를 Coordinator로 위임
    picker.subviews.forEach { subview in
        subview.backgroundColor = .clear 
        
        // 변경점
        if let scrollView = subview as? UIScrollView {
            scrollView.delegate = context.coordinator
        }
    }
    /// ...
    return picker
}
  • if let scrollView = subview as? UIScrollViewUIPickerView는 겉으로는 피커지만, 내부적으로는 UIScrollView(또는 UITableView)로 구성되어 있습니다. 이 숨겨진 스크롤 뷰를 찾아냅니다.
  • scrollView.delegate = context.coordinator: 원래는 피커 내부 로직이 스크롤을 관리하지만, 스크롤 되는 순간을 포착하기 위해 Coordinator가 스크롤 델리게이트 권한을 가져옵니다. 이렇게 해야 scrollViewDidScroll 이벤트를 받을 수 있습니다.

기존 코드 (didSelectRow)

// 스크롤이 멈췄을 때 1번 호출
funcpickerView(_pickerView: UIPickerView,didSelectRowrow:Int,inComponentcomponent:Int) {
    parent.selection= parent.range.lowerBound+ row
}

실시간 코드 (scrollViewDidScroll)

// 스크롤이 움직일 때마다 계속 호출됨
funcscrollViewDidScroll(_scrollView: UIScrollView) {
// 1. 현재 스크롤된 y 위치에서 행 높이 절반을 더해 중앙 보정
let yOffset= scrollView.contentOffset.y+ (44.adjustedH/2)

// 2. 높이로 나누어 현재 몇 번째 row인지 계산 (반올림 같은 효과)
var row=Int(yOffset/44.adjustedH)

// 3. 인덱스 범위 안전 장치 (0보다 작거나 최대 개수 넘지 않게)
    row=max(0,min(row, parent.range.count-1))

// 4. 값이 실제로 변했을 때만 SwiftUI 상태 업데이트
let newValue= parent.range.lowerBound+ row
if parent.selection!= newValue {
        parent.selection= newValue
    }
}

근데….. 이렇게 했을때 실시간 동기화는 해결했지만 UIPicker의 delegate를 뻇어왔기 때문에 손을 놨을때 자동으로 가까운 숫자로 가는 “스냅” 기능이 비활성화되는 문제가 발생했습니다.

그래서,,,,,,

스냅 기능을 아예 만들기….

// 사용자가 드래그를 끝내는 순간 호출됨
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    
    // 1. 예상되는 최종 정지 위치 (y좌표)
    let expectedY = targetContentOffset.pointee.y
    
    // 2. 행 높이
    let rowHeight = 44.adjustedH
    
    // 3. 가장 가까운 행의 인덱스를 계산
    var index = round(expectedY / rowHeight)
    
    // 4. 인덱스가 범위를 벗어나지 않도록 안전 장치 (0 ~ 마지막 인덱스)
    index = max(0, min(index, CGFloat(parent.range.count - 1)))
    
    // 5. 스냅 위치 보정
    // 시스템이 원래 멈추려던 위치 계산한 '정확한 칸 위치'로 덮어씌우기
    targetContentOffset.pointee = CGPoint(x: 0, y: index * rowHeight)
}

@soseoyo12 soseoyo12 requested a review from a team January 20, 2026 14:30
@soseoyo12 soseoyo12 self-assigned this Jan 20, 2026
@soseoyo12 soseoyo12 requested review from sum130, wotjs020708 and y-eonee and removed request for a team January 20, 2026 14:30
@soseoyo12 soseoyo12 linked an issue Jan 20, 2026 that may be closed by this pull request
1 task
@soseoyo12 soseoyo12 added the Feature 새로운 기능 구현 및 API 연결 label Jan 20, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

이 PR은 Splash 에셋의 메타데이터 파일을 삭제하고, CherrishPicker 컴포넌트를 수정하여 스크롤 인터랙션을 기반으로 피커 값을 실시간으로 반영하도록 개선합니다.

Changes

Cohort / File(s) 변경 요약
에셋 메타데이터 제거
Cherrish-iOS/Cherrish-iOS/Assets.xcassets/Splash/Contents.json
Splash 에셋 카탈로그의 메타데이터 파일 삭제 (6줄)
피커 스크롤 위임 및 상태 관리
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishPicker.swift
Coordinator가 UIScrollViewDelegate를 추가로 준수하여 중첩 UIScrollView의 스크롤 이벤트를 감지하고, 스크롤 델타를 계산하여 피커 행을 실시간으로 업데이트합니다. 초기 콘텐츠 오프셋, 행 높이, 스크롤 상태를 추적하는 상태 변수들이 추가되었으며, 원본 위임자에게 이벤트를 포워드하여 기존 동작을 보존합니다. (+66/-4)

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant ScrollView as UIScrollView
    participant Coordinator as Coordinator<br/>(UIScrollViewDelegate)
    participant Picker as UIPickerView
    participant State as State

    User->>ScrollView: 피커 드래그 시작
    ScrollView->>Coordinator: scrollViewWillBeginDragging()
    Coordinator->>State: 초기 상태 저장<br/>(contentOffset, isScrolling=true)
    
    loop 스크롤 진행
        User->>ScrollView: 드래그 계속
        ScrollView->>Coordinator: scrollViewDidScroll()
        Coordinator->>Coordinator: contentOffset 델타 계산
        Coordinator->>Coordinator: 대응 행 번호 도출
        Coordinator->>State: selectedRow 업데이트
        Coordinator->>Picker: 선택 행 업데이트
    end
    
    User->>ScrollView: 드래그 종료
    ScrollView->>Coordinator: scrollViewDidEndDragging()
    Coordinator->>State: isScrolling=false
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • Style/#56 커스텀 피커 #57: CherrishPicker.swift의 PickerViewRepresentable/Coordinator 구현을 동일하게 수정하며, 피커 선택 및 위임자 동작과 관련된 변경사항을 공유합니다.

Suggested reviewers

  • wotjs020708
  • sum130
  • y-eonee
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 'Feat/#109 피커 실시간 변동 적용'은 변경사항의 주요 내용인 UIPickerView의 실시간 선택값 반영을 명확하게 설명하고 있습니다.
Description check ✅ Passed PR 설명은 구현된 기능(실시간 동기화, 스냅 기능), 코드 설명, 시뮬레이터 GIF 등을 포함하여 변경사항과 관련이 있습니다.
Linked Issues check ✅ Passed PR의 구현 내용이 #109의 요구사항(UIPickerView 선택값 실시간 반영)을 완전히 충족하고 있습니다. 스크롤 중 값 변경 감지 및 스냅 기능 구현이 모두 포함되어 있습니다.
Out of Scope Changes check ✅ Passed CherrishPicker.swift의 변경사항은 피커 실시간 동기화 기능 구현에 필요한 범위 내이며, Contents.json 삭제는 자산 카탈로그 메타데이터 정리로 기능 변경 없는 범위 내 변경입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#109-pickerRealtime

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishPicker.swift`:
- Around line 46-48: 현재 findAndConnectScrollViews(in: picker, coordinator:
context.coordinator)가 UIPickerView 내부에서 UIScrollView를 찾아 연결하는데 내부 구조에 의존하므로, 찾지
못할 때의 폴백을 추가하세요: 변경할 위치는 findAndConnectScrollViews(in:coordinator:) 구현부와
호출부(DispatchQueue.main.async 블록)이며, 함수가 UIScrollView를 찾지 못하면 false를 반환하거나
completion 을 호출하도록 하고, 그 경우 UIPickerView 자체에 UIPanGestureRecognizer 또는
UITapGestureRecognizer를 추가하여 context.coordinator(예:
coordinator.handlePan/handleTap 같은 기존 핸들러)를 연결하도록 구현하세요; 이렇게 하면 내부 서브뷰 탐색에 실패할
때도 사용자 입력을 캡처할 수 있습니다.
- Line 80: There is a duplicated row height value — remove the local
constant/duplicate definition inside pickerView(_:rowHeightForComponent:) and
make that method return the single property rowHeight (private let rowHeight:
CGFloat = 44.adjustedH) so the component uses the central constant; update any
references in pickerView(_:rowHeightForComponent:) to use rowHeight and delete
the redundant duplicate.
- Around line 53-61: 현재 findAndConnectScrollViews(in:coordinator:)가 여러
UIScrollView를 순회할 때 coordinator.originalScrollViewDelegate를 덮어써 첫 발견 델리게이트를 잃을 수
있습니다; 수정 방법은 Coordinator에 원본 델리게이트를 하나가 아닌 스크롤뷰별로 저장하도록 변경하고(예:
originalScrollViewDelegates: [UIScrollView: UIScrollViewDelegate?])
findAndConnectScrollViews에서는 각 scrollView에 대해
coordinator.originalScrollViewDelegates[scrollView] = scrollView.delegate 후
scrollView.delegate = coordinator로 설정하도록 바꾸거나, 단순히 첫번째 UIScrollView를 찾은 뒤 탐색을
중단하도록 로직을 변경하여 덮어쓰기를 방지하세요; 이후 원복 로직(예: disconnect/teardown)도 Coordinator의 매핑을
사용해 각 스크롤뷰의 원본 델리게이트를 복원하도록 업데이트해야 합니다.

Comment on lines +46 to +48
DispatchQueue.main.async {
self.findAndConnectScrollViews(in: picker, coordinator: context.coordinator)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

내부 UIPickerView 구조에 의존하는 구현

UIPickerView의 내부 서브뷰 계층 구조는 비공개 API이므로, iOS 버전 업데이트 시 UIScrollView를 찾지 못할 수 있습니다. 이 접근 방식은 현재 동작하지만 향후 iOS 업데이트에서 깨질 수 있으므로, 스크롤 뷰를 찾지 못하는 경우에 대한 폴백(fallback) 로직 추가를 권장합니다.

🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishPicker.swift`
around lines 46 - 48, 현재 findAndConnectScrollViews(in: picker, coordinator:
context.coordinator)가 UIPickerView 내부에서 UIScrollView를 찾아 연결하는데 내부 구조에 의존하므로, 찾지
못할 때의 폴백을 추가하세요: 변경할 위치는 findAndConnectScrollViews(in:coordinator:) 구현부와
호출부(DispatchQueue.main.async 블록)이며, 함수가 UIScrollView를 찾지 못하면 false를 반환하거나
completion 을 호출하도록 하고, 그 경우 UIPickerView 자체에 UIPanGestureRecognizer 또는
UITapGestureRecognizer를 추가하여 context.coordinator(예:
coordinator.handlePan/handleTap 같은 기존 핸들러)를 연결하도록 구현하세요; 이렇게 하면 내부 서브뷰 탐색에 실패할
때도 사용자 입력을 캡처할 수 있습니다.

Comment on lines +53 to +61
private func findAndConnectScrollViews(in view: UIView, coordinator: Coordinator) {
for subview in view.subviews {
if let scrollView = subview as? UIScrollView {
coordinator.originalScrollViewDelegate = scrollView.delegate
scrollView.delegate = coordinator
}
findAndConnectScrollViews(in: subview, coordinator: coordinator)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

여러 UIScrollView가 존재할 경우 원본 델리게이트 손실 가능

UIPickerView 내부에 여러 UIScrollView가 있을 경우, originalScrollViewDelegate가 마지막에 발견된 스크롤 뷰의 델리게이트로 덮어씌워집니다. 첫 번째 스크롤 뷰를 찾은 후 탐색을 중단하거나, 각 스크롤 뷰별로 원본 델리게이트를 저장하는 것이 안전합니다.

♻️ 제안된 수정사항
 private func findAndConnectScrollViews(in view: UIView, coordinator: Coordinator) {
     for subview in view.subviews {
         if let scrollView = subview as? UIScrollView {
             coordinator.originalScrollViewDelegate = scrollView.delegate
             scrollView.delegate = coordinator
+            return  // 첫 번째 스크롤 뷰만 연결
         }
         findAndConnectScrollViews(in: subview, coordinator: coordinator)
     }
 }
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishPicker.swift`
around lines 53 - 61, 현재 findAndConnectScrollViews(in:coordinator:)가 여러
UIScrollView를 순회할 때 coordinator.originalScrollViewDelegate를 덮어써 첫 발견 델리게이트를 잃을 수
있습니다; 수정 방법은 Coordinator에 원본 델리게이트를 하나가 아닌 스크롤뷰별로 저장하도록 변경하고(예:
originalScrollViewDelegates: [UIScrollView: UIScrollViewDelegate?])
findAndConnectScrollViews에서는 각 scrollView에 대해
coordinator.originalScrollViewDelegates[scrollView] = scrollView.delegate 후
scrollView.delegate = coordinator로 설정하도록 바꾸거나, 단순히 첫번째 UIScrollView를 찾은 뒤 탐색을
중단하도록 로직을 변경하여 덮어쓰기를 방지하세요; 이후 원복 로직(예: disconnect/teardown)도 Coordinator의 매핑을
사용해 각 스크롤뷰의 원본 델리게이트를 복원하도록 업데이트해야 합니다.

class Coordinator: NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
class Coordinator: NSObject, UIPickerViewDelegate, UIPickerViewDataSource, UIScrollViewDelegate {
var parent: PickerViewRepresentable
private let rowHeight: CGFloat = 44.adjustedH
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

행 높이 상수 중복

rowHeight가 80번째 줄과 115번째 줄(pickerView(_:rowHeightForComponent:))에서 각각 정의되어 있습니다. 하나의 상수로 통일하면 유지보수성이 향상됩니다.

♻️ 제안된 수정사항
 func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
-    44.adjustedH
+    rowHeight
 }
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishPicker.swift`
at line 80, There is a duplicated row height value — remove the local
constant/duplicate definition inside pickerView(_:rowHeightForComponent:) and
make that method return the single property rowHeight (private let rowHeight:
CGFloat = 44.adjustedH) so the component uses the central constant; update any
references in pickerView(_:rowHeightForComponent:) to use rowHeight and delete
the redundant duplicate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인부탁~~

Copy link
Contributor

@y-eonee y-eonee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMG_9691

Comment on lines +55 to +57
if let scrollView = subview as? UIScrollView {
coordinator.originalScrollViewDelegate = scrollView.delegate
scrollView.delegate = coordinator
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와이거개고능하네요

class Coordinator: NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
class Coordinator: NSObject, UIPickerViewDelegate, UIPickerViewDataSource, UIScrollViewDelegate {
var parent: PickerViewRepresentable
private let rowHeight: CGFloat = 44.adjustedH
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인부탁~~

originalScrollViewDelegate?.scrollViewDidEndDecelerating?(scrollView)
}

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개행 한번만여

Comment on lines +149 to +153
let deltaOffset = scrollView.contentOffset.y - initialContentOffset
let deltaRow = Int(round(deltaOffset / rowHeight))
let newRow = initialRow + deltaRow
let clampedRow = max(0, min(newRow, parent.range.count - 1))
let newSelection = parent.range.lowerBound + clampedRow
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와우어렵네요 고능하다

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와우띵..

Copy link
Contributor

@wotjs020708 wotjs020708 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 어려운걸.. 고생하셨습니다 리뷰 확인 부탁드려욤~

Copy link
Contributor

@sum130 sum130 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어려운디 수고했어용 감자합니다~~~ 어푸푸!!

Comment on lines +149 to +153
let deltaOffset = scrollView.contentOffset.y - initialContentOffset
let deltaRow = Int(round(deltaOffset / rowHeight))
let newRow = initialRow + deltaRow
let clampedRow = max(0, min(newRow, parent.range.count - 1))
let newSelection = parent.range.lowerBound + clampedRow
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와우띵..

@soseoyo12 soseoyo12 merged commit a128ce1 into develop Jan 20, 2026
2 checks passed
@soseoyo12 soseoyo12 deleted the feat/#109-pickerRealtime branch January 20, 2026 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature 새로운 기능 구현 및 API 연결 성용👶

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 피커 값 실시간 반영

4 participants