import SwiftUI
import UIKit
struct ScrubberConfig2 {
var count:Int
var majorTickInterval:Int
var spacing:CGFloat
let labelFormatter: ((Int) -> String)?
init(count: Int, majorTickInterval: Int, spacing: CGFloat, labelFormatter: ((Int) -> String)? = nil) {
self.count = count
self.majorTickInterval = majorTickInterval
self.spacing = spacing
self.labelFormatter = labelFormatter
}
}
struct VerticalScrubber2: View {
var config: ScrubberConfig2
@Binding var value: Int
let tickHeight: CGFloat = 5 //
@State private var isUserInteracting: Bool = false // New state to track user interaction
@State private var isScrollPositionSet = false //
@State private var scrollPosition:Int?
var body: some View {
GeometryReader { geometry in
let verticalPadding = (geometry.size.height - tickHeight) / 2
ZStack(alignment: .trailing) {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: config.spacing) {
ForEach(0...config.count, id: \.self) { index in
horizontalTickMark(for: index)
.frame(height: tickHeight)
.id(index)
}
}
.frame(width: 80)
.scrollTargetLayout()
}
.defaultScrollAnchor(.center)
.scrollTargetBehavior(.viewAligned)
.scrollPosition(id: $scrollPosition, anchor: .center)
.contentMargins(.vertical, verticalPadding)
Capsule()
.frame(width: 32, height: 3)
.foregroundColor(.accentColor)
.shadow(color: .accentColor.opacity(0.3), radius: 3, x: 0, y: 1)
}
.frame(width: 100)
.onAppear {
scrollPosition = value
}
.onChange(of: value, { oldValue, newValue in
if scrollPosition != newValue, !isUserInteracting {
print("
scrollPosition = newValue
}
})
.onChange(of: scrollPosition, initial: true, { oldValue, newValue in
if let newValue = newValue, value != newValue, isUserInteracting {
print("
value = newValue
}
})
.onScrollPhaseChange { oldPhase, newPhase in
if newPhase == .interacting {
isUserInteracting = true
} else {
isUserInteracting = false
}
}
}
}
private func horizontalTickMark(for index: Int) -> some View {
let isMajorTick = index % config.majorTickInterval == 0
return HStack(spacing: 8) {
Rectangle()
.fill(isMajorTick ? Color.accentColor : Color.gray.opacity(0.5))
.frame(width: isMajorTick ? 24 : 12, height: isMajorTick ? 2 : 1)
if isMajorTick {
Text(labelText(for: index))
.font(.system(size: 12, weight: .medium))
.foregroundColor(.primary)
.fixedSize()
}
}
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.trailing, 8)
}
private func labelText(for index: Int) -> String {
if let formatter = config.labelFormatter {
return formatter(index)
} else {
// Default: show index / steps * 5
let tickValue = index
return "\(tickValue)"
}
}
}
struct VerticalScrubberPreview2: View {
@State private var value: Int = 0
private let config = ScrubberConfig2(count: 100, majorTickInterval: 5, spacing: 5)
var body: some View {
Text("Vertical Scrubber (0–100 in steps of 5)")
.font(.title2)
.padding()
HStack(spacing: 30) {
VerticalScrubber2(config: config, value: $value)
.frame(width:120, height: 300)
.background(Color(.systemBackground))
.border(Color.gray.opacity(0.3))
VStack {
Text("Current Value:")
.font(.headline)
Text("\(value)")
.font(.system(size: 36, weight: .bold))
.padding()
}
Spacer()
}
.padding()
}
}
#Preview {
VerticalScrubberPreview2()
}
Подробнее здесь: https://stackoverflow.com/questions/798 ... autoscroll
Мобильная версия