Skip to content

Commit c26cc76

Browse files
SahilSainiYMLMark Pospesel
andauthored
[CM-1211] Add Accessibility (#3)
* [UPDATE] Basic stepper with Accessibility [UPDATE] change value on min max value update [UPDATE]rebase conflict [UPDATE] added test case for image testing [UPDATE] resolve review comments [UPDATE] file renamed [UPDATE] image change and test cases update [UPDATE] updated size according to string size updated StepperControl+Appearance.swift comments * [UPDATE] typo corrected * [UPDATE] update the width according to scale, provided minWidth * [UPDATE] Review comments resolve * [UPDATE] added support for dark mode * Minor whitespace change * Adjust sizing * [UPDATE] edit accessibility label for value --------- Co-authored-by: Mark Pospesel <mark.pospesel@ymedialabs.com>
1 parent 418ebb0 commit c26cc76

File tree

8 files changed

+201
-90
lines changed

8 files changed

+201
-90
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ let package = Package(
1818
],
1919
dependencies: [
2020
// For UI layout support, contrast ratio calculations.
21-
.package(url: "https://github.com/yml-org/YCoreUI.git", from: "1.4.0"),
21+
.package(url: "https://github.com/yml-org/YCoreUI.git", from: "1.5.0"),
2222
// For Typography support
2323
.package(url: "https://github.com/yml-org/YMatterType.git", from: "1.4.0")
2424
],
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
Localizable.strings
3+
YStepper
4+
5+
Created by Sahil on 9/03/23.
6+
Copyright © 2023 Y Media Labs. All rights reserved.
7+
*/
8+
9+
"Increment_Button_A11y_label" = "Increment";
10+
"Decrement_Button_A11y_label" = "Decrement";
11+
"Delete_Button_A11y_label" = "Delete";
12+
"Value_A11y_label" = "Current value";

Sources/YStepper/Enums/YStepper+Images.swift

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// String+Size.swift
3+
// YStepper
4+
//
5+
// Created by Sahil Saini on 14/03/23.
6+
// Copyright © 2023 Y Media Labs. All rights reserved.
7+
//
8+
9+
import UIKit
10+
import YCoreUI
11+
12+
extension String {
13+
func size(withFont font: UIFont) -> CGSize {
14+
let fontAttributes = [NSAttributedString.Key.font: font]
15+
let size = size(withAttributes: fontAttributes)
16+
return CGSize(width: size.width.ceiled(), height: size.height.ceiled())
17+
}
18+
}

Sources/YStepper/SwiftUI/Views/Stepper.swift

Lines changed: 101 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,14 @@ import YMatterType
1111

1212
/// A SwiftUI stepper control.
1313
public struct Stepper {
14-
enum ButtonType {
15-
case increment
16-
case decrement
17-
}
18-
19-
let minimumSize: CGSize = CGSize(width: 44, height: 44)
20-
14+
@Environment(\.sizeCategory) var sizeCategory
15+
let buttonSize: CGSize = CGSize(width: 44, height: 44)
2116
@ObservedObject private var appearanceObserver = Stepper.AppearanceObserver()
2217
@ObservedObject private var valueObserver = Stepper.ValueObserver()
2318

2419
/// Receive value change notification
2520
public weak var delegate: StepperDelegate?
21+
2622
/// Stepper appearance
2723
public var appearance: StepperControl.Appearance {
2824
get {
@@ -32,21 +28,24 @@ public struct Stepper {
3228
self.appearanceObserver.appearance = newValue
3329
}
3430
}
35-
/// Optional minimum vale. Minimum possible value for the stepper.
31+
32+
/// Minimum value. Minimum possible value for the stepper.
3633
public var minimumValue: Double {
3734
get { valueObserver.minimumValue }
3835
set {
3936
onMinimumValueChange(newValue: newValue)
4037
}
4138
}
42-
/// Optional maximum value. Maximum possible value for the stepper.
39+
40+
/// Maximum value. Maximum possible value for the stepper.
4341
public var maximumValue: Double {
4442
get { valueObserver.maximumValue }
4543
set {
4644
onMaximumValueChange(newValue: newValue)
4745
}
4846
}
49-
/// Optional step value. The step, or increment, value for the stepper.
47+
48+
/// Step value. The step, or increment, value for the stepper.
5049
public var stepValue: Double {
5150
get { valueObserver.stepValue }
5251
set { valueObserver.stepValue = newValue }
@@ -57,18 +56,20 @@ public struct Stepper {
5756
get { valueObserver.value }
5857
set { onValueChange(newValue: newValue) }
5958
}
59+
6060
/// Decimal digits in current value
6161
public var decimalPlaces: Int {
6262
get { valueObserver.decimalValue }
6363
set { valueObserver.decimalValue = newValue }
6464
}
65-
/// Initializes Stepper
65+
66+
/// Initializes a stepper view.
6667
/// - Parameters:
6768
/// - appearance: appearance for the stepper. Default is `.default`
6869
/// - minimumValue: minimum value. Default is `0`
6970
/// - maximumValue: maximum value. Default is `100`
7071
/// - stepValue: Step value. Default is `1`
71-
/// - value: Current value. Default is `0` or minimumValue (if provided)
72+
/// - value: Current value. Default is `0` or minimumValue
7273
public init(
7374
appearance: StepperControl.Appearance = .default,
7475
minimumValue: Double = 0,
@@ -81,56 +82,82 @@ public struct Stepper {
8182
self.maximumValue = maximumValue
8283
self.stepValue = stepValue
8384
self.value = (minimumValue...maximumValue).contains(value) ?
84-
minimumValue : value
85+
value : minimumValue
8586
}
8687
}
8788

8889
extension Stepper: View {
8990
/// :nodoc:
9091
public var body: some View {
9192
HStack(spacing: 0) {
92-
generateButton(buttonType: .decrement) {
93-
valueObserver.value -= stepValue
94-
updateCurrentValue(newValue: valueObserver.value)
95-
}
96-
97-
TextStyleLabel(getValueText(), typography: appearance.textStyle.typography) { label in
98-
label.textAlignment = .center
99-
}.frame(minWidth: minimumSize.width, idealWidth: minimumSize.height)
100-
101-
generateButton(buttonType: .increment) {
102-
valueObserver.value += stepValue
103-
updateCurrentValue(newValue: valueObserver.value)
104-
}
93+
getDecrementButton()
94+
getTextView()
95+
getIncrementButton()
10596
}
97+
.frame(width: (2 * buttonSize.width) + getStringSize(sizeCategory).width)
10698
.background(
10799
Capsule()
108100
.strokeBorder(Color(appearance.borderColor), lineWidth: appearance.borderWidth)
109101
.background(Capsule().foregroundColor(Color(appearance.backgroundColor)))
110102
)
111103
}
112104

113-
func generateButton(
114-
buttonType: ButtonType,
115-
action: @escaping () -> Void
116-
) -> some View {
117-
let button = Button(action: action) {
118-
switch buttonType {
119-
case .increment:
120-
getIncrementImage()
121-
case .decrement:
122-
getImageForDecrementButton()
123-
}
105+
@ViewBuilder
106+
func getIncrementButton() -> some View {
107+
Button { buttonAction(buttonType: .increment) } label: {
108+
getIncrementImage().renderingMode(.template).foregroundColor(Color(appearance.textStyle.textColor))
109+
}
110+
.frame(width: buttonSize.width, height: buttonSize.height)
111+
.accessibilityLabel(StepperControl.Strings.incrementA11yButton.localized)
112+
}
113+
114+
@ViewBuilder
115+
func getDecrementButton() -> some View {
116+
Button { buttonAction(buttonType: .decrement) } label: {
117+
getImageForDecrementButton()?.renderingMode(.template).foregroundColor(
118+
Color(appearance.textStyle.textColor)
119+
)
120+
}
121+
.frame(width: buttonSize.width, height: buttonSize.height)
122+
.accessibilityLabel(getAccessibilityText())
123+
}
124+
125+
func getTextView() -> some View {
126+
TextStyleLabel(
127+
getValueText(),
128+
typography: appearance.textStyle.typography
129+
) { label in
130+
label.textAlignment = .center
131+
label.numberOfLines = 1
124132
}
125-
return button.frame(minWidth: minimumSize.width, minHeight: minimumSize.height)
133+
.frame(width: getStringSize(sizeCategory).width)
134+
.accessibilityLabel(getAccessibilityLabelText())
126135
}
127136
}
128137

129138
extension Stepper {
139+
enum ButtonType {
140+
case increment
141+
case decrement
142+
}
143+
130144
func getValueText() -> String {
145+
formatText(for: value)
146+
}
147+
148+
func formatText(for value: Double) -> String {
131149
String(format: "%.\(decimalPlaces)f", value)
132150
}
133151

152+
func getAccessibilityText() -> String {
153+
if appearance.hasDeleteButton
154+
&& value <= stepValue
155+
&& minimumValue == 0 {
156+
return StepperControl.Strings.deleteA11yButton.localized
157+
}
158+
return StepperControl.Strings.decrementA11yButton.localized
159+
}
160+
134161
func updateCurrentValue(newValue: Double) {
135162
if newValue < valueObserver.minimumValue {
136163
valueObserver.value = valueObserver.minimumValue
@@ -141,22 +168,52 @@ extension Stepper {
141168
}
142169
delegate?.valueDidChange(newValue: valueObserver.value)
143170
}
171+
172+
func buttonAction(buttonType: ButtonType) {
173+
switch buttonType {
174+
case .increment:
175+
valueObserver.value += stepValue
176+
case .decrement:
177+
valueObserver.value -= stepValue
178+
}
179+
updateCurrentValue(newValue: valueObserver.value)
180+
}
181+
182+
func getStringSize(_ size: ContentSizeCategory) -> CGSize {
183+
let traits = UITraitCollection(preferredContentSizeCategory: UIContentSizeCategory(size))
184+
let layout = appearance.textStyle.typography.generateLayout(compatibleWith: traits)
185+
let valueSize = getValueText().size(withFont: layout.font)
186+
let maxSize = formatText(for: maximumValue).size(withFont: layout.font)
187+
return CGSize(
188+
width: max(valueSize.width, maxSize.width),
189+
height: max(valueSize.height, layout.lineHeight)
190+
)
191+
}
192+
193+
func getAccessibilityLabelText() -> String {
194+
StepperControl.Strings.valueA11yLabel.localized + getValueText()
195+
}
144196
}
145197

146198
extension Stepper {
147-
func getDeleteImage() -> Image {
148-
Image(uiImage: appearance.deleteImage ?? StepperControl.Appearance.defaultDeleteImage)
199+
func getDeleteImage() -> Image? {
200+
if let image = appearance.deleteImage {
201+
return Image(uiImage: image)
202+
}
203+
return nil
149204
}
150205

206+
@ViewBuilder
151207
func getIncrementImage() -> Image {
152-
Image(uiImage: appearance.incrementImage ?? StepperControl.Appearance.defaultIncrementImage)
208+
Image(uiImage: appearance.incrementImage)
153209
}
154210

211+
@ViewBuilder
155212
func getDecrementImage() -> Image {
156-
Image(uiImage: appearance.decrementImage ?? StepperControl.Appearance.defaultDecrementImage)
213+
Image(uiImage: appearance.decrementImage)
157214
}
158215

159-
func getImageForDecrementButton() -> Image {
216+
func getImageForDecrementButton() -> Image? {
160217
if appearance.hasDeleteButton
161218
&& value <= stepValue
162219
&& minimumValue == 0 {
@@ -191,10 +248,6 @@ private extension Stepper {
191248

192249
struct Stepper_Previews: PreviewProvider {
193250
static var previews: some View {
194-
HStack {
195-
Spacer().frame(maxWidth: .infinity)
196-
Stepper()
197-
Spacer().frame(maxWidth: .infinity)
198-
}
251+
Stepper()
199252
}
200253
}

Sources/YStepper/UIKit/StepperControl+Appearance.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ extension StepperControl {
2020
public var borderColor: UIColor
2121
/// Border width for stepper view
2222
public var borderWidth: CGFloat
23-
/// Delete button image
23+
/// Delete button image. Nil means no delete button
2424
public var deleteImage: UIImage?
2525
/// Increment button image
26-
public var incrementImage: UIImage?
26+
public var incrementImage: UIImage
2727
/// Decrement button image
28-
public var decrementImage: UIImage?
28+
public var decrementImage: UIImage
2929
/// Whether to show delete button or not.
3030
var hasDeleteButton: Bool { deleteImage != nil }
3131

@@ -47,8 +47,8 @@ extension StepperControl {
4747
borderColor: UIColor = .label,
4848
borderWidth: CGFloat = 1.0,
4949
deleteImage: UIImage? = Appearance.defaultDeleteImage,
50-
incrementImage: UIImage? = Appearance.defaultIncrementImage,
51-
decrementImage: UIImage? = Appearance.defaultDecrementImage
50+
incrementImage: UIImage = Appearance.defaultIncrementImage,
51+
decrementImage: UIImage = Appearance.defaultDecrementImage
5252
) {
5353
self.textStyle = textStyle
5454
self.backgroundColor = backgroundColor
@@ -64,10 +64,10 @@ extension StepperControl {
6464
extension StepperControl.Appearance {
6565
/// Default stepper appearance
6666
public static let `default` = StepperControl.Appearance()
67-
/// Default image for delete button. Is a `trash.circle` from SF Symbols in template rendering mode
68-
public static let defaultDeleteImage = Images.delete.image
69-
/// Default image for increment button. Is a `plus.circle` from SF Symbols in template rendering mode
70-
public static let defaultIncrementImage = Images.increment.image
71-
/// Default image for decrement button. Is a `minus.circle` from SF Symbols in template rendering mode
72-
public static let defaultDecrementImage = Images.decrement.image
67+
/// Default image for delete button. Is a `trash` from SF Symbols in template rendering mode
68+
public static let defaultDeleteImage = StepperControl.Images.delete.image
69+
/// Default image for increment button. Is a `plus` from SF Symbols in template rendering mode
70+
public static let defaultIncrementImage = StepperControl.Images.increment.image
71+
/// Default image for decrement button. Is a `minus` from SF Symbols in template rendering mode
72+
public static let defaultDecrementImage = StepperControl.Images.decrement.image
7373
}

Sources/YStepper/UIKit/StepperControl.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,25 @@ public class StepperControl: UIControl {
8181
}
8282
}
8383

84+
extension StepperControl {
85+
/// Collection of Images
86+
enum Images: String, CaseIterable, SystemImage {
87+
case increment = "plus"
88+
case decrement = "minus"
89+
case delete = "trash"
90+
}
91+
92+
/// Collection of Strings
93+
enum Strings: String, Localizable, CaseIterable {
94+
case incrementA11yButton = "Increment_Button_A11y_label"
95+
case decrementA11yButton = "Decrement_Button_A11y_label"
96+
case deleteA11yButton = "Delete_Button_A11y_label"
97+
case valueA11yLabel = "Value_A11y_label"
98+
99+
static var bundle: Bundle { .module }
100+
}
101+
}
102+
84103
extension StepperControl: StepperDelegate {
85104
/// This method is used to inform when there is a change in value.
86105
/// - Parameter value: new value

0 commit comments

Comments
 (0)