From c9cd755dc3885836ea48fdb41ea42e2585419912 Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Tue, 1 Aug 2023 12:13:01 +0100 Subject: [PATCH 01/12] Improve responsiveness of dragging thumbs --- .../Styles/Rectangular/RectangularPointSliderStyle.swift | 2 +- .../Styles/Horizontal/HorizontalRangeSliderStyle.swift | 8 ++++---- .../Styles/Vertical/VerticalRangeSliderStyle.swift | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/Sliders/PointSlider/Styles/Rectangular/RectangularPointSliderStyle.swift b/Sources/Sliders/PointSlider/Styles/Rectangular/RectangularPointSliderStyle.swift index 6b0e9e2..bb1fa57 100644 --- a/Sources/Sliders/PointSlider/Styles/Rectangular/RectangularPointSliderStyle.swift +++ b/Sources/Sliders/PointSlider/Styles/Rectangular/RectangularPointSliderStyle.swift @@ -79,7 +79,7 @@ public struct RectangularPointSliderStyle: PointSlider ) ) .gesture( - DragGesture() + DragGesture(minimumDistance: 0) .onChanged { gestureValue in configuration.onEditingChanged(true) diff --git a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift index 5ceb961..b5cbbfd 100644 --- a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift +++ b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift @@ -49,7 +49,7 @@ public struct HorizontalRangeSliderStyle Date: Tue, 1 Aug 2023 12:21:05 +0100 Subject: [PATCH 02/12] Improve responsiveness of dragging thumbs --- .../HorizontalRangeSliderStyle.swift | 142 +++++++++--------- 1 file changed, 74 insertions(+), 68 deletions(-) diff --git a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift index b5cbbfd..6cde021 100644 --- a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift +++ b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift @@ -45,47 +45,50 @@ public struct HorizontalRangeSliderStyle Date: Tue, 1 Aug 2023 12:24:43 +0100 Subject: [PATCH 03/12] Improve responsiveness of dragging thumbs --- .../Styles/Horizontal/HorizontalRangeSliderStyle.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift index 6cde021..ccc729a 100644 --- a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift +++ b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift @@ -47,7 +47,7 @@ public struct HorizontalRangeSliderStyle Date: Mon, 7 Aug 2023 22:07:23 +0100 Subject: [PATCH 04/12] Add interactive scrubbing --- Sources/Sliders/Base/SliderGestureState.swift | 53 ++++++ Sources/Sliders/RangeSlider/RangeSlider.swift | 16 +- .../Style/RangeSliderStyleConfiguration.swift | 6 +- .../HorizontalRangeSliderStyle.swift | 163 +++++++++--------- .../Styles/RangeSliderOptions.swift | 3 +- .../Style/ValueSliderStyleConfiguration.swift | 7 +- .../HorizontalValueSliderStyle.swift | 89 +++++----- .../Styles/ValueSliderOptions.swift | 3 +- Sources/Sliders/ValueSlider/ValueSlider.swift | 15 +- 9 files changed, 218 insertions(+), 137 deletions(-) create mode 100644 Sources/Sliders/Base/SliderGestureState.swift diff --git a/Sources/Sliders/Base/SliderGestureState.swift b/Sources/Sliders/Base/SliderGestureState.swift new file mode 100644 index 0000000..100442e --- /dev/null +++ b/Sources/Sliders/Base/SliderGestureState.swift @@ -0,0 +1,53 @@ +import Foundation +import SwiftUI + +public struct SliderGestureState: Equatable { + enum Speed: CGFloat { + case normal = 1 + case half = 0.5 + case quarter = 0.25 + case eighth = 0.125 + } + + let precisionScrubbing: Bool + var speed = Speed.normal + + private var lastOffset: CGFloat + private var accumulations: [Speed:CGFloat] = [ + .normal: 0 + ] + + var offset: CGFloat { + accumulations.reduce(0) { accum, tuple in + let (speed, value) = tuple + let appliedValue = precisionScrubbing ? speed.rawValue * value : value + return accum + appliedValue + } + } + + init(precisionScrubbing: Bool, initialOffset: CGFloat) { + self.precisionScrubbing = precisionScrubbing + self.lastOffset = initialOffset + } + + private func speed(crossAxisOffset: CGFloat) -> Speed { + if abs(crossAxisOffset) > 200 { + return .eighth + } else if abs(crossAxisOffset) > 150 { + return .quarter + } else if abs(crossAxisOffset) > 100 { + return .half + } else { + return .normal + } + } + + func updating(with offset: CGFloat, crossAxisOffset: CGFloat) -> Self { + var mutSelf = self + let speed = speed(crossAxisOffset: crossAxisOffset) + mutSelf.speed = speed + mutSelf.accumulations[speed] = (accumulations[speed] ?? 0) + offset - lastOffset + mutSelf.lastOffset = offset + return mutSelf + } +} diff --git a/Sources/Sliders/RangeSlider/RangeSlider.swift b/Sources/Sliders/RangeSlider/RangeSlider.swift index a49eac1..6f9aded 100644 --- a/Sources/Sliders/RangeSlider/RangeSlider.swift +++ b/Sources/Sliders/RangeSlider/RangeSlider.swift @@ -3,12 +3,18 @@ import SwiftUI public struct RangeSlider: View { @Environment(\.rangeSliderStyle) private var style @State private var dragOffset: CGFloat? + @GestureState private var lowerGestureState: SliderGestureState? + @GestureState private var upperGestureState: SliderGestureState? private var configuration: RangeSliderStyleConfiguration public var body: some View { self.style.makeBody(configuration: - self.configuration.with(dragOffset: self.$dragOffset) + self.configuration.with( + dragOffset: self.$dragOffset, + lowerGestureState: self.$lowerGestureState, + upperGestureState: self.$upperGestureState + ) ) } } @@ -37,7 +43,9 @@ extension RangeSlider { step: CGFloat(step), distance: CGFloat(distance.lowerBound) ... CGFloat(distance.upperBound), onEditingChanged: onEditingChanged, - dragOffset: .constant(0) + dragOffset: .constant(0), + lowerGestureState: .init(initialValue: nil), + upperGestureState: .init(initialValue: nil) ) ) } @@ -61,7 +69,9 @@ extension RangeSlider { step: CGFloat(step), distance: CGFloat(distance.lowerBound) ... CGFloat(distance.upperBound), onEditingChanged: onEditingChanged, - dragOffset: .constant(0) + dragOffset: .constant(0), + lowerGestureState: .init(initialValue: nil), + upperGestureState: .init(initialValue: nil) ) ) } diff --git a/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift b/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift index 639634c..44e1331 100644 --- a/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift +++ b/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift @@ -7,10 +7,14 @@ public struct RangeSliderStyleConfiguration { public let distance: ClosedRange public let onEditingChanged: (Bool) -> Void public var dragOffset: Binding + public var lowerGestureState: GestureState + public var upperGestureState: GestureState - func with(dragOffset: Binding) -> Self { + func with(dragOffset: Binding, lowerGestureState: GestureState, upperGestureState: GestureState) -> Self { var mutSelf = self mutSelf.dragOffset = dragOffset + mutSelf.lowerGestureState = lowerGestureState + mutSelf.upperGestureState = upperGestureState return mutSelf } } diff --git a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift index ccc729a..1defd15 100644 --- a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift +++ b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift @@ -16,6 +16,26 @@ public struct HorizontalRangeSliderStyle Void let onSelectUpper: () -> Void + private func lowerX(configuration: Self.Configuration, geometry: GeometryProxy) -> CGFloat { + distanceFrom( + value: configuration.range.wrappedValue.lowerBound, + availableDistance: geometry.size.width, + bounds: configuration.bounds, + leadingOffset: lowerThumbSize.width / 2, + trailingOffset: lowerThumbSize.width / 2 + ) + } + + private func upperX(configuration: Self.Configuration, geometry: GeometryProxy) -> CGFloat { + distanceFrom( + value: configuration.range.wrappedValue.upperBound, + availableDistance: geometry.size.width, + bounds: configuration.bounds, + leadingOffset: upperThumbSize.width / 2, + trailingOffset: upperThumbSize.width / 2 + ) + } + public func makeBody(configuration: Self.Configuration) -> some View { GeometryReader { geometry in ZStack { @@ -36,53 +56,23 @@ public struct HorizontalRangeSliderStyle SliderGestureState in + let x = upperX(configuration: configuration, geometry: geometry) + return SliderGestureState( + precisionScrubbing: options.contains(.precisionScrubbing), + initialOffset: value.location.x - x ) - } - - let computedUpperBound = valueFrom( - distance: gestureValue.location.x - (configuration.dragOffset.wrappedValue ?? 0), - availableDistance: geometry.size.width, - bounds: configuration.bounds, - step: configuration.step, - leadingOffset: self.lowerThumbSize.width + self.upperThumbSize.width / 2, - trailingOffset: self.upperThumbSize.width / 2 - ) - - configuration.range.wrappedValue = rangeFrom( - lowerBound: configuration.range.wrappedValue.lowerBound, - updatedUpperBound: computedUpperBound, - bounds: configuration.bounds, - distance: configuration.distance, - forceAdjacent: options.contains(.forceAdjacentValue) + }()).updating( + with: value.location.x, + crossAxisOffset: value.translation.height ) - } - .onEnded { _ in - configuration.dragOffset.wrappedValue = nil - configuration.onEditingChanged(false) }, TapGesture() .onEnded { _ in @@ -154,6 +114,49 @@ public struct HorizontalRangeSliderStyle Void public var dragOffset: Binding + public var gestureState: GestureState - public init(value: Binding, bounds: ClosedRange, step: CGFloat, onEditingChanged: @escaping (Bool) -> Void, dragOffset: Binding) { + public init(value: Binding, bounds: ClosedRange, step: CGFloat, onEditingChanged: @escaping (Bool) -> Void, dragOffset: Binding, gestureState: GestureState) { self.value = value self.bounds = bounds self.step = step self.onEditingChanged = onEditingChanged self.dragOffset = dragOffset + self.gestureState = gestureState } - func with(dragOffset: Binding) -> Self { + func with(dragOffset: Binding, gestureState: GestureState) -> Self { var mutSelf = self mutSelf.dragOffset = dragOffset + mutSelf.gestureState = gestureState return mutSelf } } diff --git a/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift b/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift index 5eb9d61..46155e4 100644 --- a/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift +++ b/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift @@ -7,6 +7,16 @@ public struct HorizontalValueSliderStyle: ValueSliderS private let thumbInteractiveSize: CGSize private let options: ValueSliderOptions + private func x(configuration: Self.Configuration, geometry: GeometryProxy) -> CGFloat { + distanceFrom( + value: configuration.value.wrappedValue, + availableDistance: geometry.size.width, + bounds: configuration.bounds, + leadingOffset: thumbSize.width / 2, + trailingOffset: thumbSize.width / 2 + ) + } + public func makeBody(configuration: Self.Configuration) -> some View { let track = self.track .environment(\.trackValue, configuration.value.wrappedValue) @@ -22,20 +32,16 @@ public struct HorizontalValueSliderStyle: ValueSliderS if self.options.contains(.interactiveTrack) { track.gesture( DragGesture(minimumDistance: 0) - .onChanged { gestureValue in - configuration.onEditingChanged(true) - let computedValue = valueFrom( - distance: gestureValue.location.x, - availableDistance: geometry.size.width, - bounds: configuration.bounds, - step: configuration.step, - leadingOffset: self.thumbSize.width / 2, - trailingOffset: self.thumbSize.width / 2 + .updating(configuration.gestureState) { value, state, transaction in + state = (state ?? { + SliderGestureState( + precisionScrubbing: options.contains(.precisionScrubbing), + initialOffset: 0 + ) + }()).updating( + with: value.location.x, + crossAxisOffset: value.translation.height ) - configuration.value.wrappedValue = computedValue - } - .onEnded { _ in - configuration.onEditingChanged(false) } ) } else { @@ -48,48 +54,41 @@ public struct HorizontalValueSliderStyle: ValueSliderS } .frame(minWidth: self.thumbInteractiveSize.width, minHeight: self.thumbInteractiveSize.height) .position( - x: distanceFrom( - value: configuration.value.wrappedValue, - availableDistance: geometry.size.width, - bounds: configuration.bounds, - leadingOffset: self.thumbSize.width / 2, - trailingOffset: self.thumbSize.width / 2 - ), + x: x(configuration: configuration, geometry: geometry), y: geometry.size.height / 2 ) .gesture( DragGesture(minimumDistance: 0) - .onChanged { gestureValue in - configuration.onEditingChanged(true) - - if configuration.dragOffset.wrappedValue == nil { - configuration.dragOffset.wrappedValue = gestureValue.startLocation.x - distanceFrom( - value: configuration.value.wrappedValue, - availableDistance: geometry.size.width, - bounds: configuration.bounds, - leadingOffset: self.thumbSize.width / 2, - trailingOffset: self.thumbSize.width / 2 + .updating(configuration.gestureState) { value, state, transaction in + state = (state ?? { + let x = x(configuration: configuration, geometry: geometry) + return SliderGestureState( + precisionScrubbing: options.contains(.precisionScrubbing), + initialOffset: value.location.x - x ) - } - - let computedValue = valueFrom( - distance: gestureValue.location.x - (configuration.dragOffset.wrappedValue ?? 0), - availableDistance: geometry.size.width, - bounds: configuration.bounds, - step: configuration.step, - leadingOffset: self.thumbSize.width / 2, - trailingOffset: self.thumbSize.width / 2 + }()).updating( + with: value.location.x, + crossAxisOffset: value.translation.height ) - - configuration.value.wrappedValue = computedValue - } - .onEnded { _ in - configuration.dragOffset.wrappedValue = nil - configuration.onEditingChanged(false) } ) } .frame(height: geometry.size.height) + .onChange(of: configuration.gestureState.wrappedValue) { state in + guard let state else { return } + + configuration.value.wrappedValue = valueFrom( + distance: state.offset - (configuration.dragOffset.wrappedValue ?? 0), + availableDistance: geometry.size.width, + bounds: configuration.bounds, + step: configuration.step, + leadingOffset: self.thumbSize.width / 2, + trailingOffset: self.thumbSize.width / 2 + ) + } + .onChange(of: configuration.gestureState.wrappedValue != nil) { editing in + configuration.onEditingChanged(editing) + } } .frame(minHeight: self.thumbInteractiveSize.height) } diff --git a/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift b/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift index 755b78f..2fac45f 100644 --- a/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift +++ b/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift @@ -3,7 +3,8 @@ import SwiftUI public struct ValueSliderOptions: OptionSet { public let rawValue: Int - public static let interactiveTrack = ValueSliderOptions(rawValue: 1 << 0) + public static let precisionScrubbing = ValueSliderOptions(rawValue: 1 << 0) + public static let interactiveTrack = ValueSliderOptions(rawValue: 1 << 1) public static let defaultOptions: ValueSliderOptions = [] public init(rawValue: Int) { diff --git a/Sources/Sliders/ValueSlider/ValueSlider.swift b/Sources/Sliders/ValueSlider/ValueSlider.swift index d7b4a3f..7133638 100644 --- a/Sources/Sliders/ValueSlider/ValueSlider.swift +++ b/Sources/Sliders/ValueSlider/ValueSlider.swift @@ -3,12 +3,16 @@ import SwiftUI public struct ValueSlider: View { @Environment(\.valueSliderStyle) private var style @State private var dragOffset: CGFloat? + @GestureState private var gestureState: SliderGestureState? private var configuration: ValueSliderStyleConfiguration public var body: some View { self.style.makeBody(configuration: - self.configuration.with(dragOffset: self.$dragOffset) + self.configuration.with( + dragOffset: self.$dragOffset, + gestureState: self.$gestureState + ) ) } } @@ -28,7 +32,8 @@ extension ValueSlider { bounds: CGFloat(bounds.lowerBound)...CGFloat(bounds.upperBound), step: CGFloat(step), onEditingChanged: onEditingChanged, - dragOffset: .constant(0) + dragOffset: .constant(0), + gestureState: .init(initialValue: nil) ) ) } @@ -42,7 +47,8 @@ extension ValueSlider { bounds: CGFloat(bounds.lowerBound)...CGFloat(bounds.upperBound), step: CGFloat(step), onEditingChanged: onEditingChanged, - dragOffset: .constant(0) + dragOffset: .constant(0), + gestureState: .init(initialValue: nil) ) ) } @@ -62,7 +68,8 @@ extension ValueSlider { bounds: CGFloat(bounds.lowerBound.value)...CGFloat(bounds.upperBound.value), step: CGFloat(step.value), onEditingChanged: onEditingChanged, - dragOffset: .constant(0) + dragOffset: .constant(0), + gestureState: .init(initialValue: nil) ) ) } From a3b74d98e82634e7301f9797973882f3b35c0e7b Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Mon, 7 Aug 2023 22:50:02 +0100 Subject: [PATCH 05/12] Add callbacks --- Sources/Sliders/Base/SliderGestureState.swift | 14 ++++----- Sources/Sliders/RangeSlider/RangeSlider.swift | 8 +++-- .../Style/RangeSliderStyleConfiguration.swift | 1 + .../HorizontalRangeSliderStyle.swift | 3 ++ .../Style/ValueSliderStyleConfiguration.swift | 12 +++++++- .../HorizontalValueSliderStyle.swift | 3 ++ Sources/Sliders/ValueSlider/ValueSlider.swift | 29 +++++++++++++++---- 7 files changed, 55 insertions(+), 15 deletions(-) diff --git a/Sources/Sliders/Base/SliderGestureState.swift b/Sources/Sliders/Base/SliderGestureState.swift index 100442e..3359f4b 100644 --- a/Sources/Sliders/Base/SliderGestureState.swift +++ b/Sources/Sliders/Base/SliderGestureState.swift @@ -2,11 +2,11 @@ import Foundation import SwiftUI public struct SliderGestureState: Equatable { - enum Speed: CGFloat { + enum Speed: Float { case normal = 1 - case half = 0.5 - case quarter = 0.25 - case eighth = 0.125 + case half = 2 + case quarter = 4 + case eighth = 8 } let precisionScrubbing: Bool @@ -20,7 +20,7 @@ public struct SliderGestureState: Equatable { var offset: CGFloat { accumulations.reduce(0) { accum, tuple in let (speed, value) = tuple - let appliedValue = precisionScrubbing ? speed.rawValue * value : value + let appliedValue = precisionScrubbing ? value / CGFloat(speed.rawValue) : value return accum + appliedValue } } @@ -31,9 +31,9 @@ public struct SliderGestureState: Equatable { } private func speed(crossAxisOffset: CGFloat) -> Speed { - if abs(crossAxisOffset) > 200 { + if abs(crossAxisOffset) > 300 { return .eighth - } else if abs(crossAxisOffset) > 150 { + } else if abs(crossAxisOffset) > 200 { return .quarter } else if abs(crossAxisOffset) > 100 { return .half diff --git a/Sources/Sliders/RangeSlider/RangeSlider.swift b/Sources/Sliders/RangeSlider/RangeSlider.swift index 6f9aded..d2f2269 100644 --- a/Sources/Sliders/RangeSlider/RangeSlider.swift +++ b/Sources/Sliders/RangeSlider/RangeSlider.swift @@ -31,7 +31,8 @@ extension RangeSlider { in bounds: ClosedRange = 0.0...1.0, step: V.Stride = 0.001, distance: ClosedRange = 0.0 ... .infinity, - onEditingChanged: @escaping (Bool) -> Void = { _ in } + onEditingChanged: @escaping (Bool) -> Void = { _ in }, + onPrecisionScrubbingChange: @escaping (Float?) -> Void = { _ in } ) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint { self.init( RangeSliderStyleConfiguration( @@ -43,6 +44,7 @@ extension RangeSlider { step: CGFloat(step), distance: CGFloat(distance.lowerBound) ... CGFloat(distance.upperBound), onEditingChanged: onEditingChanged, + onPrecisionScrubbingChange: onPrecisionScrubbingChange, dragOffset: .constant(0), lowerGestureState: .init(initialValue: nil), upperGestureState: .init(initialValue: nil) @@ -57,7 +59,8 @@ extension RangeSlider { in bounds: ClosedRange = 0...1, step: V.Stride = 1, distance: ClosedRange = 0 ... .max, - onEditingChanged: @escaping (Bool) -> Void = { _ in } + onEditingChanged: @escaping (Bool) -> Void = { _ in }, + onPrecisionScrubbingChange: @escaping (Float?) -> Void = { _ in } ) where V : FixedWidthInteger, V.Stride : FixedWidthInteger { self.init( RangeSliderStyleConfiguration( @@ -69,6 +72,7 @@ extension RangeSlider { step: CGFloat(step), distance: CGFloat(distance.lowerBound) ... CGFloat(distance.upperBound), onEditingChanged: onEditingChanged, + onPrecisionScrubbingChange: onPrecisionScrubbingChange, dragOffset: .constant(0), lowerGestureState: .init(initialValue: nil), upperGestureState: .init(initialValue: nil) diff --git a/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift b/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift index 44e1331..ba327b0 100644 --- a/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift +++ b/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift @@ -6,6 +6,7 @@ public struct RangeSliderStyleConfiguration { public let step: CGFloat public let distance: ClosedRange public let onEditingChanged: (Bool) -> Void + public let onPrecisionScrubbingChange: (Float?) -> Void public var dragOffset: Binding public var lowerGestureState: GestureState public var upperGestureState: GestureState diff --git a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift index 1defd15..8ab03e5 100644 --- a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift +++ b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift @@ -157,6 +157,9 @@ public struct HorizontalRangeSliderStyle public let step: CGFloat public let onEditingChanged: (Bool) -> Void + public let onPrecisionScrubbingChange: (Float?) -> Void public var dragOffset: Binding public var gestureState: GestureState - public init(value: Binding, bounds: ClosedRange, step: CGFloat, onEditingChanged: @escaping (Bool) -> Void, dragOffset: Binding, gestureState: GestureState) { + public init( + value: Binding, + bounds: ClosedRange, + step: CGFloat, + onEditingChanged: @escaping (Bool) -> Void, + onPrecisionScrubbingChange: @escaping (Float?) -> Void, + dragOffset: Binding, + gestureState: GestureState + ) { self.value = value self.bounds = bounds self.step = step self.onEditingChanged = onEditingChanged + self.onPrecisionScrubbingChange = onPrecisionScrubbingChange self.dragOffset = dragOffset self.gestureState = gestureState } diff --git a/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift b/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift index 46155e4..8b5c273 100644 --- a/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift +++ b/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift @@ -89,6 +89,9 @@ public struct HorizontalValueSliderStyle: ValueSliderS .onChange(of: configuration.gestureState.wrappedValue != nil) { editing in configuration.onEditingChanged(editing) } + .onChange(of: configuration.gestureState.wrappedValue?.speed) { speed in + configuration.onPrecisionScrubbingChange(speed?.rawValue) + } } .frame(minHeight: self.thumbInteractiveSize.height) } diff --git a/Sources/Sliders/ValueSlider/ValueSlider.swift b/Sources/Sliders/ValueSlider/ValueSlider.swift index 7133638..5591f04 100644 --- a/Sources/Sliders/ValueSlider/ValueSlider.swift +++ b/Sources/Sliders/ValueSlider/ValueSlider.swift @@ -24,14 +24,20 @@ extension ValueSlider { } extension ValueSlider { - public init(value: Binding, in bounds: ClosedRange = 0.0...1.0, step: V.Stride = 0.001, onEditingChanged: @escaping (Bool) -> Void = { _ in }) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint { - + public init( + value: Binding, + in bounds: ClosedRange = 0.0...1.0, + step: V.Stride = 0.001, + onEditingChanged: @escaping (Bool) -> Void = { _ in }, + onPrecisionScrubbingChange: @escaping (Float?) -> Void = { _ in } + ) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint { self.init( ValueSliderStyleConfiguration( value: Binding(get: { CGFloat(value.wrappedValue.clamped(to: bounds)) }, set: { value.wrappedValue = V($0) }), bounds: CGFloat(bounds.lowerBound)...CGFloat(bounds.upperBound), step: CGFloat(step), onEditingChanged: onEditingChanged, + onPrecisionScrubbingChange: onPrecisionScrubbingChange, dragOffset: .constant(0), gestureState: .init(initialValue: nil) ) @@ -40,13 +46,20 @@ extension ValueSlider { } extension ValueSlider { - public init(value: Binding, in bounds: ClosedRange = 0...1, step: V.Stride = 1, onEditingChanged: @escaping (Bool) -> Void = { _ in }) where V : FixedWidthInteger, V.Stride : FixedWidthInteger { + public init( + value: Binding, + in bounds: ClosedRange = 0...1, + step: V.Stride = 1, + onEditingChanged: @escaping (Bool) -> Void = { _ in }, + onPrecisionScrubbingChange: @escaping (Float?) -> Void = { _ in } + ) where V : FixedWidthInteger, V.Stride : FixedWidthInteger { self.init( ValueSliderStyleConfiguration( value: Binding(get: { CGFloat(value.wrappedValue.clamped(to: bounds)) }, set: { value.wrappedValue = V($0) }), bounds: CGFloat(bounds.lowerBound)...CGFloat(bounds.upperBound), step: CGFloat(step), onEditingChanged: onEditingChanged, + onPrecisionScrubbingChange: onPrecisionScrubbingChange, dragOffset: .constant(0), gestureState: .init(initialValue: nil) ) @@ -55,8 +68,13 @@ extension ValueSlider { } extension ValueSlider { - public init(value: Binding>, in bounds: ClosedRange>, step: Measurement, onEditingChanged: @escaping (Bool) -> Void = { _ in }) { - + public init( + value: Binding>, + in bounds: ClosedRange>, + step: Measurement, + onEditingChanged: @escaping (Bool) -> Void = { _ in }, + onPrecisionScrubbingChange: @escaping (Float?) -> Void = { _ in } + ) { self.init( ValueSliderStyleConfiguration( value: Binding(get: { @@ -68,6 +86,7 @@ extension ValueSlider { bounds: CGFloat(bounds.lowerBound.value)...CGFloat(bounds.upperBound.value), step: CGFloat(step.value), onEditingChanged: onEditingChanged, + onPrecisionScrubbingChange: onPrecisionScrubbingChange, dragOffset: .constant(0), gestureState: .init(initialValue: nil) ) From e6d19d3c14c01ce8b2a19634b2d80b248d29038a Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Tue, 8 Aug 2023 12:57:58 +0100 Subject: [PATCH 06/12] Reset accumulations when reducing speed --- Sources/Sliders/Base/SliderGestureState.swift | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Sources/Sliders/Base/SliderGestureState.swift b/Sources/Sliders/Base/SliderGestureState.swift index 3359f4b..c2e9a60 100644 --- a/Sources/Sliders/Base/SliderGestureState.swift +++ b/Sources/Sliders/Base/SliderGestureState.swift @@ -2,7 +2,11 @@ import Foundation import SwiftUI public struct SliderGestureState: Equatable { - enum Speed: Float { + enum Speed: Float, Comparable, CaseIterable { + static func < (lhs: SliderGestureState.Speed, rhs: SliderGestureState.Speed) -> Bool { + lhs.rawValue < rhs.rawValue + } + case normal = 1 case half = 2 case quarter = 4 @@ -44,10 +48,23 @@ public struct SliderGestureState: Equatable { func updating(with offset: CGFloat, crossAxisOffset: CGFloat) -> Self { var mutSelf = self + let speed = speed(crossAxisOffset: crossAxisOffset) mutSelf.speed = speed - mutSelf.accumulations[speed] = (accumulations[speed] ?? 0) + offset - lastOffset + + var accumulations = Speed.allCases.reduce([:]) { (accum: [Speed:CGFloat], accumSpeed: Speed) in + var out = accum + + let appliedSpeed = min(accumSpeed, speed) + out[appliedSpeed] = (out[appliedSpeed] ?? 0) + (self.accumulations[accumSpeed] ?? 0) + + return out + } + accumulations[speed] = (accumulations[speed] ?? 0) + offset - lastOffset + mutSelf.accumulations = accumulations + mutSelf.lastOffset = offset + return mutSelf } } From 796842e16d9ddba7690a8f398f4f7b56a378fc23 Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Tue, 8 Aug 2023 17:23:06 +0100 Subject: [PATCH 07/12] Fix positions --- .../Styles/Horizontal/HorizontalRangeSliderStyle.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift index 8ab03e5..72a84c1 100644 --- a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift +++ b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift @@ -19,7 +19,7 @@ public struct HorizontalRangeSliderStyle CGFloat { distanceFrom( value: configuration.range.wrappedValue.lowerBound, - availableDistance: geometry.size.width, + availableDistance: geometry.size.width - upperThumbSize.width, bounds: configuration.bounds, leadingOffset: lowerThumbSize.width / 2, trailingOffset: lowerThumbSize.width / 2 @@ -31,7 +31,7 @@ public struct HorizontalRangeSliderStyle Date: Wed, 9 Aug 2023 19:17:50 +0100 Subject: [PATCH 08/12] Update precision scrubbing --- Sources/Sliders/Base/PrecisionScrubbing.swift | 23 +++++++++ Sources/Sliders/Base/SliderGestureState.swift | 47 ++++--------------- Sources/Sliders/RangeSlider/RangeSlider.swift | 12 ++--- .../Style/RangeSliderStyleConfiguration.swift | 5 +- .../HorizontalRangeSliderStyle.swift | 17 ++----- .../Styles/RangeSliderOptions.swift | 3 +- .../Style/ValueSliderStyleConfiguration.swift | 9 ++-- .../HorizontalValueSliderStyle.swift | 19 ++------ .../Styles/ValueSliderOptions.swift | 3 +- Sources/Sliders/ValueSlider/ValueSlider.swift | 17 ++++--- 10 files changed, 63 insertions(+), 92 deletions(-) create mode 100644 Sources/Sliders/Base/PrecisionScrubbing.swift diff --git a/Sources/Sliders/Base/PrecisionScrubbing.swift b/Sources/Sliders/Base/PrecisionScrubbing.swift new file mode 100644 index 0000000..21a38fb --- /dev/null +++ b/Sources/Sliders/Base/PrecisionScrubbing.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct PrecisionScrubbingKey: EnvironmentKey { + static let defaultValue: (Float) -> Float = { _ in 1 } +} + +extension EnvironmentValues { + var precisionScrubbing: (Float) -> Float { + get { self[PrecisionScrubbingKey.self] } + set { self[PrecisionScrubbingKey.self] = newValue } + } +} + +public extension View { + func precisionScrubbing( + _ getScrubValue: @escaping (Float) -> ScrubValue + ) -> some View + where ScrubValue.RawValue == Float { + self.environment(\.precisionScrubbing, { offset in + getScrubValue(offset).rawValue + }) + } +} diff --git a/Sources/Sliders/Base/SliderGestureState.swift b/Sources/Sliders/Base/SliderGestureState.swift index c2e9a60..906570f 100644 --- a/Sources/Sliders/Base/SliderGestureState.swift +++ b/Sources/Sliders/Base/SliderGestureState.swift @@ -2,61 +2,30 @@ import Foundation import SwiftUI public struct SliderGestureState: Equatable { - enum Speed: Float, Comparable, CaseIterable { - static func < (lhs: SliderGestureState.Speed, rhs: SliderGestureState.Speed) -> Bool { - lhs.rawValue < rhs.rawValue - } - - case normal = 1 - case half = 2 - case quarter = 4 - case eighth = 8 - } - - let precisionScrubbing: Bool - var speed = Speed.normal - private var lastOffset: CGFloat - private var accumulations: [Speed:CGFloat] = [ - .normal: 0 - ] + private var accumulations: [Float:CGFloat] = [1: 0] var offset: CGFloat { accumulations.reduce(0) { accum, tuple in let (speed, value) = tuple - let appliedValue = precisionScrubbing ? value / CGFloat(speed.rawValue) : value - return accum + appliedValue + return accum + value / CGFloat(speed) } } - init(precisionScrubbing: Bool, initialOffset: CGFloat) { - self.precisionScrubbing = precisionScrubbing + init(initialOffset: CGFloat) { self.lastOffset = initialOffset } - private func speed(crossAxisOffset: CGFloat) -> Speed { - if abs(crossAxisOffset) > 300 { - return .eighth - } else if abs(crossAxisOffset) > 200 { - return .quarter - } else if abs(crossAxisOffset) > 100 { - return .half - } else { - return .normal - } - } - - func updating(with offset: CGFloat, crossAxisOffset: CGFloat) -> Self { + func updating(with offset: CGFloat, speed: Float) -> Self { var mutSelf = self - let speed = speed(crossAxisOffset: crossAxisOffset) - mutSelf.speed = speed + var accumulations = self.accumulations.reduce([:]) { (accum: [Float:CGFloat], element) in + let (elementSpeed, elementValue) = element - var accumulations = Speed.allCases.reduce([:]) { (accum: [Speed:CGFloat], accumSpeed: Speed) in var out = accum - let appliedSpeed = min(accumSpeed, speed) - out[appliedSpeed] = (out[appliedSpeed] ?? 0) + (self.accumulations[accumSpeed] ?? 0) + let appliedSpeed = min(elementSpeed, speed) + out[appliedSpeed] = (out[appliedSpeed] ?? 0) + elementValue return out } diff --git a/Sources/Sliders/RangeSlider/RangeSlider.swift b/Sources/Sliders/RangeSlider/RangeSlider.swift index d2f2269..48efc1b 100644 --- a/Sources/Sliders/RangeSlider/RangeSlider.swift +++ b/Sources/Sliders/RangeSlider/RangeSlider.swift @@ -3,6 +3,7 @@ import SwiftUI public struct RangeSlider: View { @Environment(\.rangeSliderStyle) private var style @State private var dragOffset: CGFloat? + @Environment(\.precisionScrubbing) private var precisionScrubbing @GestureState private var lowerGestureState: SliderGestureState? @GestureState private var upperGestureState: SliderGestureState? @@ -11,6 +12,7 @@ public struct RangeSlider: View { public var body: some View { self.style.makeBody(configuration: self.configuration.with( + precisionScrubbing: self.precisionScrubbing, dragOffset: self.$dragOffset, lowerGestureState: self.$lowerGestureState, upperGestureState: self.$upperGestureState @@ -31,8 +33,7 @@ extension RangeSlider { in bounds: ClosedRange = 0.0...1.0, step: V.Stride = 0.001, distance: ClosedRange = 0.0 ... .infinity, - onEditingChanged: @escaping (Bool) -> Void = { _ in }, - onPrecisionScrubbingChange: @escaping (Float?) -> Void = { _ in } + onEditingChanged: @escaping (Bool) -> Void = { _ in } ) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint { self.init( RangeSliderStyleConfiguration( @@ -44,7 +45,7 @@ extension RangeSlider { step: CGFloat(step), distance: CGFloat(distance.lowerBound) ... CGFloat(distance.upperBound), onEditingChanged: onEditingChanged, - onPrecisionScrubbingChange: onPrecisionScrubbingChange, + precisionScrubbing: { _ in 1 }, dragOffset: .constant(0), lowerGestureState: .init(initialValue: nil), upperGestureState: .init(initialValue: nil) @@ -59,8 +60,7 @@ extension RangeSlider { in bounds: ClosedRange = 0...1, step: V.Stride = 1, distance: ClosedRange = 0 ... .max, - onEditingChanged: @escaping (Bool) -> Void = { _ in }, - onPrecisionScrubbingChange: @escaping (Float?) -> Void = { _ in } + onEditingChanged: @escaping (Bool) -> Void = { _ in } ) where V : FixedWidthInteger, V.Stride : FixedWidthInteger { self.init( RangeSliderStyleConfiguration( @@ -72,7 +72,7 @@ extension RangeSlider { step: CGFloat(step), distance: CGFloat(distance.lowerBound) ... CGFloat(distance.upperBound), onEditingChanged: onEditingChanged, - onPrecisionScrubbingChange: onPrecisionScrubbingChange, + precisionScrubbing: { _ in 1 }, dragOffset: .constant(0), lowerGestureState: .init(initialValue: nil), upperGestureState: .init(initialValue: nil) diff --git a/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift b/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift index ba327b0..408caeb 100644 --- a/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift +++ b/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift @@ -6,13 +6,14 @@ public struct RangeSliderStyleConfiguration { public let step: CGFloat public let distance: ClosedRange public let onEditingChanged: (Bool) -> Void - public let onPrecisionScrubbingChange: (Float?) -> Void + public var precisionScrubbing: (Float) -> Float public var dragOffset: Binding public var lowerGestureState: GestureState public var upperGestureState: GestureState - func with(dragOffset: Binding, lowerGestureState: GestureState, upperGestureState: GestureState) -> Self { + func with(precisionScrubbing: @escaping (Float) -> Float, dragOffset: Binding, lowerGestureState: GestureState, upperGestureState: GestureState) -> Self { var mutSelf = self + mutSelf.precisionScrubbing = precisionScrubbing mutSelf.dragOffset = dragOffset mutSelf.lowerGestureState = lowerGestureState mutSelf.upperGestureState = upperGestureState diff --git a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift index 72a84c1..5474003 100644 --- a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift +++ b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift @@ -65,13 +65,10 @@ public struct HorizontalRangeSliderStyle SliderGestureState in let x = upperX(configuration: configuration, geometry: geometry) - return SliderGestureState( - precisionScrubbing: options.contains(.precisionScrubbing), - initialOffset: value.location.x - x - ) + return SliderGestureState(initialOffset: value.location.x - x) }()).updating( with: value.location.x, - crossAxisOffset: value.translation.height + speed: configuration.precisionScrubbing(Float(value.translation.height)) ) }, TapGesture() @@ -157,9 +151,6 @@ public struct HorizontalRangeSliderStyle public let step: CGFloat public let onEditingChanged: (Bool) -> Void - public let onPrecisionScrubbingChange: (Float?) -> Void + public var precisionScrubbing: (Float) -> Float public var dragOffset: Binding public var gestureState: GestureState @@ -14,7 +14,7 @@ public struct ValueSliderStyleConfiguration { bounds: ClosedRange, step: CGFloat, onEditingChanged: @escaping (Bool) -> Void, - onPrecisionScrubbingChange: @escaping (Float?) -> Void, + precisionScrubbing: @escaping (Float) -> Float, dragOffset: Binding, gestureState: GestureState ) { @@ -22,13 +22,14 @@ public struct ValueSliderStyleConfiguration { self.bounds = bounds self.step = step self.onEditingChanged = onEditingChanged - self.onPrecisionScrubbingChange = onPrecisionScrubbingChange + self.precisionScrubbing = precisionScrubbing self.dragOffset = dragOffset self.gestureState = gestureState } - func with(dragOffset: Binding, gestureState: GestureState) -> Self { + func with(precisionScrubbing: @escaping (Float) -> Float, dragOffset: Binding, gestureState: GestureState) -> Self { var mutSelf = self + mutSelf.precisionScrubbing = precisionScrubbing mutSelf.dragOffset = dragOffset mutSelf.gestureState = gestureState return mutSelf diff --git a/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift b/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift index 8b5c273..fb81f8b 100644 --- a/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift +++ b/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift @@ -33,14 +33,9 @@ public struct HorizontalValueSliderStyle: ValueSliderS track.gesture( DragGesture(minimumDistance: 0) .updating(configuration.gestureState) { value, state, transaction in - state = (state ?? { - SliderGestureState( - precisionScrubbing: options.contains(.precisionScrubbing), - initialOffset: 0 - ) - }()).updating( + state = (state ?? SliderGestureState(initialOffset: 0)).updating( with: value.location.x, - crossAxisOffset: value.translation.height + speed: configuration.precisionScrubbing(Float(value.translation.height)) ) } ) @@ -62,13 +57,10 @@ public struct HorizontalValueSliderStyle: ValueSliderS .updating(configuration.gestureState) { value, state, transaction in state = (state ?? { let x = x(configuration: configuration, geometry: geometry) - return SliderGestureState( - precisionScrubbing: options.contains(.precisionScrubbing), - initialOffset: value.location.x - x - ) + return SliderGestureState(initialOffset: value.location.x - x) }()).updating( with: value.location.x, - crossAxisOffset: value.translation.height + speed: configuration.precisionScrubbing(Float(value.translation.height)) ) } ) @@ -89,9 +81,6 @@ public struct HorizontalValueSliderStyle: ValueSliderS .onChange(of: configuration.gestureState.wrappedValue != nil) { editing in configuration.onEditingChanged(editing) } - .onChange(of: configuration.gestureState.wrappedValue?.speed) { speed in - configuration.onPrecisionScrubbingChange(speed?.rawValue) - } } .frame(minHeight: self.thumbInteractiveSize.height) } diff --git a/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift b/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift index 2fac45f..755b78f 100644 --- a/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift +++ b/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift @@ -3,8 +3,7 @@ import SwiftUI public struct ValueSliderOptions: OptionSet { public let rawValue: Int - public static let precisionScrubbing = ValueSliderOptions(rawValue: 1 << 0) - public static let interactiveTrack = ValueSliderOptions(rawValue: 1 << 1) + public static let interactiveTrack = ValueSliderOptions(rawValue: 1 << 0) public static let defaultOptions: ValueSliderOptions = [] public init(rawValue: Int) { diff --git a/Sources/Sliders/ValueSlider/ValueSlider.swift b/Sources/Sliders/ValueSlider/ValueSlider.swift index 5591f04..01691cd 100644 --- a/Sources/Sliders/ValueSlider/ValueSlider.swift +++ b/Sources/Sliders/ValueSlider/ValueSlider.swift @@ -2,6 +2,7 @@ import SwiftUI public struct ValueSlider: View { @Environment(\.valueSliderStyle) private var style + @Environment(\.precisionScrubbing) private var precisionScrubbing @State private var dragOffset: CGFloat? @GestureState private var gestureState: SliderGestureState? @@ -10,6 +11,7 @@ public struct ValueSlider: View { public var body: some View { self.style.makeBody(configuration: self.configuration.with( + precisionScrubbing: self.precisionScrubbing, dragOffset: self.$dragOffset, gestureState: self.$gestureState ) @@ -28,8 +30,7 @@ extension ValueSlider { value: Binding, in bounds: ClosedRange = 0.0...1.0, step: V.Stride = 0.001, - onEditingChanged: @escaping (Bool) -> Void = { _ in }, - onPrecisionScrubbingChange: @escaping (Float?) -> Void = { _ in } + onEditingChanged: @escaping (Bool) -> Void = { _ in } ) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint { self.init( ValueSliderStyleConfiguration( @@ -37,7 +38,7 @@ extension ValueSlider { bounds: CGFloat(bounds.lowerBound)...CGFloat(bounds.upperBound), step: CGFloat(step), onEditingChanged: onEditingChanged, - onPrecisionScrubbingChange: onPrecisionScrubbingChange, + precisionScrubbing: { _ in 1 }, dragOffset: .constant(0), gestureState: .init(initialValue: nil) ) @@ -50,8 +51,7 @@ extension ValueSlider { value: Binding, in bounds: ClosedRange = 0...1, step: V.Stride = 1, - onEditingChanged: @escaping (Bool) -> Void = { _ in }, - onPrecisionScrubbingChange: @escaping (Float?) -> Void = { _ in } + onEditingChanged: @escaping (Bool) -> Void = { _ in } ) where V : FixedWidthInteger, V.Stride : FixedWidthInteger { self.init( ValueSliderStyleConfiguration( @@ -59,7 +59,7 @@ extension ValueSlider { bounds: CGFloat(bounds.lowerBound)...CGFloat(bounds.upperBound), step: CGFloat(step), onEditingChanged: onEditingChanged, - onPrecisionScrubbingChange: onPrecisionScrubbingChange, + precisionScrubbing: { _ in 1 }, dragOffset: .constant(0), gestureState: .init(initialValue: nil) ) @@ -72,8 +72,7 @@ extension ValueSlider { value: Binding>, in bounds: ClosedRange>, step: Measurement, - onEditingChanged: @escaping (Bool) -> Void = { _ in }, - onPrecisionScrubbingChange: @escaping (Float?) -> Void = { _ in } + onEditingChanged: @escaping (Bool) -> Void = { _ in } ) { self.init( ValueSliderStyleConfiguration( @@ -86,7 +85,7 @@ extension ValueSlider { bounds: CGFloat(bounds.lowerBound.value)...CGFloat(bounds.upperBound.value), step: CGFloat(step.value), onEditingChanged: onEditingChanged, - onPrecisionScrubbingChange: onPrecisionScrubbingChange, + precisionScrubbing: { _ in 1 }, dragOffset: .constant(0), gestureState: .init(initialValue: nil) ) From 7bcd07d1582f388100cddd2374e7a43cd589681e Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Wed, 9 Aug 2023 21:27:32 +0100 Subject: [PATCH 09/12] OnChange for precision scrubbing --- Sources/Sliders/Base/PrecisionScrubbing.swift | 42 +++++++++++++++---- Sources/Sliders/Base/SliderGestureState.swift | 3 ++ Sources/Sliders/RangeSlider/RangeSlider.swift | 4 +- .../Style/RangeSliderStyleConfiguration.swift | 4 +- .../HorizontalRangeSliderStyle.swift | 21 ++++++++-- .../Style/ValueSliderStyleConfiguration.swift | 6 +-- .../HorizontalValueSliderStyle.swift | 7 +++- Sources/Sliders/ValueSlider/ValueSlider.swift | 6 +-- 8 files changed, 69 insertions(+), 24 deletions(-) diff --git a/Sources/Sliders/Base/PrecisionScrubbing.swift b/Sources/Sliders/Base/PrecisionScrubbing.swift index 21a38fb..1a467b7 100644 --- a/Sources/Sliders/Base/PrecisionScrubbing.swift +++ b/Sources/Sliders/Base/PrecisionScrubbing.swift @@ -1,23 +1,47 @@ import SwiftUI +public struct PrecisionScrubbingConfig { + let scrubValue: (Float) -> Float + let onChange: ((Float?) -> Void)? +} + struct PrecisionScrubbingKey: EnvironmentKey { - static let defaultValue: (Float) -> Float = { _ in 1 } + typealias Value = PrecisionScrubbingConfig + + static let defaultValue: PrecisionScrubbingConfig = PrecisionScrubbingConfig( + scrubValue: { _ in 1 }, + onChange: nil + ) } extension EnvironmentValues { - var precisionScrubbing: (Float) -> Float { + var precisionScrubbing: PrecisionScrubbingConfig { get { self[PrecisionScrubbingKey.self] } set { self[PrecisionScrubbingKey.self] = newValue } } } public extension View { - func precisionScrubbing( - _ getScrubValue: @escaping (Float) -> ScrubValue - ) -> some View - where ScrubValue.RawValue == Float { - self.environment(\.precisionScrubbing, { offset in - getScrubValue(offset).rawValue - }) + func precisionScrubbing>( + _ scrubValue: @escaping (Float) -> ScrubValue, + onChange: ((ScrubValue?) -> Void)? = nil + ) -> some View { + self.environment( + \.precisionScrubbing, + PrecisionScrubbingConfig { offset in + scrubValue(offset).rawValue + } onChange: { value in + guard let value else { + onChange?(nil) + return + } + + if let value = ScrubValue(rawValue: value) { + onChange?(value) + } else { + print("Invalid conversion") + } + } + ) } } diff --git a/Sources/Sliders/Base/SliderGestureState.swift b/Sources/Sliders/Base/SliderGestureState.swift index 906570f..56dadd6 100644 --- a/Sources/Sliders/Base/SliderGestureState.swift +++ b/Sources/Sliders/Base/SliderGestureState.swift @@ -2,6 +2,7 @@ import Foundation import SwiftUI public struct SliderGestureState: Equatable { + var speed: Float? private var lastOffset: CGFloat private var accumulations: [Float:CGFloat] = [1: 0] @@ -19,6 +20,8 @@ public struct SliderGestureState: Equatable { func updating(with offset: CGFloat, speed: Float) -> Self { var mutSelf = self + mutSelf.speed = speed + var accumulations = self.accumulations.reduce([:]) { (accum: [Float:CGFloat], element) in let (elementSpeed, elementValue) = element diff --git a/Sources/Sliders/RangeSlider/RangeSlider.swift b/Sources/Sliders/RangeSlider/RangeSlider.swift index 48efc1b..0403464 100644 --- a/Sources/Sliders/RangeSlider/RangeSlider.swift +++ b/Sources/Sliders/RangeSlider/RangeSlider.swift @@ -45,7 +45,7 @@ extension RangeSlider { step: CGFloat(step), distance: CGFloat(distance.lowerBound) ... CGFloat(distance.upperBound), onEditingChanged: onEditingChanged, - precisionScrubbing: { _ in 1 }, + precisionScrubbing: PrecisionScrubbingKey.defaultValue, dragOffset: .constant(0), lowerGestureState: .init(initialValue: nil), upperGestureState: .init(initialValue: nil) @@ -72,7 +72,7 @@ extension RangeSlider { step: CGFloat(step), distance: CGFloat(distance.lowerBound) ... CGFloat(distance.upperBound), onEditingChanged: onEditingChanged, - precisionScrubbing: { _ in 1 }, + precisionScrubbing: PrecisionScrubbingKey.defaultValue, dragOffset: .constant(0), lowerGestureState: .init(initialValue: nil), upperGestureState: .init(initialValue: nil) diff --git a/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift b/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift index 408caeb..0e10dde 100644 --- a/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift +++ b/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift @@ -6,12 +6,12 @@ public struct RangeSliderStyleConfiguration { public let step: CGFloat public let distance: ClosedRange public let onEditingChanged: (Bool) -> Void - public var precisionScrubbing: (Float) -> Float + public var precisionScrubbing: PrecisionScrubbingConfig public var dragOffset: Binding public var lowerGestureState: GestureState public var upperGestureState: GestureState - func with(precisionScrubbing: @escaping (Float) -> Float, dragOffset: Binding, lowerGestureState: GestureState, upperGestureState: GestureState) -> Self { + func with(precisionScrubbing: PrecisionScrubbingConfig, dragOffset: Binding, lowerGestureState: GestureState, upperGestureState: GestureState) -> Self { var mutSelf = self mutSelf.precisionScrubbing = precisionScrubbing mutSelf.dragOffset = dragOffset diff --git a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift index 5474003..f4b666c 100644 --- a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift +++ b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift @@ -37,7 +37,19 @@ public struct HorizontalRangeSliderStyle some View { - GeometryReader { geometry in + let editing = configuration.lowerGestureState.wrappedValue != nil || configuration.upperGestureState.wrappedValue != nil + let precisionScrubbingSpeed = { () -> Float? in + switch ( + configuration.lowerGestureState.wrappedValue?.speed, + configuration.upperGestureState.wrappedValue?.speed + ) { + case (let lower?, let upper?): return min(lower, upper) + case (let only?, nil), (nil, let only?): return only + case (nil, nil): return nil + } + }() + + return GeometryReader { geometry in ZStack { self.track .environment(\.trackRange, configuration.range.wrappedValue) @@ -68,7 +80,7 @@ public struct HorizontalRangeSliderStyle public let step: CGFloat public let onEditingChanged: (Bool) -> Void - public var precisionScrubbing: (Float) -> Float + public var precisionScrubbing: PrecisionScrubbingConfig public var dragOffset: Binding public var gestureState: GestureState @@ -14,7 +14,7 @@ public struct ValueSliderStyleConfiguration { bounds: ClosedRange, step: CGFloat, onEditingChanged: @escaping (Bool) -> Void, - precisionScrubbing: @escaping (Float) -> Float, + precisionScrubbing: PrecisionScrubbingConfig, dragOffset: Binding, gestureState: GestureState ) { @@ -27,7 +27,7 @@ public struct ValueSliderStyleConfiguration { self.gestureState = gestureState } - func with(precisionScrubbing: @escaping (Float) -> Float, dragOffset: Binding, gestureState: GestureState) -> Self { + func with(precisionScrubbing: PrecisionScrubbingConfig, dragOffset: Binding, gestureState: GestureState) -> Self { var mutSelf = self mutSelf.precisionScrubbing = precisionScrubbing mutSelf.dragOffset = dragOffset diff --git a/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift b/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift index fb81f8b..308756c 100644 --- a/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift +++ b/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift @@ -35,7 +35,7 @@ public struct HorizontalValueSliderStyle: ValueSliderS .updating(configuration.gestureState) { value, state, transaction in state = (state ?? SliderGestureState(initialOffset: 0)).updating( with: value.location.x, - speed: configuration.precisionScrubbing(Float(value.translation.height)) + speed: configuration.precisionScrubbing.scrubValue(Float(value.translation.height)) ) } ) @@ -60,7 +60,7 @@ public struct HorizontalValueSliderStyle: ValueSliderS return SliderGestureState(initialOffset: value.location.x - x) }()).updating( with: value.location.x, - speed: configuration.precisionScrubbing(Float(value.translation.height)) + speed: configuration.precisionScrubbing.scrubValue(Float(value.translation.height)) ) } ) @@ -81,6 +81,9 @@ public struct HorizontalValueSliderStyle: ValueSliderS .onChange(of: configuration.gestureState.wrappedValue != nil) { editing in configuration.onEditingChanged(editing) } + .onChange(of: configuration.gestureState.wrappedValue?.speed) { speed in + configuration.precisionScrubbing.onChange?(speed) + } } .frame(minHeight: self.thumbInteractiveSize.height) } diff --git a/Sources/Sliders/ValueSlider/ValueSlider.swift b/Sources/Sliders/ValueSlider/ValueSlider.swift index 01691cd..1993acb 100644 --- a/Sources/Sliders/ValueSlider/ValueSlider.swift +++ b/Sources/Sliders/ValueSlider/ValueSlider.swift @@ -38,7 +38,7 @@ extension ValueSlider { bounds: CGFloat(bounds.lowerBound)...CGFloat(bounds.upperBound), step: CGFloat(step), onEditingChanged: onEditingChanged, - precisionScrubbing: { _ in 1 }, + precisionScrubbing: PrecisionScrubbingKey.defaultValue, dragOffset: .constant(0), gestureState: .init(initialValue: nil) ) @@ -59,7 +59,7 @@ extension ValueSlider { bounds: CGFloat(bounds.lowerBound)...CGFloat(bounds.upperBound), step: CGFloat(step), onEditingChanged: onEditingChanged, - precisionScrubbing: { _ in 1 }, + precisionScrubbing: PrecisionScrubbingKey.defaultValue, dragOffset: .constant(0), gestureState: .init(initialValue: nil) ) @@ -85,7 +85,7 @@ extension ValueSlider { bounds: CGFloat(bounds.lowerBound.value)...CGFloat(bounds.upperBound.value), step: CGFloat(step.value), onEditingChanged: onEditingChanged, - precisionScrubbing: { _ in 1 }, + precisionScrubbing: PrecisionScrubbingKey.defaultValue, dragOffset: .constant(0), gestureState: .init(initialValue: nil) ) From 2446408b37794513651f7451e1ef36f968de8338 Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Wed, 9 Aug 2023 21:51:26 +0100 Subject: [PATCH 10/12] Fix simultaneous gestures --- .../Style/ValueSliderStyleConfiguration.swift | 19 ++++++++++++++----- .../HorizontalValueSliderStyle.swift | 11 ++++++----- Sources/Sliders/ValueSlider/ValueSlider.swift | 15 ++++++++++----- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/Sources/Sliders/ValueSlider/Style/ValueSliderStyleConfiguration.swift b/Sources/Sliders/ValueSlider/Style/ValueSliderStyleConfiguration.swift index 9fc0849..5120ed2 100644 --- a/Sources/Sliders/ValueSlider/Style/ValueSliderStyleConfiguration.swift +++ b/Sources/Sliders/ValueSlider/Style/ValueSliderStyleConfiguration.swift @@ -7,7 +7,8 @@ public struct ValueSliderStyleConfiguration { public let onEditingChanged: (Bool) -> Void public var precisionScrubbing: PrecisionScrubbingConfig public var dragOffset: Binding - public var gestureState: GestureState + public var thumbGestureState: GestureState + public var trackGestureState: GestureState public init( value: Binding, @@ -16,7 +17,8 @@ public struct ValueSliderStyleConfiguration { onEditingChanged: @escaping (Bool) -> Void, precisionScrubbing: PrecisionScrubbingConfig, dragOffset: Binding, - gestureState: GestureState + thumbGestureState: GestureState, + trackGestureState: GestureState ) { self.value = value self.bounds = bounds @@ -24,14 +26,21 @@ public struct ValueSliderStyleConfiguration { self.onEditingChanged = onEditingChanged self.precisionScrubbing = precisionScrubbing self.dragOffset = dragOffset - self.gestureState = gestureState + self.thumbGestureState = thumbGestureState + self.trackGestureState = trackGestureState } - func with(precisionScrubbing: PrecisionScrubbingConfig, dragOffset: Binding, gestureState: GestureState) -> Self { + func with( + precisionScrubbing: PrecisionScrubbingConfig, + dragOffset: Binding, + thumbGestureState: GestureState, + trackGestureState: GestureState + ) -> Self { var mutSelf = self mutSelf.precisionScrubbing = precisionScrubbing mutSelf.dragOffset = dragOffset - mutSelf.gestureState = gestureState + mutSelf.thumbGestureState = thumbGestureState + mutSelf.trackGestureState = trackGestureState return mutSelf } } diff --git a/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift b/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift index 308756c..ad82959 100644 --- a/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift +++ b/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift @@ -18,6 +18,7 @@ public struct HorizontalValueSliderStyle: ValueSliderS } public func makeBody(configuration: Self.Configuration) -> some View { + let prominentGesture = configuration.thumbGestureState.wrappedValue ?? configuration.trackGestureState.wrappedValue let track = self.track .environment(\.trackValue, configuration.value.wrappedValue) .environment(\.valueTrackConfiguration, ValueTrackConfiguration( @@ -32,7 +33,7 @@ public struct HorizontalValueSliderStyle: ValueSliderS if self.options.contains(.interactiveTrack) { track.gesture( DragGesture(minimumDistance: 0) - .updating(configuration.gestureState) { value, state, transaction in + .updating(configuration.trackGestureState) { value, state, transaction in state = (state ?? SliderGestureState(initialOffset: 0)).updating( with: value.location.x, speed: configuration.precisionScrubbing.scrubValue(Float(value.translation.height)) @@ -54,7 +55,7 @@ public struct HorizontalValueSliderStyle: ValueSliderS ) .gesture( DragGesture(minimumDistance: 0) - .updating(configuration.gestureState) { value, state, transaction in + .updating(configuration.thumbGestureState) { value, state, transaction in state = (state ?? { let x = x(configuration: configuration, geometry: geometry) return SliderGestureState(initialOffset: value.location.x - x) @@ -66,7 +67,7 @@ public struct HorizontalValueSliderStyle: ValueSliderS ) } .frame(height: geometry.size.height) - .onChange(of: configuration.gestureState.wrappedValue) { state in + .onChange(of: prominentGesture) { state in guard let state else { return } configuration.value.wrappedValue = valueFrom( @@ -78,10 +79,10 @@ public struct HorizontalValueSliderStyle: ValueSliderS trailingOffset: self.thumbSize.width / 2 ) } - .onChange(of: configuration.gestureState.wrappedValue != nil) { editing in + .onChange(of: prominentGesture != nil) { editing in configuration.onEditingChanged(editing) } - .onChange(of: configuration.gestureState.wrappedValue?.speed) { speed in + .onChange(of: prominentGesture?.speed) { speed in configuration.precisionScrubbing.onChange?(speed) } } diff --git a/Sources/Sliders/ValueSlider/ValueSlider.swift b/Sources/Sliders/ValueSlider/ValueSlider.swift index 1993acb..fda558e 100644 --- a/Sources/Sliders/ValueSlider/ValueSlider.swift +++ b/Sources/Sliders/ValueSlider/ValueSlider.swift @@ -4,7 +4,8 @@ public struct ValueSlider: View { @Environment(\.valueSliderStyle) private var style @Environment(\.precisionScrubbing) private var precisionScrubbing @State private var dragOffset: CGFloat? - @GestureState private var gestureState: SliderGestureState? + @GestureState private var thumbGestureState: SliderGestureState? + @GestureState private var trackGestureState: SliderGestureState? private var configuration: ValueSliderStyleConfiguration @@ -13,7 +14,8 @@ public struct ValueSlider: View { self.configuration.with( precisionScrubbing: self.precisionScrubbing, dragOffset: self.$dragOffset, - gestureState: self.$gestureState + thumbGestureState: self.$thumbGestureState, + trackGestureState: self.$trackGestureState ) ) } @@ -40,7 +42,8 @@ extension ValueSlider { onEditingChanged: onEditingChanged, precisionScrubbing: PrecisionScrubbingKey.defaultValue, dragOffset: .constant(0), - gestureState: .init(initialValue: nil) + thumbGestureState: .init(initialValue: nil), + trackGestureState: .init(initialValue: nil) ) ) } @@ -61,7 +64,8 @@ extension ValueSlider { onEditingChanged: onEditingChanged, precisionScrubbing: PrecisionScrubbingKey.defaultValue, dragOffset: .constant(0), - gestureState: .init(initialValue: nil) + thumbGestureState: .init(initialValue: nil), + trackGestureState: .init(initialValue: nil) ) ) } @@ -87,7 +91,8 @@ extension ValueSlider { onEditingChanged: onEditingChanged, precisionScrubbing: PrecisionScrubbingKey.defaultValue, dragOffset: .constant(0), - gestureState: .init(initialValue: nil) + thumbGestureState: .init(initialValue: nil), + trackGestureState: .init(initialValue: nil) ) ) } From 23d21795faeba3ac89ad0092b15bdbe428647917 Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Wed, 9 Aug 2023 21:58:13 +0100 Subject: [PATCH 11/12] Fix warning --- .../Styles/Horizontal/HorizontalRangeSliderStyle.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift index f4b666c..1d6707d 100644 --- a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift +++ b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift @@ -160,7 +160,7 @@ public struct HorizontalRangeSliderStyle Date: Fri, 27 Jun 2025 19:23:09 +0100 Subject: [PATCH 12/12] . --- Sources/Sliders/Base/EditingRange.swift | 12 +++++++++ Sources/Sliders/Base/PrecisionScrubbing.swift | 6 ++--- Sources/Sliders/RangeSlider/RangeSlider.swift | 4 +-- .../Style/RangeSliderStyleConfiguration.swift | 2 +- .../HorizontalRangeSliderStyle.swift | 27 +++++++++++++------ .../Styles/RangeSliderOptions.swift | 1 + .../Vertical/VerticalRangeSliderStyle.swift | 10 ++++--- .../HorizontalValueSliderStyle.swift | 5 ++-- .../Styles/ValueSliderOptions.swift | 1 + .../Vertical/VerticalValueSliderStyle.swift | 5 ++-- 10 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 Sources/Sliders/Base/EditingRange.swift diff --git a/Sources/Sliders/Base/EditingRange.swift b/Sources/Sliders/Base/EditingRange.swift new file mode 100644 index 0000000..b908657 --- /dev/null +++ b/Sources/Sliders/Base/EditingRange.swift @@ -0,0 +1,12 @@ +import Foundation + +public struct EditingRange: OptionSet { + public let rawValue: UInt8 + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } + + public static let upper = EditingRange(rawValue: 0b01) + public static let lower = EditingRange(rawValue: 0b10) +} diff --git a/Sources/Sliders/Base/PrecisionScrubbing.swift b/Sources/Sliders/Base/PrecisionScrubbing.swift index 1a467b7..efafa8e 100644 --- a/Sources/Sliders/Base/PrecisionScrubbing.swift +++ b/Sources/Sliders/Base/PrecisionScrubbing.swift @@ -28,9 +28,9 @@ public extension View { ) -> some View { self.environment( \.precisionScrubbing, - PrecisionScrubbingConfig { offset in - scrubValue(offset).rawValue - } onChange: { value in + PrecisionScrubbingConfig { offset in + scrubValue(offset).rawValue + } onChange: { value in guard let value else { onChange?(nil) return diff --git a/Sources/Sliders/RangeSlider/RangeSlider.swift b/Sources/Sliders/RangeSlider/RangeSlider.swift index 0403464..132cbf0 100644 --- a/Sources/Sliders/RangeSlider/RangeSlider.swift +++ b/Sources/Sliders/RangeSlider/RangeSlider.swift @@ -33,7 +33,7 @@ extension RangeSlider { in bounds: ClosedRange = 0.0...1.0, step: V.Stride = 0.001, distance: ClosedRange = 0.0 ... .infinity, - onEditingChanged: @escaping (Bool) -> Void = { _ in } + onEditingChanged: @escaping (EditingRange) -> Void = { _ in } ) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint { self.init( RangeSliderStyleConfiguration( @@ -60,7 +60,7 @@ extension RangeSlider { in bounds: ClosedRange = 0...1, step: V.Stride = 1, distance: ClosedRange = 0 ... .max, - onEditingChanged: @escaping (Bool) -> Void = { _ in } + onEditingChanged: @escaping (EditingRange) -> Void = { _ in } ) where V : FixedWidthInteger, V.Stride : FixedWidthInteger { self.init( RangeSliderStyleConfiguration( diff --git a/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift b/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift index 0e10dde..c85c335 100644 --- a/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift +++ b/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift @@ -5,7 +5,7 @@ public struct RangeSliderStyleConfiguration { public let bounds: ClosedRange public let step: CGFloat public let distance: ClosedRange - public let onEditingChanged: (Bool) -> Void + public let onEditingChanged: (EditingRange) -> Void public var precisionScrubbing: PrecisionScrubbingConfig public var dragOffset: Binding public var lowerGestureState: GestureState diff --git a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift index 1d6707d..440b6cb 100644 --- a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift +++ b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift @@ -19,10 +19,10 @@ public struct HorizontalRangeSliderStyle CGFloat { distanceFrom( value: configuration.range.wrappedValue.lowerBound, - availableDistance: geometry.size.width - upperThumbSize.width, + availableDistance: geometry.size.width, bounds: configuration.bounds, leadingOffset: lowerThumbSize.width / 2, - trailingOffset: lowerThumbSize.width / 2 + trailingOffset: lowerThumbSize.width / 2 + upperThumbSize.width ) } @@ -37,7 +37,17 @@ public struct HorizontalRangeSliderStyle some View { - let editing = configuration.lowerGestureState.wrappedValue != nil || configuration.upperGestureState.wrappedValue != nil + let editing = { () -> EditingRange in + switch ( + configuration.lowerGestureState.wrappedValue?.speed, + configuration.upperGestureState.wrappedValue?.speed + ) { + case (_?, _?): return [.upper, .lower] + case (_?, nil): return [.lower] + case (nil, _?): return [.upper] + case (nil, nil): return [] + } + }() let precisionScrubbingSpeed = { () -> Float? in switch ( configuration.lowerGestureState.wrappedValue?.speed, @@ -50,15 +60,16 @@ public struct HorizontalRangeSliderStyle: ValueSliderS public func makeBody(configuration: Self.Configuration) -> some View { let prominentGesture = configuration.thumbGestureState.wrappedValue ?? configuration.trackGestureState.wrappedValue + let fullBleedTrack = self.options.contains(.fullBleedTrack) let track = self.track .environment(\.trackValue, configuration.value.wrappedValue) .environment(\.valueTrackConfiguration, ValueTrackConfiguration( bounds: configuration.bounds, - leadingOffset: self.thumbSize.width / 2, - trailingOffset: self.thumbSize.width / 2) + leadingOffset: fullBleedTrack ? 0 : self.thumbSize.width / 2, + trailingOffset: fullBleedTrack ? 0 : self.thumbSize.width / 2) ) .accentColor(Color.accentColor) diff --git a/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift b/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift index 755b78f..cc75c8f 100644 --- a/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift +++ b/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift @@ -4,6 +4,7 @@ public struct ValueSliderOptions: OptionSet { public let rawValue: Int public static let interactiveTrack = ValueSliderOptions(rawValue: 1 << 0) + public static let fullBleedTrack = ValueSliderOptions(rawValue: 1 << 1) public static let defaultOptions: ValueSliderOptions = [] public init(rawValue: Int) { diff --git a/Sources/Sliders/ValueSlider/Styles/Vertical/VerticalValueSliderStyle.swift b/Sources/Sliders/ValueSlider/Styles/Vertical/VerticalValueSliderStyle.swift index ebfe5c0..026a437 100644 --- a/Sources/Sliders/ValueSlider/Styles/Vertical/VerticalValueSliderStyle.swift +++ b/Sources/Sliders/ValueSlider/Styles/Vertical/VerticalValueSliderStyle.swift @@ -8,12 +8,13 @@ public struct VerticalValueSliderStyle: ValueSliderSty private let options: ValueSliderOptions public func makeBody(configuration: Self.Configuration) -> some View { + let fullBleedTrack = self.options.contains(.fullBleedTrack) let track = self.track .environment(\.trackValue, configuration.value.wrappedValue) .environment(\.valueTrackConfiguration, ValueTrackConfiguration( bounds: configuration.bounds, - leadingOffset: self.thumbSize.height / 2, - trailingOffset: self.thumbSize.height / 2) + leadingOffset: fullBleedTrack ? 0 : self.thumbSize.height / 2, + trailingOffset: fullBleedTrack ? 0 : self.thumbSize.height / 2) ) .accentColor(Color.accentColor)