Skip to content

[CM-1211] Add Accessibility #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ let package = Package(
],
dependencies: [
// For UI layout support, contrast ratio calculations.
.package(url: "https://github.com/yml-org/YCoreUI.git", from: "1.4.0"),
.package(url: "https://github.com/yml-org/YCoreUI.git", from: "1.5.0"),
// For Typography support
.package(url: "https://github.com/yml-org/YMatterType.git", from: "1.4.0")
],
Expand Down
12 changes: 12 additions & 0 deletions Sources/YStepper/Assets/Strings/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
Localizable.strings
YStepper

Created by Sahil on 9/03/23.
Copyright © 2023 Y Media Labs. All rights reserved.
*/

"Increment_Button_A11y_label" = "Increment";
"Decrement_Button_A11y_label" = "Decrement";
"Delete_Button_A11y_label" = "Delete";
"Value_A11y_label" = "Current value";
23 changes: 0 additions & 23 deletions Sources/YStepper/Enums/YStepper+Images.swift

This file was deleted.

18 changes: 18 additions & 0 deletions Sources/YStepper/Extension/String+Size.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// String+Size.swift
// YStepper
//
// Created by Sahil Saini on 14/03/23.
// Copyright © 2023 Y Media Labs. All rights reserved.
//

import UIKit
import YCoreUI

extension String {
func size(withFont font: UIFont) -> CGSize {
let fontAttributes = [NSAttributedString.Key.font: font]
let size = size(withAttributes: fontAttributes)
return CGSize(width: size.width.ceiled(), height: size.height.ceiled())
}
}
149 changes: 101 additions & 48 deletions Sources/YStepper/SwiftUI/Views/Stepper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,14 @@ import YMatterType

