diff --git a/Sources/YStepper/SwiftUI/Views/Stepper.swift b/Sources/YStepper/SwiftUI/Views/Stepper.swift index a2487a4..74be2aa 100644 --- a/Sources/YStepper/SwiftUI/Views/Stepper.swift +++ b/Sources/YStepper/SwiftUI/Views/Stepper.swift @@ -13,6 +13,7 @@ import YMatterType public struct Stepper { @Environment(\.sizeCategory) var sizeCategory let buttonSize: CGSize = CGSize(width: 44, height: 44) + @ScaledMetric var scale = 1.0 @ObservedObject private var appearanceObserver = Stepper.AppearanceObserver() @ObservedObject private var valueObserver = Stepper.ValueObserver() @@ -96,16 +97,15 @@ extension Stepper: View { } .frame(width: (2 * buttonSize.width) + getStringSize(sizeCategory).width) .background( - Capsule() - .strokeBorder(Color(appearance.borderColor), lineWidth: appearance.borderWidth) - .background(Capsule().foregroundColor(Color(appearance.backgroundColor))) + getShape() + .background(getShapeWithoutStroke().foregroundColor(Color(appearance.backgroundColor))) ) } @ViewBuilder func getIncrementButton() -> some View { Button { buttonAction(buttonType: .increment) } label: { - getIncrementImage().renderingMode(.template).foregroundColor(Color(appearance.textStyle.textColor)) + getIncrementImage().renderingMode(.template) } .frame(width: buttonSize.width, height: buttonSize.height) .accessibilityLabel(StepperControl.Strings.incrementA11yButton.localized) @@ -114,9 +114,7 @@ extension Stepper: View { @ViewBuilder func getDecrementButton() -> some View { Button { buttonAction(buttonType: .decrement) } label: { - getImageForDecrementButton()?.renderingMode(.template).foregroundColor( - Color(appearance.textStyle.textColor) - ) + getImageForDecrementButton()?.renderingMode(.template) } .frame(width: buttonSize.width, height: buttonSize.height) .accessibilityLabel(getAccessibilityText()) @@ -129,10 +127,63 @@ extension Stepper: View { ) { label in label.textAlignment = .center label.numberOfLines = 1 + label.textColor = appearance.textStyle.textColor } .frame(width: getStringSize(sizeCategory).width) .accessibilityLabel(getAccessibilityLabelText()) } + + @ViewBuilder + func getShape() -> some View { + switch appearance.shape { + case .none: + EmptyView() + case .rectangle: + Rectangle().strokeBorder(Color(appearance.borderColor), lineWidth: appearance.borderWidth) + case .roundRect(cornerRadius: let cornerRadius): + RoundedRectangle( + cornerSize: CGSize( + width: cornerRadius, + height: cornerRadius + ) + ).strokeBorder(Color(appearance.borderColor), lineWidth: appearance.borderWidth) + case .scaledRoundRect(cornerRadius: let cornerRadius): + RoundedRectangle( + cornerSize: CGSize( + width: cornerRadius * scale, + height: cornerRadius * scale + ) + ).strokeBorder(Color(appearance.borderColor), lineWidth: appearance.borderWidth) + case .capsule: + Capsule().strokeBorder(Color(appearance.borderColor), lineWidth: appearance.borderWidth) + } + } + + @ViewBuilder + func getShapeWithoutStroke() -> some View { + switch appearance.shape { + case .none: + EmptyView() + case .rectangle: + Rectangle() + case .roundRect(cornerRadius: let cornerRadius): + RoundedRectangle( + cornerSize: CGSize( + width: cornerRadius, + height: cornerRadius + ) + ) + case .scaledRoundRect(cornerRadius: let cornerRadius): + RoundedRectangle( + cornerSize: CGSize( + width: cornerRadius * scale, + height: cornerRadius * scale + ) + ) + case .capsule: + Capsule() + } + } } extension Stepper { diff --git a/Sources/YStepper/UIKit/StepperControl+Appearance.swift b/Sources/YStepper/UIKit/StepperControl+Appearance.swift index 036603b..1a23d99 100644 --- a/Sources/YStepper/UIKit/StepperControl+Appearance.swift +++ b/Sources/YStepper/UIKit/StepperControl+Appearance.swift @@ -26,6 +26,8 @@ extension StepperControl { public var incrementImage: UIImage /// Decrement button image public var decrementImage: UIImage + /// Stepper's shape + public var shape: Shape /// Whether to show delete button or not. var hasDeleteButton: Bool { deleteImage != nil } @@ -40,6 +42,8 @@ extension StepperControl { /// - deleteImage: Delete button image. Default is `Appearance.defaultDeleteImage` /// - incrementImage: Increment button image. Default is `Appearance.defaultIncrementImage` /// - decrementImage: Decrement button image. Default is `Appearance.defaultDecrementImage` + /// - shape: Stepper's shape. Default is `.capsule` + public init( textStyle: (textColor: UIColor, typography: Typography) = (.label, .systemLabel), foregroundColor: UIColor = .label, @@ -48,7 +52,8 @@ extension StepperControl { borderWidth: CGFloat = 1.0, deleteImage: UIImage? = Appearance.defaultDeleteImage, incrementImage: UIImage = Appearance.defaultIncrementImage, - decrementImage: UIImage = Appearance.defaultDecrementImage + decrementImage: UIImage = Appearance.defaultDecrementImage, + shape: Shape = .capsule ) { self.textStyle = textStyle self.backgroundColor = backgroundColor @@ -57,6 +62,7 @@ extension StepperControl { self.deleteImage = deleteImage self.incrementImage = incrementImage self.decrementImage = decrementImage + self.shape = shape } } } diff --git a/Sources/YStepper/UIKit/YStepper+Shapes.swift b/Sources/YStepper/UIKit/YStepper+Shapes.swift new file mode 100644 index 0000000..bfdb341 --- /dev/null +++ b/Sources/YStepper/UIKit/YStepper+Shapes.swift @@ -0,0 +1,25 @@ +// +// YStepper+Shapes.swift +// YStepper +// +// Created by Sahil Saini on 15/03/23. +// Copyright © 2023 Y Media Labs. All rights reserved. +// + +import UIKit + +extension StepperControl.Appearance { + /// Stepper's shape. + public enum Shape: Equatable { + /// None + case none + /// Rectangle. + case rectangle + /// Rounded rectangle. + case roundRect(cornerRadius: CGFloat) + /// Rounded rectangle that scales with Dynamic Type. + case scaledRoundRect(cornerRadius: CGFloat) + /// Capsule. + case capsule + } +} diff --git a/Tests/YStepperTests/Views/StepperTests.swift b/Tests/YStepperTests/Views/StepperTests.swift index 24e1f4b..171fb44 100644 --- a/Tests/YStepperTests/Views/StepperTests.swift +++ b/Tests/YStepperTests/Views/StepperTests.swift @@ -145,13 +145,87 @@ final class StepperTests: XCTestCase { XCTAssertEqual(sut.value, sut.minimumValue) } - func testBackgroundNotNill() { + func testBackgroundNotNil() { let sut = makeSUT() let button = sut.getDecrementButton() XCTAssertNotNil(button) } + func testShapesNotNil() { + var expectedAppearance = StepperControl.Appearance(shape: .rectangle) + var sut = makeSUT(appearance: expectedAppearance) + let rectShape = sut.getShape() + + XCTAssertEqual(sut.appearance.shape, expectedAppearance.shape) + XCTAssertNotNil(rectShape) + + expectedAppearance = StepperControl.Appearance(shape: .roundRect(cornerRadius: 10)) + sut.appearance = expectedAppearance + let roundedRectShape = sut.getShape() + + XCTAssertEqual(sut.appearance.shape, expectedAppearance.shape) + XCTAssertNotNil(roundedRectShape) + + expectedAppearance = StepperControl.Appearance(shape: .scaledRoundRect(cornerRadius: 10)) + sut.appearance = expectedAppearance + let scaledRoundRect = sut.getShape() + + XCTAssertEqual(sut.appearance.shape, expectedAppearance.shape) + XCTAssertNotNil(scaledRoundRect) + + expectedAppearance = StepperControl.Appearance(shape: .capsule) + sut.appearance = expectedAppearance + let capsuleShape = sut.getShape() + + XCTAssertEqual(sut.appearance.shape, expectedAppearance.shape) + XCTAssertNotNil(capsuleShape) + + expectedAppearance = StepperControl.Appearance(shape: .none) + sut.appearance = expectedAppearance + let emptyView = sut.getShape() + + XCTAssertEqual(sut.appearance.shape, expectedAppearance.shape) + XCTAssertNotNil(emptyView) + } + + func testShapesWithoutStrokeNotNil() { + var expectedAppearance = StepperControl.Appearance(shape: .rectangle) + var sut = makeSUT(appearance: expectedAppearance) + let rectShape = sut.getShapeWithoutStroke() + + XCTAssertEqual(sut.appearance.shape, expectedAppearance.shape) + XCTAssertNotNil(rectShape) + + expectedAppearance = StepperControl.Appearance(shape: .roundRect(cornerRadius: 10)) + sut.appearance = expectedAppearance + let roundedRectShape = sut.getShapeWithoutStroke() + + XCTAssertEqual(sut.appearance.shape, expectedAppearance.shape) + XCTAssertNotNil(roundedRectShape) + + expectedAppearance = StepperControl.Appearance(shape: .scaledRoundRect(cornerRadius: 10)) + sut.appearance = expectedAppearance + let scaledRoundRect = sut.getShapeWithoutStroke() + + XCTAssertEqual(sut.appearance.shape, expectedAppearance.shape) + XCTAssertNotNil(scaledRoundRect) + + expectedAppearance = StepperControl.Appearance(shape: .capsule) + sut.appearance = expectedAppearance + let capsuleShape = sut.getShapeWithoutStroke() + + XCTAssertEqual(sut.appearance.shape, expectedAppearance.shape) + XCTAssertNotNil(capsuleShape) + + expectedAppearance = StepperControl.Appearance(shape: .none) + sut.appearance = expectedAppearance + let emptyView = sut.getShapeWithoutStroke() + + XCTAssertEqual(sut.appearance.shape, expectedAppearance.shape) + XCTAssertNotNil(emptyView) + } + func testPreviewNotNil() { XCTAssertNotNil(Stepper_Previews.previews) }