Skip to content

Commit 676830e

Browse files
committed
Add a new expandOnFocus slider option
1 parent fab455d commit 676830e

14 files changed

+274
-82
lines changed

Demo/Sources/Demo/SystemDemo.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,29 @@ struct CompactSliderSystemDemo: View {
1111

1212
var body: some View {
1313
VStack(spacing: 20) {
14-
Text("System Slider")
14+
Text("System slider")
1515
Slider(value: $progress) {
1616
Text("Progress")
1717
}
1818

19-
Text("\"System\" Sliders")
19+
Text("\"System\" sliders")
2020
SystemSlider(value: $progress)
2121
SystemSlider(value: $progress, step: 0.1)
22-
22+
23+
Text("\"System\" expandable slider")
24+
25+
SystemSlider(value: $progress)
26+
.systemSliderStyle(handleStyle: .hidden())
27+
.compactSliderOptionsByAdding(.expandOnFocus(minScale: 0.4))
28+
.compactSliderAnimation(.bouncy, when: .dragging)
29+
.compactSliderAnimation(.bouncy, when: .hovering)
30+
31+
Text("\"System\" scrollable slider")
32+
2333
SystemSlider(value: $progress, step: 0.1)
2434
.systemSliderStyle(.scrollableHorizontal)
2535

26-
Text("Vertical \"System\" Sliders")
36+
Text("Vertical \"System\" sliders")
2737

2838
HStack(spacing: 20) {
2939
SystemSlider(value: $progress)

Sources/CompactSlider/CompactSlider.swift

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ public struct CompactSlider<Value: BinaryFloatingPoint, Point: CompactSliderPoin
132132
@Environment(\.compactSliderStyle) var compactSliderStyle
133133
@Environment(\.compactSliderGridStyle) var compactSliderGridStyle
134134
@Environment(\.compactSliderCircularGridStyle) var compactSliderCircularGridStyle
135+
@Environment(\.compactSliderAnimations) var animations
135136

136137
let bounds: ClosedRange<Value>
137138
let pointBounds: ClosedRange<Point>
@@ -190,7 +191,13 @@ public struct CompactSlider<Value: BinaryFloatingPoint, Point: CompactSliderPoin
190191
.dragGesture(
191192
options: options,
192193
onChanged: {
193-
isDragging = true
194+
if let animation = animations[.dragging] {
195+
withAnimation(animation) {
196+
isDragging = true
197+
}
198+
} else {
199+
isDragging = true
200+
}
194201

195202
if startDragLocation == nil {
196203
startDragLocation = nearestProgressLocation(
@@ -221,7 +228,14 @@ public struct CompactSlider<Value: BinaryFloatingPoint, Point: CompactSliderPoin
221228
}
222229

223230
startDragLocation = nil
224-
isDragging = false
231+
232+
if let animation = animations[.dragging] {
233+
withAnimation(animation) {
234+
isDragging = false
235+
}
236+
} else {
237+
isDragging = false
238+
}
225239
}
226240
)
227241
#if os(macOS)
@@ -250,7 +264,15 @@ public struct CompactSlider<Value: BinaryFloatingPoint, Point: CompactSliderPoin
250264
}
251265
.opacity(isEnabled ? 1 : 0.5)
252266
#if os(macOS) || os(iOS)
253-
.onHover { isHovering = isEnabled && $0 }
267+
.onHover { isHovering in
268+
if let animation = animations[.hovering] {
269+
withAnimation(animation) {
270+
self.isHovering = isEnabled && isHovering
271+
}
272+
} else {
273+
self.isHovering = isEnabled && isHovering
274+
}
275+
}
254276
#endif
255277
.onChange(of: progress, perform: onProgressesChange)
256278
.onChange(of: lowerValue, perform: onLowerValueChange)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// The MIT License (MIT)
2+
//
3+
// Copyright (c) 2025 Alexey Bukhtin (github.com/buh).
4+
//
5+
6+
import SwiftUI
7+
8+
/// Compact slider animations events.
9+
public enum CompactSliderAnimationEvent {
10+
case hovering
11+
case dragging
12+
}
13+
14+
// MARK: - Environment
15+
16+
struct CompactSliderAnimationsKey: EnvironmentKey {
17+
static var defaultValue: [CompactSliderAnimationEvent: Animation] = [:]
18+
}
19+
20+
extension EnvironmentValues {
21+
/// The environment value for the compact slider options.
22+
var compactSliderAnimations: [CompactSliderAnimationEvent: Animation] {
23+
get { self[CompactSliderAnimationsKey.self] }
24+
set { self[CompactSliderAnimationsKey.self] = newValue }
25+
}
26+
}
27+
28+
extension View {
29+
/// Sets the slider animations.
30+
public func compactSliderAnimation(_ animation: Animation?, when event: CompactSliderAnimationEvent) -> some View {
31+
modifier(CompactSliderAnimationsModifier(animation: animation, event: event))
32+
}
33+
}
34+
35+
36+
struct CompactSliderAnimationsModifier: ViewModifier {
37+
@Environment(\.compactSliderAnimations) var animations
38+
39+
var animation: Animation?
40+
var event: CompactSliderAnimationEvent
41+
42+
func body(content: Content) -> some View {
43+
content.environment(
44+
\.compactSliderAnimations,
45+
{
46+
var animations = animations
47+
animations[event] = animation
48+
return animations
49+
}()
50+
)
51+
}
52+
}

Sources/CompactSlider/CompactSliderOption.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public enum CompactSliderOption: Hashable {
2525
case withoutBackground
2626
/// Allows the slider to loop values.
2727
case loopValues
28+
/// Allows the slider to expand the background and progress view when focused.
29+
case expandOnFocus(minScale: CGFloat)
2830
}
2931

3032
/// A set of drag gesture options: minimum drag distance, delayed touch, and high priority.
@@ -50,6 +52,16 @@ extension Set<CompactSliderOption> {
5052

5153
return 1
5254
}
55+
56+
var expandOnFocusMinScale: CGFloat? {
57+
for option in self {
58+
if case .expandOnFocus(let value) = option {
59+
return value
60+
}
61+
}
62+
63+
return 1
64+
}
5365
}
5466

5567
// MARK: - View Extension

Sources/CompactSlider/CompactSliderStyleConfiguration.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,10 @@ extension CompactSliderStyleConfiguration {
288288

289289
/// A handle visibility depending on the slider type, progress values and focus state.
290290
public func isHandleVisible(handleStyle: HandleStyle) -> Bool {
291+
if handleStyle.visibility == .hidden {
292+
return false
293+
}
294+
291295
if progress.isMultipleValues
292296
|| progress.isGridValues
293297
|| progress.isCircularGridValues
@@ -356,6 +360,21 @@ extension CompactSliderStyleConfiguration {
356360
}
357361
}
358362

363+
// MARK: Options
364+
365+
extension CompactSliderStyleConfiguration {
366+
/// A frame size depending on the focus state and the expand on focus min scale.
367+
public func frameMaxValue(_ expandOnFocusMinScale: CGFloat) -> OptionalCGSize? {
368+
guard !focusState.isFocused else { return nil }
369+
370+
if type.isHorizontal {
371+
return OptionalCGSize(height: size.height * expandOnFocusMinScale.clamped(0.01))
372+
}
373+
374+
return OptionalCGSize(width: size.width * expandOnFocusMinScale.clamped(0.01))
375+
}
376+
}
377+
359378
// MARK: - Grid
360379

361380
extension CompactSliderStyleConfiguration {

Sources/CompactSlider/Components/Handle/HandleStyle.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ public struct HandleStyle: Equatable {
6363
// MARK: - Constructors
6464

6565
extension HandleStyle {
66+
/// Creates a handle style, which hides the handle.
67+
public static func hidden() -> HandleStyle {
68+
.init(
69+
type: .rectangle,
70+
visibility: .hidden,
71+
progressAlignment: .center,
72+
color: .accentColor,
73+
width: 0,
74+
cornerRadius: 0,
75+
strokeStyle: nil
76+
)
77+
}
78+
6679
/// Creates a rectangle handle style.
6780
///
6881
/// - Parameters:

Sources/CompactSlider/Styles/BackgroundViewWrapper.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,24 @@ import SwiftUI
88
struct BackgroundViewWrapper: View {
99
@Environment(\.compactSliderOptions) var sliderOptions
1010
@Environment(\.compactSliderBackgroundView) var backgroundView
11+
let configuration: CompactSliderStyleConfiguration
1112
let padding: EdgeInsets
1213

13-
init(padding: EdgeInsets = .zero) {
14+
init(configuration: CompactSliderStyleConfiguration, padding: EdgeInsets = .zero) {
15+
self.configuration = configuration
1416
self.padding = padding
1517
}
1618

1719
var body: some View {
1820
if !sliderOptions.contains(.withoutBackground) {
19-
backgroundView(padding)
21+
if configuration.type.isLinear,
22+
let expandOnFocusMinScale = sliderOptions.expandOnFocusMinScale {
23+
let maxValue = configuration.frameMaxValue(expandOnFocusMinScale)
24+
backgroundView(padding)
25+
.frame(maxWidth: maxValue?.width, maxHeight: maxValue?.height)
26+
} else {
27+
backgroundView(padding)
28+
}
2029
}
2130
}
2231
}

Sources/CompactSlider/Styles/DefaultCompactSliderStyle.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ public struct DefaultCompactSliderStyle: CompactSliderStyle {
4545
}
4646

4747
public func makeBody(configuration: Configuration) -> some View {
48-
ZStack(alignment: .center) {
48+
ZStack {
4949
if !configuration.progress.isMultipleValues,
5050
!configuration.progress.isGridValues,
5151
!configuration.progress.isCircularGridValues,
5252
!configuration.type.isScrollable {
53-
ProgressViewWrapper()
53+
ProgressViewWrapper(configuration: configuration)
5454
.clipShapeStyleIf(
5555
!clipShapeStyle.options.contains(.all) && clipShapeStyle.options.contains(.progress),
5656
shape: clipShapeStyle.shape
@@ -65,9 +65,10 @@ public struct DefaultCompactSliderStyle: CompactSliderStyle {
6565

6666
HandleViewWrapper(configuration: configuration)
6767
}
68+
.frame(maxWidth: .infinity, maxHeight: .infinity)
6869
.padding(padding)
6970
.background(
70-
BackgroundViewWrapper(padding: padding)
71+
BackgroundViewWrapper(configuration: configuration, padding: padding)
7172
.clipShapeStyleIf(
7273
!clipShapeStyle.options.contains(.all) && clipShapeStyle.options.contains(.background),
7374
shape: clipShapeStyle.shape

Sources/CompactSlider/Styles/ProgressViewWrapper.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,18 @@
66
import SwiftUI
77

88
struct ProgressViewWrapper: View {
9+
@Environment(\.compactSliderOptions) var sliderOptions
910
@Environment(\.compactSliderProgressView) var progressView
11+
let configuration: CompactSliderStyleConfiguration
1012

1113
public var body: some View {
12-
progressView
14+
if configuration.type.isLinear,
15+
let expandOnFocusMinScale = sliderOptions.expandOnFocusMinScale {
16+
let maxValue = configuration.frameMaxValue(expandOnFocusMinScale)
17+
progressView
18+
.frame(maxWidth: maxValue?.width, maxHeight: maxValue?.height)
19+
} else {
20+
progressView
21+
}
1322
}
1423
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// The MIT License (MIT)
2+
//
3+
// Copyright (c) 2025 Alexey Bukhtin (github.com/buh).
4+
//
5+
6+
import SwiftUI
7+
8+
struct SystemSliderStyleKey: EnvironmentKey {
9+
static var defaultValue: (slider: DefaultCompactSliderStyle, handle: HandleStyle?) = (
10+
DefaultCompactSliderStyle.horizontal(clipShapeStyle: .none), nil
11+
)
12+
}
13+
14+
extension EnvironmentValues {
15+
var systemSliderStyle: (slider: DefaultCompactSliderStyle, handle: HandleStyle?) {
16+
get { self[SystemSliderStyleKey.self] }
17+
set { self[SystemSliderStyleKey.self] = newValue }
18+
}
19+
}
20+
21+
extension View {
22+
/// Sets a style for the "system" slider. The style supports horizontal and vertical sliders.
23+
/// - Parameters:
24+
/// - type: The type of the "system" slider.
25+
/// - handleStyle: The style of the handle. Default is `nil`.
26+
/// - padding: The padding of the slider. Default is `.zero`.
27+
public func systemSliderStyle(
28+
_ type: SystemSliderType = .horizontal(.leading),
29+
handleStyle: HandleStyle? = nil,
30+
padding: EdgeInsets = .zero
31+
) -> some View {
32+
environment(
33+
\.systemSliderStyle,
34+
(
35+
DefaultCompactSliderStyle(
36+
type: type.compactSliderType,
37+
clipShapeStyle: .capsule(options: [.background, .progress, .scale]),
38+
padding: padding
39+
),
40+
handleStyle
41+
)
42+
)
43+
}
44+
}

0 commit comments

Comments
 (0)