Skip to content

Commit 1ef8117

Browse files
committed
Introducing RxSliderStyle/RadixSlider/RadixSliderStyle
1 parent 92b24c6 commit 1ef8117

13 files changed

+732
-2
lines changed

Sources/RadixUI/DemoViews/PickerDemoView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ struct PickerDemoView: View {
1818
)
1919
}
2020

21-
@State private var selection: [RadixToggleVariant] = [.checkbox, .switch, .toggle]
2221
@State private var selected: RadixToggleVariant = .switch
2322

2423
var body: some View {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// SliderDemo.swift
3+
// RadixUI
4+
//
5+
// Created by Amir Mohammadi on 11/23/1403 AP.
6+
//
7+
8+
import SwiftUI
9+
10+
struct SliderDemo: View {
11+
12+
@State private var value1 = 5.0
13+
@State private var value2 = 50.0
14+
15+
var body: some View {
16+
HStack(spacing: 25) {
17+
Text(String(describing: value1))
18+
.frame(width: 50)
19+
RadixSlider(value: $value1, step: 1, in: 0...10)
20+
.rxSliderStyle(
21+
.radixSoft(
22+
radius: .full,
23+
size: .medium,
24+
color: .grass
25+
)
26+
)
27+
}
28+
.padding()
29+
.frame(width: 400)
30+
HStack(spacing: 25) {
31+
Text(String(describing: value2))
32+
.frame(width: 50)
33+
RadixSlider(value: $value2, step: 5, in: 0...100)
34+
.rxSliderStyle(
35+
.radixSurface(
36+
radius: .full,
37+
size: .medium,
38+
color: .grass
39+
)
40+
)
41+
}
42+
.padding()
43+
.frame(width: 400)
44+
}
45+
}
46+
47+
#Preview {
48+
SliderDemo()
49+
}

Sources/RadixUI/Helpers/Enums.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ public enum RadixButtonVariant {
2121
case custom, ghost, outline, soft, solid, surface
2222
}
2323

