Conversation
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthroughSwiftUI에서 UIKit의 UIPickerView를 감싼 커스텀 휠 피커 Changes
Sequence Diagram(s)sequenceDiagram
participant SwiftUI as CustomWheelPicker (SwiftUI View)
participant Representable as PickerViewRepresentable (UIViewRepresentable)
participant UIKit as UIPickerView
participant Coordinator as Coordinator
SwiftUI->>Representable: body -> makeUIView(context:)
Representable->>UIKit: 생성 및 초기 설정 (delegate/dataSource = Coordinator)
Representable->>Coordinator: context 연결
Representable->>UIKit: selectRow(initialIndex)
UIKit->>Coordinator: 사용자 스크롤/선택 이벤트 (didSelectRow)
Coordinator->>Representable: 업데이트 바인딩(selection = rowIndex)
Representable->>SwiftUI: 바인딩 변경 반영 (selection 바뀜)
SwiftUI->>Representable: selection 외부 변경 -> updateUIView(_:context:)
Representable->>UIKit: selectRow(updatedIndex, animated: true)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 분 Possibly related PRs
Suggested reviewers
개요SwiftUI 환경에서 UIKit의 UIPickerView를 활용한 커스텀 휠 피커 컴포넌트 변경 사항
예상 코드 리뷰 노력🎯 2 (Simple) | ⏱️ ~12 분 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro ⛔ Files ignored due to path filters (6)
📒 Files selected for processing (10)
✏️ Tip: You can disable this entire section by setting 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In
@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishPicker.swift:
- Around line 39-40: When selecting the initial row in makeUIView of
CherrishPicker, avoid the unwanted scroll animation by passing animated: false
to picker.selectRow; locate the selection logic that computes let row =
selection - range.lowerBound and change the call picker.selectRow(row,
inComponent: 0, animated: true) to use animated: false so the initial state is
set without animation.
- Around line 75-86: The pickerView.subviews.forEach background-clear loop
inside pickerView(_:viewForRow:forComponent:reusing:) is inefficient because
that delegate method is called frequently; move the one-time subview background
initialization into PickerViewRepresentable.makeUIView (or guard it with a flag)
and remove the loop from the viewForRow implementation so pickerView,
viewForRow, and makeUIView no longer repeatedly clear subview backgrounds during
scrolling.
- Around line 34-43: In makeUIView, guard against selection being outside range
by clamping selection to range.lowerBound...range.upperBound (or
range.upperBound - 1 for half-open ranges) before computing row; replace direct
use of selection in the row calculation with a clampedSelection variable,
compute row = clampedSelection - range.lowerBound, and then call
picker.selectRow(row, inComponent: 0, animated: true); ensure you reference the
makeUIView(context:) method, the selection binding, and the range property when
adding this defensive logic.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (1)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishPicker.swift
🧰 Additional context used
🧬 Code graph analysis (1)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishPicker.swift (3)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Shadow.swift (1)
body(9-17)Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (2)
gray300(21-23)gray1000(49-51)Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishButton.swift (2)
backgroundColor(78-87)textColor(89-98)
🔇 Additional comments (2)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishPicker.swift (2)
11-28: LGTM!SwiftUI View 래퍼의 구조가 적절합니다. ZStack을 사용한 레이아웃과
.clipped()로 오버플로우를 방지하는 방식이 잘 구현되어 있습니다.
56-61: Coordinator의 parent 참조를 통한 업데이트 방식 검증 필요.
Coordinator가parent를var로 보유하고 있지만, SwiftUI의 재생성 사이클에서parent참조가 오래된(stale) 상태가 될 수 있습니다. 일반적인 패턴은updateUIView에서 coordinator의 parent를 갱신하는 것입니다.♻️ 권장 패턴
func updateUIView(_ uiView: UIPickerView, context: Context) { + context.coordinator.parent = self let row = selection - range.lowerBound if uiView.selectedRow(inComponent: 0) != row { uiView.selectRow(row, inComponent: 0, animated: true) } }
| func makeUIView(context: Context) -> UIPickerView { | ||
| let picker = UIPickerView() | ||
| picker.delegate = context.coordinator | ||
| picker.dataSource = context.coordinator | ||
|
|
||
| let row = selection - range.lowerBound | ||
| picker.selectRow(row, inComponent: 0, animated: true) | ||
|
|
||
| return picker | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
selection이 range 범위를 벗어날 경우 방어 로직 고려.
selection이 range 범위 밖의 값으로 바인딩되면 음수 또는 범위를 초과하는 row 인덱스가 계산될 수 있습니다. 이는 예기치 않은 동작을 유발할 수 있으므로 클램핑(clamping) 처리를 권장합니다.
♻️ 수정 제안
func makeUIView(context: Context) -> UIPickerView {
let picker = UIPickerView()
picker.delegate = context.coordinator
picker.dataSource = context.coordinator
- let row = selection - range.lowerBound
+ let clampedSelection = min(max(selection, range.lowerBound), range.upperBound)
+ let row = clampedSelection - range.lowerBound
picker.selectRow(row, inComponent: 0, animated: false)
return picker
}🤖 Prompt for AI Agents
In
@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishPicker.swift
around lines 34 - 43, In makeUIView, guard against selection being outside range
by clamping selection to range.lowerBound...range.upperBound (or
range.upperBound - 1 for half-open ranges) before computing row; replace direct
use of selection in the row calculation with a clampedSelection variable,
compute row = clampedSelection - range.lowerBound, and then call
picker.selectRow(row, inComponent: 0, animated: true); ensure you reference the
makeUIView(context:) method, the selection binding, and the range property when
adding this defensive logic.
| let row = selection - range.lowerBound | ||
| picker.selectRow(row, inComponent: 0, animated: true) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
초기 선택 시 애니메이션 불필요.
makeUIView에서 초기 행을 선택할 때는 animated: false를 사용하는 것이 자연스럽습니다. 뷰가 처음 생성될 때 스크롤 애니메이션이 보이는 것은 의도치 않은 UX일 수 있습니다.
♻️ 수정 제안
- picker.selectRow(row, inComponent: 0, animated: true)
+ picker.selectRow(row, inComponent: 0, animated: false)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let row = selection - range.lowerBound | |
| picker.selectRow(row, inComponent: 0, animated: true) | |
| let row = selection - range.lowerBound | |
| picker.selectRow(row, inComponent: 0, animated: false) |
🤖 Prompt for AI Agents
In
@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishPicker.swift
around lines 39 - 40, When selecting the initial row in makeUIView of
CherrishPicker, avoid the unwanted scroll animation by passing animated: false
to picker.selectRow; locate the selection logic that computes let row =
selection - range.lowerBound and change the call picker.selectRow(row,
inComponent: 0, animated: true) to use animated: false so the initial state is
set without animation.
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishPicker.swift
Show resolved
Hide resolved
wotjs020708
left a comment
There was a problem hiding this comment.
SwiftUI에서 UIKit 연결하느라 고생하셨으욤 어푸푸 드립니다.
| Coordinator(self) | ||
| } | ||
|
|
||
| class Coordinator: NSObject, UIPickerViewDelegate, UIPickerViewDataSource { |
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishPicker.swift
Show resolved
Hide resolved
sum130
left a comment
There was a problem hiding this comment.
새로운 내용 공부하고 정리하느라 고생했으요 굿
근데 저번 피알에도 달았지만 gif로 구현화면 꾸워주면 좋을 듯......... 직접 보면 좋겠슴요
| struct CustomWheelPicker: View { | ||
| @Binding var selection: Int | ||
| let range: ClosedRange<Int> | ||
| var width: CGFloat = 74.adjustedW |
🔗 연결된 이슈
📄 작업 내용
💻 주요 코드 설명
전체 구조
크게 두개의 struct인데
CustomWheelPicker: Picker에 커스텀 디자인은 래핑하는 역할!PickerViewRepresentable: UIKit 컴포넌트를 SwiftUI에 이식하기 위해 필요CustomWheelPicker
코드를 보면 ZStack으로 UI래핑중!!
PickerViewRepresentable
UIKit에서 SwiftUI로 이식하려면 막 하면 안되겠져
따라서
UIViewRepresentable라는 프로토콜을 채택해야함!!이 프로토콜을 채택하면 어떤것을 구현해야하느냐 ->
필수 구현
네~ 화면 그려야되고 상태 바뀌면 반영해야되니까 두개 전부 구현해야합니다
하나씩 설명해볼게요
makeUIView
coordinator: 아래에 나오니까 아래에서 확인!!
let row = selection - range.lowerBound: row(행)랑 selection을 구분해줘야하는데 index를 다룰때에는 row를 쓰고 사용자가 선택하는 순서는 selection을 씁니다 근데 index는 0부터 selection은 1부터 시작해서 이 값을 보정해주는 과정입니다picker.selectRow(row, inComponent: 0, animated: true): 피커 처음 만들때 초기값을 어떤 걸 줄지!inComponent: 몇번째 열인지! 다중열인 피커도 있기 때문에 선택해줘야합니다. 저의 케이스에서는 하나 밖에 없기때문에 0animated: 행이 강제로 선택될때(버튼으로 row값 조절 등) 뚝뚝 끊기면서 선택될지 아니면 에니메이션을 줄지 여부updateUIView
SwiftUI의
@Binding var selection값이 바뀔 때마다 호출됩니다.selectedRow(inComponent: 0): 피커가 현재 가리키고 있는 행 인덱스 반환."피커의 현재 위치"랑 "selection에서 계산한 위치"가 다를 때만 위치로 이동!
선택 구현
입니다.
static func dismantleUIView의 경우에는 커스텀 피커 구현에서 사용하지 않습니다.UIKit에선 뷰와 데이터 사이에 뷰컨이 열일을!! SwiftUI에선?
네~ 그 사이를 채워주는 역할을 담당하는것이 Coordinator입니다 (코디네이터 패턴아닙니다.)
만들어줘야겠져
위와 같이 델리게이트, 데이터소스 프로토콜을 채택하게 되는데
DataSource (필수 2개)
numberOfComponents(in:)— 필수pickerView(_:numberOfRowsInComponent:)— 필수Delegate (전부 optional)
didSelectRow— 선택 이벤트 받고 싶으면viewForRow— 커스텀 UI 원하면rowHeightForComponent— 높이 바꾸고 싶으면인데 각 각의 메서드를 설명해보도록 하겠습니다~
열을 몇개 사용할건지!
열에 행이 몇개인지
component를 매개변수로 받는 이유
만약 피커가 "년 | 월 | 일" 3열이면:
component == 0→ 년도 열 → "2000~2025니까 26개"component == 1→ 월 열 → "12개"component == 2→ 일 열 → "31개"component에 따라서 다른 값을 줘야하기 때문
근데 항상 component가 0이 들어오기 때문에 그냥
parent.range.count를 반환만약 여러개였으면
유저가 휠 굴려서 선택이 확정됐을 때. 휠이 멈추고 가운데에 딱 고정되는 순간. 호출
매개변수:
pickerView: 이벤트가 발생한 피커 자체row: 선택된 행의 인덱스 (0부터 시작)component: 몇 번째 열에서 선택됐는지 (여기선 열이 1개라 항상 0)피커가 각 행을 그릴 때마다. 화면에 보이는 행마다 한 번씩 호출
매개변수:
pickerView: 피커 자체row: 그리려는 행의 인덱스 (0부터)component: 몇 번째 열인지 (여기선 항상 0)reusing view: 재사용 가능한 뷰 (있으면 UIView, 없으면 nil) 이거 그냥 콜뷰쓸때 셀 재사용하는거랑 같음반환값:
UIView— 이 행에 표시할 뷰를 직접 만들어서 반환기본 피커는 그냥 텍스트만 보여줌 근데 이 메서드 쓰면 폰트, 색상, 이미지 등 완전 커스텀 가능
재사용 로직
view가 UILabel이면 → 그거 그대로 써view가 nil이거나 다른 타입이면 → 새 UILabel 생성사용법