diff --git a/README.md b/README.md
index 47739fa..76cc78e 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,8 @@ This package allows you to build highly customizable sliders and tracks for iOS,
- Build your own sliders and tracks using composition
- Highly customizable
- Horizontal and Vertical styles
-- Range and XY values
+- Range sliders with minimum/maximum value distance
+- XY sliders
- Different sizes for lower and upper range thumbs
@@ -53,7 +54,7 @@ See the preview of each file to see an example
Use any SwiftUI view modifiers to create custom tracks and thumbs.
```swift
-RangeSlider(range: $model.range2)
+RangeSlider(range: $model.range2, distance: 0.1 ... 1.0)
.rangeSliderStyle(
HorizontalRangeSliderStyle(
track:
@@ -84,4 +85,4 @@ RangeSlider(range: $model.range2)
Feel free to contribute via fork/pull request to master branch. If you want to request a feature or report a bug please start a new issue.
## Coffee Contributions
-If you find this project useful please consider becoming my GitHub sponsor.
+If you find this project useful please consider becoming our GitHub sponsor.
diff --git a/Sources/Sliders/Base/LinearRangeMath.swift b/Sources/Sliders/Base/LinearRangeMath.swift
index f19d6b9..9456027 100644
--- a/Sources/Sliders/Base/LinearRangeMath.swift
+++ b/Sources/Sliders/Base/LinearRangeMath.swift
@@ -7,3 +7,27 @@ import SwiftUI
let offsetUpperValue = distanceFrom(value: range.upperBound, availableDistance: overallLength, bounds: bounds, leadingOffset: upperStartOffset, trailingOffset: upperEndOffset)
return max(0, offsetUpperValue - offsetLowerValue)
}
+
+@inlinable func rangeFrom(updatedLowerBound: CGFloat, upperBound: CGFloat, bounds: ClosedRange, distance: ClosedRange, forceAdjacent: Bool) -> ClosedRange {
+ if forceAdjacent {
+ let finalLowerBound = min(updatedLowerBound, bounds.upperBound - distance.lowerBound)
+ let finalUpperBound = min(min(max(updatedLowerBound + distance.lowerBound, upperBound), updatedLowerBound + distance.upperBound), bounds.upperBound)
+ return finalLowerBound ... finalUpperBound
+ } else {
+ let finalLowerBound = min(updatedLowerBound, upperBound - distance.lowerBound)
+ let finalUpperBound = min(upperBound, updatedLowerBound + distance.upperBound)
+ return finalLowerBound ... finalUpperBound
+ }
+}
+
+@inlinable func rangeFrom(lowerBound: CGFloat, updatedUpperBound: CGFloat, bounds: ClosedRange, distance: ClosedRange, forceAdjacent: Bool) -> ClosedRange {
+ if forceAdjacent {
+ let finalLowerBound = max(max(min(lowerBound, updatedUpperBound - distance.lowerBound), updatedUpperBound - distance.upperBound), bounds.lowerBound)
+ let finalUpperBound = max(updatedUpperBound, bounds.lowerBound + distance.lowerBound)
+ return finalLowerBound ... finalUpperBound
+ } else {
+ let finalLowerBound = max(lowerBound, updatedUpperBound - distance.upperBound)
+ let finalUpperBound = max(lowerBound + distance.lowerBound, updatedUpperBound)
+ return finalLowerBound ... finalUpperBound
+ }
+}
diff --git a/Sources/Sliders/PointSlider/PointSlider.swift b/Sources/Sliders/PointSlider/PointSlider.swift
index 4260d01..ed6125b 100644
--- a/Sources/Sliders/PointSlider/PointSlider.swift
+++ b/Sources/Sliders/PointSlider/PointSlider.swift
@@ -38,7 +38,7 @@ extension PointSlider {
}
extension PointSlider {
- public init(x: Binding, xBounds: ClosedRange = 0...1, xStep: V.Stride = 1, y: Binding, yBounds: ClosedRange = 0...1, yStep: V.Stride = 1, onEditingChanged: @escaping (Bool) -> Void = { _ in }) where V : BinaryInteger, V.Stride : BinaryInteger {
+ public init(x: Binding, xBounds: ClosedRange = 0...1, xStep: V.Stride = 1, y: Binding, yBounds: ClosedRange = 0...1, yStep: V.Stride = 1, onEditingChanged: @escaping (Bool) -> Void = { _ in }) where V : FixedWidthInteger, V.Stride : FixedWidthInteger {
self.init(
PointSliderStyleConfiguration(
diff --git a/Sources/Sliders/RangeSlider/RangeSlider.swift b/Sources/Sliders/RangeSlider/RangeSlider.swift
index 7a7aa45..7b72ee5 100644
--- a/Sources/Sliders/RangeSlider/RangeSlider.swift
+++ b/Sources/Sliders/RangeSlider/RangeSlider.swift
@@ -20,16 +20,22 @@ extension RangeSlider {
}
extension RangeSlider {
- public init(range: 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(
+ range: Binding>,
+ in bounds: ClosedRange = 0.0...1.0,
+ step: V.Stride = 0.001,
+ distance: ClosedRange = 0.0 ... .infinity,
+ onEditingChanged: @escaping (Bool) -> Void = { _ in }
+ ) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint {
self.init(
RangeSliderStyleConfiguration(
range: Binding(
- get: { CGFloat(range.wrappedValue.clamped(to: bounds).lowerBound)...CGFloat(range.wrappedValue.clamped(to: bounds).upperBound) },
- set: { range.wrappedValue = V($0.lowerBound)...V($0.upperBound) }
+ get: { CGFloat(range.wrappedValue.clamped(to: bounds).lowerBound) ... CGFloat(range.wrappedValue.clamped(to: bounds).upperBound) },
+ set: { range.wrappedValue = V($0.lowerBound) ... V($0.upperBound) }
),
- bounds: CGFloat(bounds.lowerBound)...CGFloat(bounds.upperBound),
+ bounds: CGFloat(bounds.lowerBound) ... CGFloat(bounds.upperBound),
step: CGFloat(step),
+ distance: CGFloat(distance.lowerBound) ... CGFloat(distance.upperBound),
onEditingChanged: onEditingChanged,
dragOffset: .constant(0)
)
@@ -38,16 +44,22 @@ extension RangeSlider {
}
extension RangeSlider {
- public init(range: Binding>, in bounds: ClosedRange = 0...1, step: V.Stride = 1, onEditingChanged: @escaping (Bool) -> Void = { _ in }) where V : BinaryInteger, V.Stride : BinaryInteger {
-
+ public init(
+ range: Binding>,
+ in bounds: ClosedRange = 0...1,
+ step: V.Stride = 1,
+ distance: ClosedRange = 0 ... .max,
+ onEditingChanged: @escaping (Bool) -> Void = { _ in }
+ ) where V : FixedWidthInteger, V.Stride : FixedWidthInteger {
self.init(
RangeSliderStyleConfiguration(
range: Binding(
- get: { CGFloat(range.wrappedValue.lowerBound)...CGFloat(range.wrappedValue.upperBound) },
- set: { range.wrappedValue = V($0.lowerBound)...V($0.upperBound) }
+ get: { CGFloat(range.wrappedValue.lowerBound) ... CGFloat(range.wrappedValue.upperBound) },
+ set: { range.wrappedValue = V($0.lowerBound) ... V($0.upperBound) }
),
- bounds: CGFloat(bounds.lowerBound)...CGFloat(bounds.upperBound),
+ bounds: CGFloat(bounds.lowerBound) ... CGFloat(bounds.upperBound),
step: CGFloat(step),
+ distance: CGFloat(distance.lowerBound) ... CGFloat(distance.upperBound),
onEditingChanged: onEditingChanged,
dragOffset: .constant(0)
)
@@ -59,7 +71,9 @@ struct RangeSlider_Previews: PreviewProvider {
static var previews: some View {
Group {
HorizontalRangeSlidersPreview()
+ .previewDisplayName("Horizontal Range Sliders")
VerticalRangeSlidersPreview()
+ .previewDisplayName("Vertical Range Sliders")
}
}
}
@@ -76,7 +90,7 @@ private struct HorizontalRangeSlidersPreview: View {
VStack {
RangeSlider(range: $range1)
- RangeSlider(range: $range2)
+ RangeSlider(range: $range2, distance: 0.3 ... 1.0)
.rangeSliderStyle(
HorizontalRangeSliderStyle(
track:
@@ -180,7 +194,7 @@ private struct VerticalRangeSlidersPreview: View {
VerticalRangeSliderStyle()
)
- RangeSlider(range: $range2)
+ RangeSlider(range: $range2, distance: 0.5 ... 0.7)
.rangeSliderStyle(
VerticalRangeSliderStyle(
track:
diff --git a/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift b/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift
index 7632655..639634c 100644
--- a/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift
+++ b/Sources/Sliders/RangeSlider/Style/RangeSliderStyleConfiguration.swift
@@ -4,6 +4,7 @@ public struct RangeSliderStyleConfiguration {
public let range: Binding>
public let bounds: ClosedRange
public let step: CGFloat
+ public let distance: ClosedRange
public let onEditingChanged: (Bool) -> Void
public var dragOffset: Binding
diff --git a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift
index d361625..5ceb961 100644
--- a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift
+++ b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift
@@ -13,8 +13,8 @@ public struct HorizontalRangeSliderStyle Void
- let onSelectUpper: () -> Void
+ let onSelectLower: () -> Void
+ let onSelectUpper: () -> Void
public func makeBody(configuration: Self.Configuration) -> some View {
GeometryReader { geometry in
@@ -46,7 +46,7 @@ public struct HorizontalRangeSliderStyle(value: Binding, in bounds: ClosedRange = 0...1, step: V.Stride = 1, onEditingChanged: @escaping (Bool) -> Void = { _ in }) where V : BinaryInteger, V.Stride : BinaryInteger {
+ public init(value: Binding, in bounds: ClosedRange = 0...1, step: V.Stride = 1, onEditingChanged: @escaping (Bool) -> Void = { _ in }) where V : FixedWidthInteger, V.Stride : FixedWidthInteger {
self.init(
ValueSliderStyleConfiguration(
value: Binding(get: { CGFloat(value.wrappedValue) }, set: { value.wrappedValue = V($0) }),
diff --git a/Tests/SlidersTests/RangeDistanceTests.swift b/Tests/SlidersTests/RangeDistanceTests.swift
index 3b4997b..f84f8d2 100644
--- a/Tests/SlidersTests/RangeDistanceTests.swift
+++ b/Tests/SlidersTests/RangeDistanceTests.swift
@@ -36,4 +36,149 @@ class RangeDistanceTests: XCTestCase {
let fullRangeDistance = rangeDistance(overallLength: 100, range: -2.0...2.0, bounds: -2.0...2.0, lowerStartOffset: 5, lowerEndOffset: 15, upperStartOffset: 5, upperEndOffset: 15)
XCTAssert(fullRangeDistance == 80)
}
+
+ func testRangeUpdatingLowerBoundWithUnlimitedDistance() {
+ XCTAssertEqual(
+ rangeFrom(updatedLowerBound: 0.0, upperBound: 1.0, bounds: 0.0 ... 1.0, distance: 0.0 ... 1.0, forceAdjacent: true),
+ 0.0 ... 1.0
+ )
+
+ XCTAssertEqual(
+ rangeFrom(updatedLowerBound: 0.5, upperBound: 0.5, bounds: 0.0 ... 1.0, distance: 0.0 ... 1.0, forceAdjacent: true),
+ 0.5 ... 0.5
+ )
+
+ XCTAssertEqual(
+ rangeFrom(updatedLowerBound: 0.6, upperBound: 0.5, bounds: 0.0 ... 1.0, distance: 0.0 ... 1.0, forceAdjacent: true),
+ 0.6 ... 0.6
+ )
+
+ XCTAssertEqual(
+ rangeFrom(updatedLowerBound: 0.0, upperBound: 1.0, bounds: 0.0 ... 1.0, distance: 0.0 ... 1.0, forceAdjacent: false),
+ 0.0 ... 1.0
+ )
+
+ XCTAssertEqual(
+ rangeFrom(updatedLowerBound: 0.5, upperBound: 0.5, bounds: 0.0 ... 1.0, distance: 0.0 ... 1.0, forceAdjacent: false),
+ 0.5 ... 0.5
+ )
+
+ XCTAssertEqual(
+ rangeFrom(updatedLowerBound: 0.6, upperBound: 0.5, bounds: 0.0 ... 1.0, distance: 0.0 ... 1.0, forceAdjacent: false),
+ 0.5 ... 0.5
+ )
+ }
+
+ func testRangeUpdatingUpperBoundWithUnlimitedDistance() {
+ XCTAssertEqual(
+ rangeFrom(lowerBound: 0.0, updatedUpperBound: 1.0, bounds: 0.0 ... 1.0, distance: 0.0 ... 1.0, forceAdjacent: true),
+ 0.0 ... 1.0
+ )
+
+ XCTAssertEqual(
+ rangeFrom(lowerBound: 0.5, updatedUpperBound: 0.5, bounds: 0.0 ... 1.0, distance: 0.0 ... 1.0, forceAdjacent: true),
+ 0.5 ... 0.5
+ )
+
+ XCTAssertEqual(
+ rangeFrom(lowerBound: 0.5, updatedUpperBound: 0.4, bounds: 0.0 ... 1.0, distance: 0.0 ... 1.0, forceAdjacent: true),
+ 0.4 ... 0.4
+ )
+
+ XCTAssertEqual(
+ rangeFrom(lowerBound: 0.0, updatedUpperBound: 1.0, bounds: 0.0 ... 1.0, distance: 0.0 ... 1.0, forceAdjacent: false),
+ 0.0 ... 1.0
+ )
+
+ XCTAssertEqual(
+ rangeFrom(lowerBound: 0.5, updatedUpperBound: 0.5, bounds: 0.0 ... 1.0, distance: 0.0 ... 1.0, forceAdjacent: false),
+ 0.5 ... 0.5
+ )
+
+ XCTAssertEqual(
+ rangeFrom(lowerBound: 0.5, updatedUpperBound: 0.4, bounds: 0.0 ... 1.0, distance: 0.0 ... 1.0, forceAdjacent: false),
+ 0.5 ... 0.5
+ )
+ }
+
+ func testRangeUpdatingLowerBoundWithDistance() {
+ XCTAssertEqual(
+ rangeFrom(updatedLowerBound: 0.95, upperBound: 1.0, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: true),
+ 0.9 ... 1.0
+ )
+
+ XCTAssertEqual(
+ rangeFrom(updatedLowerBound: 0.0, upperBound: 1.0, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: true),
+ 0.0 ... 0.5
+ )
+
+ XCTAssertEqual(
+ rangeFrom(updatedLowerBound: 0.5, upperBound: 0.5, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: true),
+ 0.5 ... 0.6
+ )
+
+ XCTAssertEqual(
+ rangeFrom(updatedLowerBound: 0.6, upperBound: 0.5, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: true),
+ 0.6 ... 0.7
+ )
+
+ XCTAssertEqual(
+ rangeFrom(updatedLowerBound: 0.0, upperBound: 1.0, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: false),
+ 0.0 ... 0.5
+ )
+
+ XCTAssertEqual(
+ rangeFrom(updatedLowerBound: 0.5, upperBound: 0.5, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: false),
+ 0.4 ... 0.5
+ )
+
+ XCTAssertEqual(
+ rangeFrom(updatedLowerBound: 0.6, upperBound: 0.5, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: false),
+ 0.4 ... 0.5
+ )
+ }
+
+ func testRangeUpdatingUpperBoundWithDistance() {
+ XCTAssertEqual(
+ rangeFrom(lowerBound: 0.0, updatedUpperBound: 0.05, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: true),
+ 0.0 ... 0.1
+ )
+
+ XCTAssertEqual(
+ rangeFrom(lowerBound: 0.0, updatedUpperBound: 1.0, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: true),
+ 0.5 ... 1.0
+ )
+
+ XCTAssertEqual(
+ rangeFrom(lowerBound: 0.5, updatedUpperBound: 0.5, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: true),
+ 0.4 ... 0.5
+ )
+
+ XCTAssertEqual(
+ rangeFrom(lowerBound: 0.5, updatedUpperBound: 0.4, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: true).lowerBound,
+ 0.3,
+ accuracy: 0.0001
+ )
+
+ XCTAssertEqual(
+ rangeFrom(lowerBound: 0.5, updatedUpperBound: 0.4, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: true).upperBound,
+ 0.4,
+ accuracy: 0.0001
+ )
+
+ XCTAssertEqual(
+ rangeFrom(lowerBound: 0.0, updatedUpperBound: 1.0, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: false),
+ 0.5 ... 1.0
+ )
+
+ XCTAssertEqual(
+ rangeFrom(lowerBound: 0.5, updatedUpperBound: 0.5, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: false),
+ 0.5 ... 0.6
+ )
+
+ XCTAssertEqual(
+ rangeFrom(lowerBound: 0.5, updatedUpperBound: 0.4, bounds: 0.0 ... 1.0, distance: 0.1 ... 0.5, forceAdjacent: false),
+ 0.5 ... 0.6
+ )
+ }
}