Skip to content

Instantly share code, notes, and snippets.

@Kirill12a
Created December 13, 2023 18:52
Show Gist options
  • Save Kirill12a/3016cf6d0099790d6d655e104b819109 to your computer and use it in GitHub Desktop.
Save Kirill12a/3016cf6d0099790d6d655e104b819109 to your computer and use it in GitHub Desktop.
Custom slider for changing time on a time scale (hours, days, weeks, months, year)
import SwiftUI
enum SliderConstants {
static let sliderHeight: CGFloat = 60
static let horizontalPadding: CGFloat = 10
static let trackHeight: CGFloat = 8
static let largeThumbSize: CGFloat = 24
static let regularThumbSize: CGFloat = 16
static let keyIntervalThreshold: CGFloat = 4
static let labelOffsetY: CGFloat = -30
static let thumbOffsetY: CGFloat = 25
static let pointDiameter: CGFloat = 15
}
struct KeyInterval: Hashable, Identifiable{
let id = UUID()
let hours: CGFloat
let label: String
}
struct TimePicker: View {
@State private var sliderValue: CGFloat = 0.0
let maxHours: CGFloat = 100
let keyIntervals: [KeyInterval] = [
KeyInterval(hours: 0, label: "1 час"),
KeyInterval(hours: 25, label: "1 день"),
KeyInterval(hours: 50, label: "1 неделя"),
KeyInterval(hours: 75, label: "1 месяц"),
KeyInterval(hours: 100, label: "1 год")
]
var body: some View {
VStack {
SliderView(
sliderValue: $sliderValue,
maxHours: maxHours,
keyIntervals: keyIntervals
)
.frame(height: SliderConstants.sliderHeight)
}
.padding()
}
}
// Вид слайдера
struct SliderView: View {
@Binding var sliderValue: CGFloat
let maxHours: CGFloat
let keyIntervals: [KeyInterval]
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
SliderTrackWithPoints(
sliderValue: $sliderValue,
maxHours: maxHours,
keyIntervals: keyIntervals,
geometry: geometry
)
SliderThumb(
sliderValue: $sliderValue,
maxHours: maxHours,
geometry: geometry,
keyIntervals: keyIntervals,
onCrossingInterval: handleIntervalCrossing
)
}
}
.padding(.horizontal, SliderConstants.horizontalPadding)
.frame(height: SliderConstants.sliderHeight)
}
// Обработка пересечения ключевого интервала
private func handleIntervalCrossing() {
let impactMed = UIImpactFeedbackGenerator(style: .medium)
impactMed.impactOccurred()
}
}
// Вид трека слайдера с точками
struct SliderTrackWithPoints: View {
@Binding var sliderValue: CGFloat
let maxHours: CGFloat
let keyIntervals: [KeyInterval]
let geometry: GeometryProxy
var body: some View {
ZStack {
CustomSliderTrack(
sliderValue: sliderValue,
maxHours: maxHours,
keyIntervals: keyIntervals)
.stroke(Color.gray.opacity(0.2), lineWidth: SliderConstants.trackHeight)
.overlay(
CustomSliderTrack(
sliderValue: sliderValue,
maxHours: maxHours,
keyIntervals: keyIntervals)
.stroke(Color.blue.opacity(0.8), lineWidth: SliderConstants.trackHeight)
.mask(
Rectangle()
.frame(width: sliderPosition(sliderValue, geometry: geometry) * 2 + 15)
.offset(x: -geometry.size.width / 2 - 7.5)
)
)
// Отображение меток на ключевых интервалах
ForEach(keyIntervals, id: \.self) { interval in
Text(interval.label)
.font(.caption)
.foregroundColor(.black)
.position(
x: sliderPosition(interval.hours, geometry: geometry),
y: geometry.size.height / 2 + SliderConstants.labelOffsetY)
}
}
}
// Вычисление позиции слайдера
private func sliderPosition(
_ value: CGFloat,
geometry: GeometryProxy
) -> CGFloat {
let proportion = min(max(value, 0), maxHours) / maxHours
return proportion * geometry.size.width
}
}
// Вид ползунка слайдера
struct SliderThumb: View {
@Binding var sliderValue: CGFloat
let maxHours: CGFloat
let geometry: GeometryProxy
let keyIntervals: [KeyInterval]
var onCrossingInterval: () -> Void
var isOnKeyInterval: Bool {
keyIntervals.contains { abs($0.hours - sliderValue) < SliderConstants.keyIntervalThreshold }
}
var body: some View {
ZStack {
// Текст под ручкой слайдера
Text(formattedTime)
.font(.caption)
.foregroundColor(.black)
.offset(y: SliderConstants.thumbOffsetY)
// Ручка слайдера
Circle()
.frame(width: isOnKeyInterval ? SliderConstants.largeThumbSize : SliderConstants.regularThumbSize, height: isOnKeyInterval ? SliderConstants.largeThumbSize : SliderConstants.regularThumbSize)
.foregroundColor(.blue)
.animation(.easeInOut, value: isOnKeyInterval)
}
.position(
x: sliderPosition(sliderValue, geometry: geometry),
y: geometry.size.height / 2
)
.gesture(
DragGesture()
.onChanged { value in
let previousValue = sliderValue
sliderValue = valueFromPosition(value.location.x, geometry: geometry)
if crossedInterval(previousValue: previousValue, newValue: sliderValue) {
onCrossingInterval()
}
}
)
}
// Форматирование времени под ручкой
var formattedTime: String {
let proportion = sliderValue / maxHours
switch proportion {
case 0..<0.25: return "\(max(1, Int(proportion / 0.25 * 24))) часов"
case 0.25..<0.5: return "\(max(1, Int((proportion - 0.25) / 0.25 * 7))) дней"
case 0.5..<0.75: return "\(max(1, Int((proportion - 0.5) / 0.25 * 4))) недель"
case 0.75..<1.0: return "\(max(1, Int((proportion - 0.75) / 0.25 * 12))) месяцев"
default: return "1 год"
}
}
// Вычисление позиции и значения слайдера
private func sliderPosition(_ value: CGFloat, geometry: GeometryProxy) -> CGFloat {
let proportion = min(max(value, 0), maxHours) / maxHours
return proportion * geometry.size.width
}
private func valueFromPosition(_ position: CGFloat, geometry: GeometryProxy) -> CGFloat {
let proportion = position / geometry.size.width
return min(max(proportion * maxHours, 0), maxHours)
}
// Проверка пересечения нового ключевого интервала
private func crossedInterval(previousValue: CGFloat, newValue: CGFloat) -> Bool {
let previousInterval = keyIntervals.firstIndex(where: { previousValue < $0.hours })
let newInterval = keyIntervals.firstIndex(where: { newValue < $0.hours })
return previousInterval != newInterval
}
}
// Вид пользовательского трека слайдера
struct CustomSliderTrack: Shape {
var sliderValue: CGFloat
let maxHours: CGFloat
let keyIntervals: [KeyInterval]
func path(in rect: CGRect) -> Path {
var path = Path()
// Основная линия трека
path.addRoundedRect(
in: CGRect(
x: rect.minX,
y: rect.midY - SliderConstants.trackHeight / 2,
width: rect.width,
height: SliderConstants.trackHeight
),
cornerSize: CGSize(
width: SliderConstants.trackHeight / 2,
height: SliderConstants.trackHeight / 2
)
)
// Точки на ключевых интервалах
for interval in keyIntervals {
let xPosition = interval.hours / maxHours * rect.width
path.addEllipse(
in: CGRect(
x: xPosition - SliderConstants.pointDiameter / 2,
y: rect.midY - SliderConstants.pointDiameter / 2,
width: SliderConstants.pointDiameter,
height: SliderConstants.pointDiameter
)
)
}
return path
}
}
// Предпросмотр
struct TimePicker_Previews: PreviewProvider {
static var previews: some View {
TimePicker()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment