๋ฐ์ํ
import SwiftUI
import SwiftUIIntrospect
enum BirthPickerType {
case year
case month
}
struct BirthPickerView: View {
let type: BirthPickerType
@Binding var selectedNumber: Int
let years = Array(1923...2022)
let months = Array(1...12)
@State private var originOffset: CGFloat = 0
@State private var lastOffset: CGFloat = 0
@State private var currentOffset: CGFloat = 0
@State private var isCheckedOriginOffset: Bool = false
@State private var isCheckedLastOffset: Bool = false
@State private var isScrolling: Bool = false
init(type: BirthPickerType, selectedNumber: Binding<Int>) {
self.type = type
_selectedNumber = selectedNumber
}
private var selection: [Int] {
switch type {
case .year:
return years
case .month:
return months
}
}
var body: some View {
ZStack {
doubleLine
ScrollViewReader { proxy in
ScrollView(.vertical, showsIndicators: false) {
scrollObservableView
scrollViewContents
}
.scrollStatusByIntrospect(isScrolling: $isScrolling)
.clipped()
.onPreferenceChange(ScrollOffsetKey.self) {
setOffset($0)
}
.onChange(of: isScrolling) { isScrolling in
if !isScrolling {
moveScroll(proxy)
}
}
.onAppear {
Task {
try await Task.sleep(nanoseconds: 50_000_000)
withAnimation {
proxy.scrollTo(selectedNumber, anchor: .center)
}
if !isCheckedLastOffset {
self.lastOffset = currentOffset
isCheckedLastOffset = true
}
}
}
}
}
.frame(width: 120, height: 120)
}
}
// MARK: - Private Views
extension BirthPickerView {
@ViewBuilder
private var doubleLine: some View {
VStack(spacing: 0) {
Rectangle()
.foregroundColor(.theme.grey030)
.frame(height: 2)
Spacer()
.frame(height: 40)
Rectangle()
.foregroundColor(.theme.grey030)
.frame(height: 2)
}
}
@ViewBuilder
private var scrollViewContents: some View {
LazyVStack(spacing: 0) {
ForEach(selection, id: \.self) { number in
Text(makeSelectedItemLabel(number))
.foregroundColor(number == selectedNumber ? .theme.grey080 : .theme.grey030)
.font(.headline2)
.frame(width: 120, height: 40)
.id(number)
}
}
.padding(.vertical, 56)
}
private var scrollObservableView: some View {
GeometryReader { proxy in
let offsetY = proxy.frame(in: .global).origin.y
Color.clear
.preference(
key: ScrollOffsetKey.self,
value: offsetY
)
.onAppear {
setOriginOffset(offsetY)
}
}
.frame(height: 0)
}
}
// MARK: - Private func
extension BirthPickerView {
// ๋ํ๋ ๋ ๋ทฐ์ ์ต์ด์์น๋ฅผ ์ ์ฅํ๋ ๋ก์ง
private func setOriginOffset(_ offset: CGFloat) {
guard !isCheckedOriginOffset else { return }
self.originOffset = offset
isCheckedOriginOffset = true
}
private func setOffset(_ offset: CGFloat) {
self.currentOffset = offset
}
private func moveScroll(_ proxy: ScrollViewProxy) {
let diff = lastOffset - currentOffset
let selectedIdx: Int
if diff <= 0 {
selectedIdx = 0
} else if diff > CGFloat(selection.count - 1) * 40 {
selectedIdx = selection.count - 1
} else {
selectedIdx = Int(diff / 40)
}
let selectedValue = selection[selectedIdx]
withAnimation(.easeInOut(duration: 0.2)) {
proxy.scrollTo(selectedValue, anchor: .center)
selectedNumber = selectedValue
}
}
private func makeSelectedItemLabel(_ num: Int) -> LocalizedStringKey {
switch type {
case .year:
return "\(String(num))"
case .month:
return "\(num)์"
}
}
}
// MARK: - PreferenceKey
struct ScrollOffsetKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value += nextValue()
}
}
#Preview {
BirthPickerView(type: .month, selectedNumber: .constant(1))
}
๋ฐ์ํ
'๐ iOS > SwiftUI' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
matchedGeometryEffect(Hero animation) (0) | 2024.08.09 |
---|---|
matchedGeometryEffect (0) | 2024.08.09 |
Introspect (0) | 2023.12.06 |
SwiftUI Charts๋ก ๋ฐฉ์ฌํ ์ฐจํธ๋ง๋ค๊ธฐ (0) | 2023.06.22 |
[์ฑ์คํ ์ด ๋ฆฌ์ ] Guideline 4.3 - Design - Spam (0) | 2023.03.14 |