Skip to content

Instantly share code, notes, and snippets.

@p0dee
Created May 17, 2020 11:57
Show Gist options
  • Save p0dee/81711b10246f82031172536a62890053 to your computer and use it in GitHub Desktop.
Save p0dee/81711b10246f82031172536a62890053 to your computer and use it in GitHub Desktop.
//
// Picker.swift
// Crypic
//
// Created by Takeshi Tanaka on 2020/05/16.
// Copyright © 2020 p0dee. All rights reserved.
//
import SwiftUI
struct DigitPicker: View {
private let cellHeight: CGFloat = 40
@State private var pickerHeight: CGFloat = 0
@State private var dragStartOffsetY: CGFloat?
@State private var latestOffsetY: CGFloat?
@Binding var selectedIndex: Int
private let digits = 0..<10
init(initialIndex: Binding<Int>) {
_selectedIndex = initialIndex
}
var body: some View {
ZStack(alignment: .top) {
VStack(spacing: 0) {
ForEach(self.digits) { index in
Cell(title: "\(index)", isSelected: self.isSelected(index))
.frame(height: self.cellHeight)
.animation(.linear(duration: 0.1))
}
}
.offset(x: 0, y: self.offsetY())
LinearGradient(gradient: Gradient(colors: [
Color(.sRGBLinear, white: 1, opacity: 1.0),
Color(.sRGBLinear, white: 1, opacity: 0.0),
Color(.sRGBLinear, white: 1, opacity: 0.0),
Color(.sRGBLinear, white: 1, opacity: 1.0)
]), startPoint: .top, endPoint: .bottom)
.frame(height: self.pickerHeight)
.disabled(true)
}
.frame(minHeight: cellHeight, idealHeight: cellHeight * 1.5, maxHeight: cellHeight * 3.5, alignment: .top) //idealいつ採用される? alignmentとは?
.clipped()
.background(
GeometryReader { proxy in
Rectangle()
.fill(Color.clear)
.handleGeometryProxy(proxy) { proxy in
self.pickerHeight = proxy.size.height
}
}
)
.gesture(
DragGesture()
.onChanged({ value in
if self.dragStartOffsetY == nil {
self.dragStartOffsetY = self.offsetY()
}
self.updateOffset(y: self.dragStartOffsetY! + value.translation.height)
})
.onEnded({ value in
self.dragStartOffsetY = nil
let y = self.offsetY()
let rounded = round(y / self.cellHeight) * self.cellHeight
withAnimation {
self.updateOffset(y: rounded, rounding: true)
}
}
)
)
}
private func offsetY() -> CGFloat {
return latestOffsetY ?? offsetY(forIndex: selectedIndex)
}
private func offsetY(forIndex index: Int) -> CGFloat {
let padding = (pickerHeight - cellHeight) / 2
return -(CGFloat(index) * cellHeight - padding)
}
private func index(forOffsetY y: CGFloat) -> Int {
let padding = (pickerHeight - cellHeight) / 2
return Int(round((padding - y) / cellHeight))
}
private func updateOffset(y: CGFloat, rounding: Bool = false) {
let minY = offsetY(forIndex: 0)
let maxY = offsetY(forIndex: 9)
let rounded = max(min(y, minY), maxY)
let bounce = (y - rounded) * 0.1
selectedIndex = index(forOffsetY: rounded)
if rounding {
latestOffsetY = offsetY(forIndex: selectedIndex)
} else {
latestOffsetY = rounded + bounce
}
}
private func isSelected(_ index: Int) -> Bool {
return selectedIndex == index
}
}
private extension DigitPicker {
struct Cell: View {
let title: String
let isSelected: Bool
var body: some View {
RoundedRectangle(cornerRadius: 12)
.fill(isSelected ? Color(white: 0.0, opacity: 0.2) : Color(white: 0.0, opacity: 0.0))
.background(
Text(title)
.fontWeight(isSelected ? .bold : .medium)
.font(.title)
.foregroundColor(isSelected ? .black : .gray)
)
.contentShape(Rectangle())
.scaleEffect(isSelected ? 1.0 : 0.8)
}
}
}
private extension View {
func handleGeometryProxy(_ proxy: GeometryProxy, handler: @escaping (GeometryProxy) -> (), onMainThread: Bool = true) -> some View {
if onMainThread {
DispatchQueue.main.async {
handler(proxy)
}
} else {
handler(proxy)
}
return self
}
}
struct Picker_Previews: PreviewProvider {
static var previews: some View {
HStack {
DigitPicker(initialIndex: .constant(0))
DigitPicker(initialIndex: .constant(1))
DigitPicker(initialIndex: .constant(5))
DigitPicker(initialIndex: .constant(8))
DigitPicker(initialIndex: .constant(9))
}
.previewLayout(.sizeThatFits)
.padding()
}
}
@p0dee
Copy link
Author

p0dee commented May 17, 2020

Result:
output

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment