Skip to content

Commit 57f7bff

Browse files
authored
feat: implement Stepper (#1220)
1 parent c306403 commit 57f7bff

12 files changed

Lines changed: 2163 additions & 3 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//
2+
// StepperExample.swift
3+
// Shared
4+
5+
#if OPENSWIFTUI
6+
import OpenSwiftUI
7+
#else
8+
import SwiftUI
9+
#endif
10+
11+
struct StepperExample: View {
12+
@State private var value: Int
13+
@State private var isEditing = false
14+
15+
init(value: Int = 4) {
16+
_value = State(initialValue: value)
17+
}
18+
19+
var body: some View {
20+
VStack(spacing: 16) {
21+
Stepper(
22+
"Count: \(value)",
23+
value: $value,
24+
in: 0...10,
25+
step: 2,
26+
onEditingChanged: { isEditing = $0 }
27+
)
28+
Text(isEditing ? "Editing" : "Ready")
29+
.foregroundColor(isEditing ? .red : .blue)
30+
}
31+
.padding()
32+
}
33+
}
34+
35+
struct ActionStepperExample: View {
36+
@State private var value = 0
37+
38+
var canIncrement = true
39+
var canDecrement = true
40+
41+
var body: some View {
42+
Stepper(
43+
label: { Text("Count: \(value)") },
44+
onIncrement: canIncrement ? { value += 1 } : nil,
45+
onDecrement: canDecrement ? { value -= 1 } : nil
46+
)
47+
.padding()
48+
}
49+
}
50+
51+
struct FractionalStepperExample: View {
52+
@State private var value = 1.25
53+
54+
var body: some View {
55+
Stepper(value: $value, step: 0.25) {
56+
Text("Value: \(value, specifier: "%.2f")")
57+
}
58+
.padding()
59+
}
60+
}
61+
62+
#Preview("Bounded") {
63+
StepperExample()
64+
}
65+
66+
#Preview("Custom actions") {
67+
ActionStepperExample()
68+
}
69+
70+
#Preview("Fractional steps") {
71+
FractionalStepperExample()
72+
}
73+
74+
#Preview("Hidden labels and disabled") {
75+
VStack {
76+
StepperExample().labelsHidden()
77+
StepperExample().disabled(true)
78+
}
79+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//
2+
// StepperUITests.swift
3+
// OpenSwiftUIUITests
4+
5+
import SnapshotTesting
6+
import Testing
7+
@testable import TestingHost
8+
9+
@MainActor
10+
@Suite(.snapshots(record: .never, diffTool: diffTool))
11+
struct StepperUITests {
12+
@Test(arguments: [0, 4, 10])
13+
func boundedValues(value: Int) {
14+
openSwiftUIAssertSnapshot(
15+
of: StepperExample(value: value),
16+
size: .init(width: 320, height: 120),
17+
named: "\(value)"
18+
)
19+
}
20+
21+
@Test(arguments: [
22+
(true, true),
23+
(true, false),
24+
(false, true),
25+
(false, false),
26+
] as [(Bool, Bool)])
27+
func optionalActions(canIncrement: Bool, canDecrement: Bool) {
28+
openSwiftUIAssertSnapshot(
29+
of: ActionStepperExample(
30+
canIncrement: canIncrement,
31+
canDecrement: canDecrement
32+
),
33+
size: .init(width: 320, height: 100),
34+
named: "increment-\(canIncrement)-decrement-\(canDecrement)"
35+
)
36+
}
37+
38+
@Test
39+
func fractionalSteps() {
40+
openSwiftUIAssertSnapshot(
41+
of: FractionalStepperExample(),
42+
size: .init(width: 320, height: 100)
43+
)
44+
}
45+
46+
@Test
47+
func labelsHidden() {
48+
openSwiftUIAssertSnapshot(
49+
of: StepperExample().labelsHidden(),
50+
size: .init(width: 320, height: 120)
51+
)
52+
}
53+
54+
@Test
55+
func disabled() {
56+
openSwiftUIAssertSnapshot(
57+
of: StepperExample().disabled(true),
58+
size: .init(width: 320, height: 120)
59+
)
60+
}
61+
62+
@Test(arguments: [.mini, .small, .regular, .large, .extraLarge] as [ControlSize])
63+
func controlSizes(size: ControlSize) {
64+
openSwiftUIAssertSnapshot(
65+
of: StepperExample().controlSize(size),
66+
size: .init(width: 320, height: 120),
67+
named: "\(size)"
68+
)
69+
}
70+
}

‎Sources/OpenSwiftUI/Accessibility/AccessibilityConfiguration.swift‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,39 @@ struct AccessibilityLabelModifier<Content: View>: AccessibilityConfigurationModi
149149
}
150150
}
151151

152-
// TODO: AccessibilityStepperModifier
152+
// MARK: - AccessibilityStepperModifier
153+
154+
struct AccessibilityStepperModifier<Content>: AccessibilityConfigurationModifier where Content: View {
155+
var base: AccessibilityAdjustableModifier<Content, AccessibilityStepperValue>
156+
157+
init(_ configuration: StepperStyleConfiguration, value: AccessibilityBoundedNumber?) {
158+
base = AccessibilityAdjustableModifier(configuration: .init(
159+
value: value.map { AccessibilityStepperValue($0) },
160+
continuous: false,
161+
onIncrement: configuration.onIncrement,
162+
onDecrement: configuration.onDecrement,
163+
onSet: { number in
164+
guard let value else {
165+
return
166+
}
167+
let delta = number.value.doubleValue - value.value.doubleValue
168+
let count = Int(abs((delta / (value.step?.doubleValue ?? 1)).rounded()))
169+
for _ in 0 ..< count {
170+
if delta > 0 {
171+
configuration.onIncrement?()
172+
} else {
173+
configuration.onDecrement?()
174+
}
175+
}
176+
},
177+
onEditingChanged: configuration.onEditingChanged
178+
))
179+
}
180+
181+
func body(content: Content) -> some View {
182+
base.body(content: content)
183+
}
184+
}
153185

154186
// MARK: - AccessibilityAdjustableModifier
155187

‎Sources/OpenSwiftUI/Event/Responder/AllowsWindowActivationEventsResponder.swift‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,22 @@
66
// Status: WIP
77
// ID: 302179F1EB9AE99B83C6A183C0B4143E (?)
88

9-
#if os(macOS)
109
@_spi(ForOpenSwiftUIOnly)
11-
import OpenSwiftUICore
10+
public import OpenSwiftUICore
11+
12+
// MARK: - View + allowsWindowActivationEvents [TBA]
13+
14+
@available(OpenSwiftUI_v6_0, *)
15+
@available(tvOS, unavailable)
16+
@available(watchOS, unavailable)
17+
extension View {
18+
nonisolated public func allowsWindowActivationEvents(_ value: Bool? = true) -> some View {
19+
_openSwiftUIUnimplementedWarning()
20+
return self
21+
}
22+
}
1223

24+
#if os(macOS)
1325
// TODO: AllowsWindowActivationEventsResponder
1426
class AllowsWindowActivationEventsResponder: DefaultLayoutViewResponder {
1527
var value: Bool?

0 commit comments

Comments
 (0)