24+
// MARK: - Enums used in SliderStyle
25+
public enum RadixSliderType {
26+
case ranged, single
27+
}
28+
29+
public enum RadixSliderSize {
30+
case small, medium, large
31+
}
32+
33+
public enum RadixSliderVariant {
34+
case soft, surface
35+
}
36+
2437
// MARK: - Enums used in TextFieldStyle
2538
public enum RadixTextFieldVariant {
2639
case surface, soft

Sources/RadixUI/Helpers/EnvironmentKey.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,20 @@ fileprivate struct LoadingKey: EnvironmentKey {
1111
static let defaultValue: Binding<Bool> = .constant(false)
1212
}
1313

14+
fileprivate struct RxSliderStyleKey: EnvironmentKey {
15+
static let defaultValue: AnyRxSliderStyle = AnyRxSliderStyle(DefaultRxSliderStyle())
16+
}
17+
1418
public extension EnvironmentValues {
1519

1620
var isLoading: Binding<Bool> {
1721
get { self[LoadingKey.self] }
1822
set { self[LoadingKey.self] = newValue }
1923
}
2024

25+
var rxSliderStyle: AnyRxSliderStyle {
26+
get { self[RxSliderStyleKey.self] }
27+
set { self[RxSliderStyleKey.self] = newValue }
28+
}
29+
2130
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//
2+
// Shape+Extensions.swift
3+
// RadixUI
4+
//
5+
// Created by Amir Mohammadi on 11/23/1403 AP.
6+
//
7+
8+
import SwiftUI
9+
10+
//MARK: - Slider Thumb Shape Modifiers
11+
internal extension Shape {
12+
13+
func thumbBorderShapeModifier(size: CGSize) -> some View {
14+
self
15+
.stroke(RadixAutoColor.gray.border3, lineWidth: 2)
16+
.background(.clear)
17+
.frame(width: size.width, height: size.height)
18+
}
19+
20+
func thumbActiveShapeModifier(size: CGSize) -> some View {
21+
self
22+
.fill(RadixAutoColor.blackA.border2)
23+
.frame(width: size.width, height: size.height)
24+
}
25+
26+
func thumbShapeModifier(size: CGSize) -> some View {
27+
self
28+
.fill(RadixAutoColor.whiteA.text2)
29+
.frame(width: size.width, height: size.height)
30+
}
31+
32+
}
33+
34+
// MARK: - Slider Track Shape Modifiers
35+
internal extension Shape {
36+
37+
func trackShapeBorderModifier(size: CGFloat) -> some View {
38+
self
39+
.stroke(RadixAutoColor.gray.border2, lineWidth: 1)
40+
.background(.clear)
41+
.frame(height: size)
42+
}
43+
44+
func trackShapeBaseModifier(size: CGFloat, color: Color) -> some View {
45+
self
46+
.fill(color)
47+
.frame(height: size)
48+
}
49+
50+
func trackShapeFillModifier(_ color: Color, _ percentage: CGFloat, _ size: CGFloat) -> some View {
51+
self
52+
.fill(color)
53+
.frame(width: percentage, height: size)
54+
}
55+
56+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// Slider+Extensions.swift
3+
// RadixUI
4+
//
5+
// Created by Amir Mohammadi on 11/18/1403 AP.
6+
//
7+
8+
import SwiftUI
9+
10+
extension View where Self == RadixSlider {
11+
12+
public func rxSliderStyle<S>(_ style: S) -> some View where S: RxSliderStyle {
13+
self.environment(\.rxSliderStyle, AnyRxSliderStyle(style))
14+
}
15+
16+
}
17+
18+
extension RxSliderStyle where Self == RadixSliderStyle {
19+
20+
public static func radixSoft(
21+
radius: RadixElementShapeRadius,
22+
size: RadixSliderSize? = nil,
23+
color: RadixAutoColor? = nil
24+
) -> Self {
25+
.init(
26+
variant: .soft,
27+
radius: radius,
28+
size: size,
29+
color: color
30+
)
31+
}
32+
33+
public static func radixSurface(
34+
radius: RadixElementShapeRadius,
35+
size: RadixSliderSize? = nil,
36+
color: RadixAutoColor? = nil
37+
) -> Self {
38+
.init(
39+
variant: .surface,
40+
radius: radius,
41+
size: size,
42+
color: color
43+
)
44+
}
45+
46+
}

Sources/RadixUI/RadixThemes/Picker/RadixSegmentedPicker.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,5 @@ public struct RadixSegmentedPicker {
5353
}
5454
}
5555
#elseif canImport(AppKit)
56-
#warning("TODO: Add RadixSegmentedPicker for macOS")
5756
#endif
57+
#warning("TODO: Add RadixSegmentedPicker for macOS")
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
//
2+
// RadixSlider.swift
3+
// RadixUI
4+
//
5+
// Created by Amir Mohammadi on 11/18/1403 AP.
6+
//
7+
8+
import SwiftUI
9+
10+
public struct RadixSlider: View {
11+
12+
@Environment(\.rxSliderStyle) private var rxStyle
13+
@Environment(\.isEnabled) private var isEnabled
14+
15+
private var value: Binding<Double>
16+
private var step: Double = 0.1
17+
private var range: ClosedRange<Double> = 0.0...1.0
18+
private var onEditingChanged: ((Bool) -> Void)?
19+
20+
@State private var isActive: Bool = false
21+
@State private var frameWidth: CGFloat = 0
22+
23+
private let space: String = "RadixSlider"
24+
25+
// Configuration Definition
26+
private var configuration: RxSliderStyleConfiguration {
27+
28+
.init(
29+
value: value,
30+
step: step,
31+
minValue: range.lowerBound,
32+
maxValue: range.upperBound,
33+
isActive: isActive,
34+
isDisabled: !isEnabled,
35+
filledPercentage: (value.wrappedValue - range.lowerBound) / (range.upperBound-range.lowerBound) * frameWidth,
36+
onEditingChanged: onEditingChanged ?? { _ in }
37+
)
38+
39+
}
40+
41+
public var body: some View {
42+
rxStyle.makeTrack(configuration: configuration)
43+
.overlay {
44+
GeometryReader { proxy in
45+
ZStack(alignment: .center) {
46+
rxStyle.makeThumb(configuration: configuration)
47+
.offset(thumbOffset(proxy))
48+
.gesture(makeGesture(proxy))
49+
.allowsHitTesting(isEnabled)
50+
}
51+
.frame(width: proxy.size.width, height: proxy.size.height)
52+
.onAppear {
53+
frameWidth = proxy.size.width
54+
}
55+
}
56+
}
57+
.coordinateSpace(name: space)
58+
}
59+
60+
}
61+
62+
// MARK: - RadixSlider Ini
63+
extension RadixSlider {
64+
65+
public init(
66+
value: Binding<Double>,
67+
step: Double = 1.0,
68+
onEditingChanged: ((Bool) -> Void)? = nil
69+
) {
70+
self.value = value
71+
self.step = step
72+
self.onEditingChanged = onEditingChanged
73+
}
74+
75+
public init(
76+
value: Binding<Double>,
77+
step: Double = 1.0,
78+
in range: ClosedRange<Double>,
79+
onEditingChanged: ((Bool) -> Void)? = nil
80+
) {
81+
self.value = value
82+
self.step = step
83+
self.range = range
84+
self.onEditingChanged = onEditingChanged
85+
}
86+
87+
}
88+
89+
// MARK: - Calculations
90+
extension RadixSlider {
91+
92+
/*
93+
uses an arbitrarily large number to gesture a line segment
94+
that is guarenteed to intersect with the bounding box,
95+
then finds those points of intersection to be used as
96+
the start and end points of the slider
97+
*/
98+
private func calculateEndPoints(
99+
_ proxy: GeometryProxy
100+
) -> (
101+
start: CGPoint, end: CGPoint
102+
) {
103+
let w = proxy.size.width
104+
let h = proxy.size.height
105+
106+
return (CGPoint(x: 0, y: h / 2), CGPoint(x: w, y: h / 2))
107+
}
108+
109+
/*
110+
calculates the normalized position of the point `p`
111+
along the horizontal line segment defined by `L1` and `L2`,
112+
clamping the value between 0 and 1.
113+
*/
114+
public func calculateParameter(
115+
_ L1: CGPoint,
116+
_ L2: CGPoint,
117+
_ p: CGPoint
118+
) -> CGFloat {
119+
return max(min((p.x - L1.x) / (L2.x - L1.x), 1), 0)
120+
}
121+
122+
private func thumbOffset(
123+
_ proxy: GeometryProxy
124+
) -> CGSize {
125+
126+
let ends = calculateEndPoints(proxy)
127+
128+
let vP1 = value.wrappedValue - range.lowerBound
129+
let vP2 = range.upperBound - range.lowerBound
130+
131+
let value = vP1 / vP2
132+
133+
let xP1 = (1 - value) * Double(ends.start.x)
134+
let xP2 = value * Double(ends.end.x)
135+
let xP3 = Double(proxy.size.width / 2)
136+
137+
let width = xP1 + xP2 - xP3
138+
139+
return CGSize(width: width, height: 0)
140+
}
141+
142+
}
143+
144+
// MARK: - RadixSlider Gesture
145+
extension RadixSlider {
146+
147+
private func makeGesture(_ proxy: GeometryProxy) -> some Gesture {
148+
149+
DragGesture(minimumDistance: 10, coordinateSpace: .named(space))
150+
.onChanged { drag in
151+
configuration.onEditingChanged(true)
152+
let ends = calculateEndPoints(proxy)
153+
let parameter = Double(calculateParameter(ends.start, ends.end, drag.location))
154+
let rawValue = (range.upperBound - range.lowerBound) * parameter + range.lowerBound
155+
let steppedValue = (rawValue / step).rounded() * step
156+
self.value.wrappedValue = min(max(steppedValue, range.lowerBound), range.upperBound)
157+
isActive = true
158+
}
159+
.onEnded { drag in
160+
let ends = calculateEndPoints(proxy)
161+
let parameter = Double(calculateParameter(
162+
ends.start,
163+
ends.end,
164+
drag.location
165+
))
166+
let rawValue = (range.upperBound - range.lowerBound) * parameter + range.lowerBound
167+
let steppedValue = (rawValue / step).rounded() * step
168+
self.value.wrappedValue = min(
169+
max(
170+
steppedValue,
171+
range.lowerBound
172+
),
173+
range.upperBound
174+
)
175+
isActive = false
176+
configuration.onEditingChanged(false)
177+
}
178+
179+
}
180+
181+
}

0 commit comments

Comments
 (0)