Skip to content

Commit c603297

Browse files
authored
Merge pull request #1 from swift-extensions/styles
Experimental slider
2 parents 226561c + 6ee7ebc commit c603297

11 files changed

+310
-12
lines changed

Examples/SlidersExamples/HorizontalSliderExamplesView.swift

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,21 @@ struct HorizontalSliderExamplesView: View {
66

77
var body: some View {
88
ScrollView {
9-
Group {
9+
Group {
10+
1011
HSlider(value: $model.value1)
1112

13+
// ValueSlider(value: $model.value1)
14+
// .background(Color.yellow)
15+
// .valueSliderStyle(
16+
// HorizontalValueSliderStyle(
17+
// track: { HorizontalValueTrack(value: $0) },
18+
// thumbSize: CGSize(width: 32, height: 32),
19+
// options: .interactiveTrack
20+
// )
21+
// )
22+
23+
1224
HSlider(value: $model.value2,
1325
configuration: .init(
1426
thumbSize: CGSize(width: 16, height: 32)
@@ -116,16 +128,17 @@ struct HorizontalSliderExamplesView: View {
116128

117129
HRangeSlider(
118130
range: $model.range3,
119-
track: HRangeTrack(
120-
range: model.range3,
121-
view: LinearGradient(gradient: Gradient(colors: [.red, .orange, .yellow, .green, .blue, .purple, .pink]), startPoint: .leading, endPoint: .trailing),
122-
configuration: .init(
123-
offsets: 32
131+
track:
132+
HRangeTrack(
133+
range: model.range3,
134+
view: LinearGradient(gradient: Gradient(colors: [.red, .orange, .yellow, .green, .blue, .purple, .pink]), startPoint: .leading, endPoint: .trailing),
135+
configuration: .init(
136+
offsets: 32
137+
)
124138
)
125-
)
126-
.background(LinearGradient(gradient: Gradient(colors: [.red, .orange, .yellow, .green, .blue, .purple, .pink]), startPoint: .leading, endPoint: .trailing).opacity(0.25))
127-
.frame(height: 32)
128-
.cornerRadius(16),
139+
.background(LinearGradient(gradient: Gradient(colors: [.red, .orange, .yellow, .green, .blue, .purple, .pink]), startPoint: .leading, endPoint: .trailing).opacity(0.25))
140+
.frame(height: 32)
141+
.cornerRadius(16),
129142
lowerThumb: HalfCapsule().foregroundColor(.white).shadow(radius: 3),
130143
upperThumb: HalfCapsule().rotation(Angle(degrees: 180)).foregroundColor(.white).shadow(radius: 3),
131144
configuration: .init(

Sources/Sliders/Base/DefaultThumb.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@ public extension CGSize {
1313
static let defaultThumbInteractiveSize : CGSize = CGSize(width: 44, height: 44)
1414
}
1515

16-
#if DEBUG
1716
struct DefaultThumb_Previews: PreviewProvider {
1817
static var previews: some View {
1918
DefaultThumb()
2019
.previewLayout(.fixed(width: 100, height: 100))
2120
}
2221
}
23-
#endif
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import SwiftUI
2+
3+
public struct ValueSlider<Track>: View where Track: View {
4+
@Environment(\.valueSliderStyle) private var style
5+
@State private var dragOffset: CGFloat?
6+
7+
private var configuration: ValueSliderStyleConfiguration
8+
9+
public var body: some View {
10+
self.style.makeBody(configuration:
11+
self.configuration.with(dragOffset: self.$dragOffset)
12+
)
13+
}
14+
}
15+
16+
extension ValueSlider {
17+
init(_ configuration: ValueSliderStyleConfiguration) {
18+
self.configuration = configuration
19+
}
20+
}
21+
22+
extension ValueSlider where Track == ValueSliderStyleConfiguration.Track {
23+
public init<V>(value: Binding<V>, in bounds: ClosedRange<V> = 0.0...1.0, step: V.Stride = 0.001, onEditingChanged: @escaping (Bool) -> Void = { _ in }) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint {
24+
25+
self.init(
26+
ValueSliderStyleConfiguration(
27+
value: Binding(get: { CGFloat(value.wrappedValue) }, set: { value.wrappedValue = V($0) }),
28+
bounds: CGFloat(bounds.lowerBound)...CGFloat(bounds.upperBound),
29+
step: CGFloat(step),
30+
onEditingChanged: onEditingChanged,
31+
dragOffset: .constant(0),
32+
track: .init(view: DefaultHorizontalValueTrack(value: CGFloat(value.wrappedValue))),
33+
thumb: .init(view: DefaultThumb())
34+
)
35+
)
36+
}
37+
}
38+
39+
extension ValueSlider {
40+
public init<V>(value: Binding<V>, in bounds: ClosedRange<V> = 0.0...1.0, step: V.Stride = 0.001, track: Track, onEditingChanged: @escaping (Bool) -> Void = { _ in }) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint {
41+
42+
self.init(
43+
ValueSliderStyleConfiguration(
44+
value: Binding(get: { CGFloat(value.wrappedValue) }, set: { value.wrappedValue = V($0) }),
45+
bounds: CGFloat(bounds.lowerBound)...CGFloat(bounds.upperBound),
46+
step: CGFloat(step),
47+
onEditingChanged: onEditingChanged,
48+
dragOffset: .constant(0),
49+
track: .init(view: track),
50+
thumb: .init(view: DefaultThumb())
51+
)
52+
)
53+
}
54+
}
55+
56+
struct ValueSlider_Previews: PreviewProvider {
57+
static var previews: some View {
58+
ValueSlider(value: .constant(0.3))
59+
.previewLayout(.fixed(width: 300, height: 100))
60+
}
61+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import SwiftUI
2+
3+
public struct HorizontalValueSliderStyle<Track: View>: ValueSliderStyle {
4+
private let track: (CGFloat) -> Track
5+
private let thumbSize: CGSize
6+
private let thumbInteractiveSize: CGSize
7+
private let options: HorizontalValueSliderOptions
8+
9+
public func makeBody(configuration: Self.Configuration) -> some View {
10+
GeometryReader { geometry in
11+
ZStack(alignment: .leading) {
12+
if self.options.contains(.interactiveTrack) {
13+
self.track(configuration.value.wrappedValue)
14+
.gesture(
15+
DragGesture(minimumDistance: 0)
16+
.onChanged { gestureValue in
17+
let computedValue = valueFrom(
18+
distance: gestureValue.location.x,
19+
availableDistance: geometry.size.width,
20+
bounds: configuration.bounds,
21+
step: configuration.step,
22+
leadingOffset: self.thumbSize.width / 2,
23+
trailingOffset: self.thumbSize.width / 2
24+
)
25+
configuration.value.wrappedValue = computedValue
26+
configuration.onEditingChanged(true)
27+
}
28+
.onEnded { _ in
29+
configuration.onEditingChanged(false)
30+
}
31+
)
32+
} else {
33+
self.track(configuration.value.wrappedValue)
34+
}
35+
36+
ZStack {
37+
configuration.thumb
38+
.frame(width: self.thumbSize.width, height: self.thumbSize.height)
39+
}
40+
.frame(minWidth: self.thumbInteractiveSize.width, minHeight: self.thumbInteractiveSize.height)
41+
42+
.position(
43+
x: distanceFrom(
44+
value: configuration.value.wrappedValue,
45+
availableDistance: geometry.size.width,
46+
bounds: configuration.bounds,
47+
leadingOffset: self.thumbSize.width / 2,
48+
trailingOffset: self.thumbSize.width / 2
49+
),
50+
y: geometry.size.height / 2
51+
)
52+
.gesture(
53+
DragGesture()
54+
.onChanged { gestureValue in
55+
if configuration.dragOffset.wrappedValue == nil {
56+
configuration.dragOffset.wrappedValue = gestureValue.startLocation.x - distanceFrom(
57+
value: configuration.value.wrappedValue,
58+
availableDistance: geometry.size.width,
59+
bounds: configuration.bounds,
60+
leadingOffset: self.thumbSize.width / 2,
61+
trailingOffset: self.thumbSize.width / 2
62+
)
63+
}
64+
65+
let computedValue = valueFrom(
66+
distance: gestureValue.location.x - (configuration.dragOffset.wrappedValue ?? 0),
67+
availableDistance: geometry.size.width,
68+
bounds: configuration.bounds,
69+
step: configuration.step,
70+
leadingOffset: self.thumbSize.width / 2,
71+
trailingOffset: self.thumbSize.width / 2
72+
)
73+
74+
configuration.value.wrappedValue = computedValue
75+
configuration.onEditingChanged(true)
76+
}
77+
.onEnded { _ in
78+
configuration.dragOffset.wrappedValue = nil
79+
configuration.onEditingChanged(false)
80+
}
81+
)
82+
}
83+
84+
}
85+
.frame(minHeight: self.thumbInteractiveSize.height)
86+
}
87+
88+
public init(@ViewBuilder track: @escaping (CGFloat) -> Track, thumbSize: CGSize = CGSize(width: 32, height: 32), thumbInteractiveSize: CGSize = CGSize(width: 44, height: 44), options: HorizontalValueSliderOptions = .defaultOptions) {
89+
self.track = track
90+
self.thumbSize = thumbSize
91+
self.thumbInteractiveSize = thumbInteractiveSize
92+
self.options = options
93+
}
94+
}
95+
96+
public struct HorizontalValueSliderOptions: OptionSet {
97+
public let rawValue: Int
98+
99+
public static let interactiveTrack = HorizontalValueSliderOptions(rawValue: 1 << 0)
100+
public static let defaultOptions: HorizontalValueSliderOptions = []
101+
102+
public init(rawValue: Int) {
103+
self.rawValue = rawValue
104+
}
105+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import SwiftUI
2+
3+
struct AnyValueSliderStyle: ValueSliderStyle {
4+
private let styleMakeBody: (ValueSliderStyle.Configuration) -> AnyView
5+
6+
init<S: ValueSliderStyle>(_ style: S) {
7+
self.styleMakeBody = style.makeTypeErasedBody
8+
}
9+
10+
func makeBody(configuration: ValueSliderStyle.Configuration) -> AnyView {
11+
self.styleMakeBody(configuration)
12+
}
13+
}
14+
15+
fileprivate extension ValueSliderStyle {
16+
func makeTypeErasedBody(configuration: ValueSliderStyle.Configuration) -> AnyView {
17+
AnyView(makeBody(configuration: configuration))
18+
}
19+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import SwiftUI
2+
3+
extension EnvironmentValues {
4+
var valueSliderStyle: AnyValueSliderStyle {
5+
get {
6+
return self[ValueSliderStyleKey.self]
7+
}
8+
set {
9+
self[ValueSliderStyleKey.self] = newValue
10+
}
11+
}
12+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import SwiftUI
2+
3+
/// Defines the implementation of all `ValueSlider` instances within a view
4+
/// hierarchy.
5+
///
6+
/// To configure the current `ValueSlider` for a view hiearchy, use the
7+
/// `.valueSliderStyle()` modifier.
8+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
9+
public protocol ValueSliderStyle {
10+
/// A `View` representing the body of a `ValueSlider`.
11+
associatedtype Body : View
12+
13+
/// Creates a `View` representing the body of a `ValueSlider`.
14+
///
15+
/// - Parameter configuration: The properties of the value slider instance being
16+
/// created.
17+
///
18+
/// This method will be called for each instance of `ValueSlider` created within
19+
/// a view hierarchy where this style is the current `ValueSliderStyle`.
20+
func makeBody(configuration: Self.Configuration) -> Self.Body
21+
22+
/// The properties of a `ValueSlider` instance being created.
23+
typealias Configuration = ValueSliderStyleConfiguration
24+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import SwiftUI
2+
3+
public struct ValueSliderStyleConfiguration {
4+
public let value: Binding<CGFloat>
5+
public let bounds: ClosedRange<CGFloat>
6+
public let step: CGFloat
7+
public let onEditingChanged: (Bool) -> Void
8+
public var dragOffset: Binding<CGFloat?>
9+
public let track: ValueSliderStyleConfiguration.Track
10+
public let thumb: ValueSliderStyleConfiguration.Thumb
11+
12+
func with(dragOffset: Binding<CGFloat?>) -> Self {
13+
var mutSelf = self
14+
mutSelf.dragOffset = dragOffset
15+
return mutSelf
16+
}
17+
}
18+
19+
public extension ValueSliderStyleConfiguration {
20+
struct Track: View {
21+
let typeErasedTrack: AnyView
22+
23+
init<T: View>(view: T) {
24+
self.typeErasedTrack = AnyView(view)
25+
}
26+
27+
public var body: some View {
28+
self.typeErasedTrack
29+
}
30+
}
31+
}
32+
33+
public extension ValueSliderStyleConfiguration {
34+
struct Thumb: View {
35+
let typeErasedThumb: AnyView
36+
37+
init<T: View>(view: T) {
38+
self.typeErasedThumb = AnyView(view)
39+
}
40+
41+
public var body: some View {
42+
self.typeErasedThumb
43+
}
44+
}
45+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import SwiftUI
2+
3+
struct ValueSliderStyleKey: EnvironmentKey {
4+
static let defaultValue: AnyValueSliderStyle = AnyValueSliderStyle(HorizontalValueSliderStyle(track: { _ in Capsule() }))
5+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import SwiftUI
2+
3+
extension View {
4+
/// Sets the style for `ValueSlider` within the environment of `self`.
5+
public func valueSliderStyle<S>(_ style: S) -> some View where S : ValueSliderStyle {
6+
self.environment(\.valueSliderStyle, AnyValueSliderStyle(style))
7+
}
8+
}

0 commit comments

Comments
 (0)