/// A SwiftUI stepper control.
public struct Stepper {
enum ButtonType {
case increment
case decrement
}

let minimumSize: CGSize = CGSize(width: 44, height: 44)

@Environment(\.sizeCategory) var sizeCategory
let buttonSize: CGSize = CGSize(width: 44, height: 44)
@ObservedObject private var appearanceObserver = Stepper.AppearanceObserver()
@ObservedObject private var valueObserver = Stepper.ValueObserver()

/// Receive value change notification
public weak var delegate: StepperDelegate?

/// Stepper appearance
public var appearance: StepperControl.Appearance {
get {
Expand All @@ -32,21 +28,24 @@ public struct Stepper {
self.appearanceObserver.appearance = newValue
}
}
/// Optional minimum vale. Minimum possible value for the stepper.

/// Minimum value. Minimum possible value for the stepper.
public var minimumValue: Double {
get { valueObserver.minimumValue }
set {
onMinimumValueChange(newValue: newValue)
}
}
/// Optional maximum value. Maximum possible value for the stepper.

/// Maximum value. Maximum possible value for the stepper.
public var maximumValue: Double {
get { valueObserver.maximumValue }
set {
onMaximumValueChange(newValue: newValue)
}
}
/// Optional step value. The step, or increment, value for the stepper.

/// Step value. The step, or increment, value for the stepper.
public var stepValue: Double {
get { valueObserver.stepValue }
set { valueObserver.stepValue = newValue }
Expand All @@ -57,18 +56,20 @@ public struct Stepper {
get { valueObserver.value }
set { onValueChange(newValue: newValue) }
}

/// Decimal digits in current value
public var decimalPlaces: Int {
get { valueObserver.decimalValue }
set { valueObserver.decimalValue = newValue }
}
/// Initializes Stepper

/// Initializes a stepper view.
/// - Parameters:
/// - appearance: appearance for the stepper. Default is `.default`
/// - minimumValue: minimum value. Default is `0`
/// - maximumValue: maximum value. Default is `100`
/// - stepValue: Step value. Default is `1`
/// - value: Current value. Default is `0` or minimumValue (if provided)
/// - value: Current value. Default is `0` or minimumValue
public init(
appearance: StepperControl.Appearance = .default,
minimumValue: Double = 0,
Expand All @@ -81,56 +82,82 @@ public struct Stepper {
self.maximumValue = maximumValue
self.stepValue = stepValue
self.value = (minimumValue...maximumValue).contains(value) ?
minimumValue : value
value : minimumValue
}
}

extension Stepper: View {
/// :nodoc:
public var body: some View {
HStack(spacing: 0) {
generateButton(buttonType: .decrement) {
valueObserver.value -= stepValue
updateCurrentValue(newValue: valueObserver.value)
}

TextStyleLabel(getValueText(), typography: appearance.textStyle.typography) { label in
label.textAlignment = .center
}.frame(minWidth: minimumSize.width, idealWidth: minimumSize.height)

generateButton(buttonType: .increment) {
valueObserver.value += stepValue
updateCurrentValue(newValue: valueObserver.value)
}
getDecrementButton()
getTextView()
getIncrementButton()
}
.frame(width: (2 * buttonSize.width) + getStringSize(sizeCategory).width)
.background(
Capsule()
.strokeBorder(Color(appearance.borderColor), lineWidth: appearance.borderWidth)
.background(Capsule().foregroundColor(Color(appearance.backgroundColor)))
)
}

func generateButton(
buttonType: ButtonType,
action: @escaping () -> Void
) -> some View {
let button = Button(action: action) {
switch buttonType {
case .increment:
getIncrementImage()
case .decrement:
getImageForDecrementButton()
}
@ViewBuilder
func getIncrementButton() -> some View {
Button { buttonAction(buttonType: .increment) } label: {
getIncrementImage().renderingMode(.template).foregroundColor(Color(appearance.textStyle.textColor))
}
.frame(width: buttonSize.width, height: buttonSize.height)
.accessibilityLabel(StepperControl.Strings.incrementA11yButton.localized)
}

@ViewBuilder
func getDecrementButton() -> some View {
Button { buttonAction(buttonType: .decrement) } label: {
getImageForDecrementButton()?.renderingMode(.template).foregroundColor(
Color(appearance.textStyle.textColor)
)
}
.frame(width: buttonSize.width, height: buttonSize.height)
.accessibilityLabel(getAccessibilityText())
}

func getTextView() -> some View {
TextStyleLabel(
getValueText(),
typography: appearance.textStyle.typography
) { label in
label.textAlignment = .center
label.numberOfLines = 1
}
return button.frame(minWidth: minimumSize.width, minHeight: minimumSize.height)
.frame(width: getStringSize(sizeCategory).width)
.accessibilityLabel(getAccessibilityLabelText())
}
}

extension Stepper {
enum ButtonType {
case increment
case decrement
}

func getValueText() -> String {
formatText(for: value)
}

func formatText(for value: Double) -> String {
String(format: "%.\(decimalPlaces)f", value)
}

func getAccessibilityText() -> String {
if appearance.hasDeleteButton
&& value <= stepValue
&& minimumValue == 0 {
return StepperControl.Strings.deleteA11yButton.localized
}
return StepperControl.Strings.decrementA11yButton.localized
}

func updateCurrentValue(newValue: Double) {
if newValue < valueObserver.minimumValue {
valueObserver.value = valueObserver.minimumValue
Expand All @@ -141,22 +168,52 @@ extension Stepper {
}
delegate?.valueDidChange(newValue: valueObserver.value)
}

func buttonAction(buttonType: ButtonType) {
switch buttonType {
case .increment:
valueObserver.value += stepValue
case .decrement:
valueObserver.value -= stepValue
}
updateCurrentValue(newValue: valueObserver.value)
}

func getStringSize(_ size: ContentSizeCategory) -> CGSize {
let traits = UITraitCollection(preferredContentSizeCategory: UIContentSizeCategory(size))
let layout = appearance.textStyle.typography.generateLayout(compatibleWith: traits)
let valueSize = getValueText().size(withFont: layout.font)
let maxSize = formatText(for: maximumValue).size(withFont: layout.font)
return CGSize(
width: max(valueSize.width, maxSize.width),
height: max(valueSize.height, layout.lineHeight)
)
}

func getAccessibilityLabelText() -> String {
StepperControl.Strings.valueA11yLabel.localized + getValueText()
}
}

extension Stepper {
func getDeleteImage() -> Image {
Image(uiImage: appearance.deleteImage ?? StepperControl.Appearance.defaultDeleteImage)
func getDeleteImage() -> Image? {
if let image = appearance.deleteImage {
return Image(uiImage: image)
}
return nil
}

@ViewBuilder
func getIncrementImage() -> Image {
Image(uiImage: appearance.incrementImage ?? StepperControl.Appearance.defaultIncrementImage)
Image(uiImage: appearance.incrementImage)
}

@ViewBuilder
func getDecrementImage() -> Image {
Image(uiImage: appearance.decrementImage ?? StepperControl.Appearance.defaultDecrementImage)
Image(uiImage: appearance.decrementImage)
}

func getImageForDecrementButton() -> Image {
func getImageForDecrementButton() -> Image? {
if appearance.hasDeleteButton
&& value <= stepValue
&& minimumValue == 0 {
Expand Down Expand Up @@ -191,10 +248,6 @@ private extension Stepper {

struct Stepper_Previews: PreviewProvider {
static var previews: some View {
HStack {
Spacer().frame(maxWidth: .infinity)
Stepper()
Spacer().frame(maxWidth: .infinity)
}
Stepper()
}
}
22 changes: 11 additions & 11 deletions Sources/YStepper/UIKit/StepperControl+Appearance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ extension StepperControl {
public var borderColor: UIColor
/// Border width for stepper view
public var borderWidth: CGFloat
/// Delete button image
/// Delete button image. Nil means no delete button
public var deleteImage: UIImage?
/// Increment button image
public var incrementImage: UIImage?
public var incrementImage: UIImage
/// Decrement button image
public var decrementImage: UIImage?
public var decrementImage: UIImage
/// Whether to show delete button or not.
var hasDeleteButton: Bool { deleteImage != nil }

Expand All @@ -47,8 +47,8 @@ extension StepperControl {
borderColor: UIColor = .label,
borderWidth: CGFloat = 1.0,
deleteImage: UIImage? = Appearance.defaultDeleteImage,
incrementImage: UIImage? = Appearance.defaultIncrementImage,
decrementImage: UIImage? = Appearance.defaultDecrementImage
incrementImage: UIImage = Appearance.defaultIncrementImage,
decrementImage: UIImage = Appearance.defaultDecrementImage
) {
self.textStyle = textStyle
self.backgroundColor = backgroundColor
Expand All @@ -64,10 +64,10 @@ extension StepperControl {
extension StepperControl.Appearance {
/// Default stepper appearance
public static let `default` = StepperControl.Appearance()
/// Default image for delete button. Is a `trash.circle` from SF Symbols in template rendering mode
public static let defaultDeleteImage = Images.delete.image
/// Default image for increment button. Is a `plus.circle` from SF Symbols in template rendering mode
public static let defaultIncrementImage = Images.increment.image
/// Default image for decrement button. Is a `minus.circle` from SF Symbols in template rendering mode
public static let defaultDecrementImage = Images.decrement.image
/// Default image for delete button. Is a `trash` from SF Symbols in template rendering mode
public static let defaultDeleteImage = StepperControl.Images.delete.image
/// Default image for increment button. Is a `plus` from SF Symbols in template rendering mode
public static let defaultIncrementImage = StepperControl.Images.increment.image
/// Default image for decrement button. Is a `minus` from SF Symbols in template rendering mode
public static let defaultDecrementImage = StepperControl.Images.decrement.image
}
19 changes: 19 additions & 0 deletions Sources/YStepper/UIKit/StepperControl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,25 @@ public class StepperControl: UIControl {
}
}

extension StepperControl {
/// Collection of Images
enum Images: String, CaseIterable, SystemImage {
case increment = "plus"
case decrement = "minus"
case delete = "trash"
}

/// Collection of Strings
enum Strings: String, Localizable, CaseIterable {
case incrementA11yButton = "Increment_Button_A11y_label"
case decrementA11yButton = "Decrement_Button_A11y_label"
case deleteA11yButton = "Delete_Button_A11y_label"
case valueA11yLabel = "Value_A11y_label"

static var bundle: Bundle { .module }
}
}

extension StepperControl: StepperDelegate {
/// This method is used to inform when there is a change in value.
/// - Parameter value: new value
Expand Down
Loading