Skip to content

Commit

Permalink
Merge pull request #675 from adevinta/204-component-slider-develop
Browse files Browse the repository at this point in the history
[Slider#204] Implementation of SliderUIControl
  • Loading branch information
LouisBorleeAdevinta authored Dec 14, 2023
2 parents 9b19852 + 80acef6 commit 84cbbc2
Show file tree
Hide file tree
Showing 25 changed files with 1,827 additions and 257 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// SliderAccessibilityIdentifier.swift
// SparkCore
//
// Created by louis.borlee on 12.12.23.
// Copyright © 2023 Adevinta. All rights reserved.
//

import Foundation

/// The accessibility identifiers for the slider.
public enum SliderAccessibilityIdentifier {

// MARK: - Properties

/// The text label accessibility identifier.
public static let slider = "spark-slider"
}
105 changes: 105 additions & 0 deletions core/Sources/Components/Slider/Handle/View/SliderHandleUIControl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//
// SliderHandleUIControl.swift
// SparkCore
//
// Created by louis.borlee on 23/11/2023.
// Copyright © 2023 Adevinta. All rights reserved.
//

import UIKit
import Combine

final class SliderHandleUIControl: UIControl {

private let handleView = UIView()
private let activeIndicatorView = UIView()

private var cancellables = Set<AnyCancellable>()

let viewModel: SliderHandleViewModel

override var isHighlighted: Bool {
didSet {
self.activeIndicatorView.isHidden = !self.isHighlighted
}
}

init(viewModel: SliderHandleViewModel) {
self.viewModel = viewModel
super.init(frame: .init(
origin: .zero,
size: .init(
width: SliderConstants.handleSize.width,
height: SliderConstants.handleSize.height
)
))
self.setupHandleView()
self.setupActiveHandleView()
self.subscribeToViewModelChanges()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func setupHandleView() {
self.handleView.frame = self.bounds
self.handleView.isUserInteractionEnabled = false
self.handleView.layer.cornerRadius = SliderConstants.handleSize.height / 2
self.addSubview(self.handleView)
}

private func setupActiveHandleView() {
self.activeIndicatorView.setBorderWidth(1.0)
self.activeIndicatorView.isUserInteractionEnabled = false
self.activeIndicatorView.isHidden = true
self.activeIndicatorView.layer.cornerRadius = SliderConstants.activeIndicatorSize.height / 2

self.activeIndicatorView.frame.size = .init(width: SliderConstants.activeIndicatorSize.width, height: SliderConstants.activeIndicatorSize.height)
self.activeIndicatorView.center = self.handleView.center

self.insertSubview(self.activeIndicatorView, belowSubview: self.handleView)
}

private func subscribeToViewModelChanges() {
self.viewModel.$color.subscribe(in: &self.cancellables) { [weak self] newColor in
guard let self else { return }
self.handleView.backgroundColor = newColor.uiColor
self.activeIndicatorView.setBorderColor(from: newColor)
}
self.viewModel.$activeIndicatorColor.subscribe(in: &self.cancellables) { [weak self] newActiveIndicatorColor in
guard let self else { return }
self.activeIndicatorView.backgroundColor = newActiveIndicatorColor.uiColor
}
}

override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let beginTracking = super.beginTracking(touch, with: event)
if let supercontrol = superview as? UIControl {
return supercontrol.beginTracking(touch, with: event)
}
return beginTracking
}

override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let continueTracking = super.continueTracking(touch, with: event)
if let supercontrol = superview as? UIControl {
return supercontrol.continueTracking(touch, with: event)
}
return continueTracking
}

override func cancelTracking(with event: UIEvent?) {
super.cancelTracking(with: event)
if let supercontrol = superview as? UIControl {
supercontrol.cancelTracking(with: event)
}
}

override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
super.endTracking(touch, with: event)
if let supercontrol = superview as? UIControl {
supercontrol.endTracking(touch, with: event)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// SliderHandleViewModel.swift
// SparkCore
//
// Created by louis.borlee on 23/11/2023.
// Copyright © 2023 Adevinta. All rights reserved.
//

import Foundation

final class SliderHandleViewModel {

@Published var color: any ColorToken
@Published var activeIndicatorColor: any ColorToken

init(color: some ColorToken,
activeIndicatorColor: some ColorToken) {
self.color = color
self.activeIndicatorColor = activeIndicatorColor
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// SliderColors+ExtensionTests.swift
// SparkCore
//
// Created by louis.borlee on 08/12/2023.
// Copyright © 2023 Adevinta. All rights reserved.
//

import Foundation
@testable import SparkCore

extension SliderColors {
static func mocked(colors: Colors) -> SliderColors {
return .init(
track: colors.feedback.alert,
indicator: colors.accent.accentVariant,
handle: colors.states.neutralPressed,
handleActiveIndicator: colors.basic.onBasicContainer
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// SliderRadii+ExtensionTests.swift
// SparkCoreUnitTests
//
// Created by louis.borlee on 08/12/2023.
// Copyright © 2023 Adevinta. All rights reserved.
//

import Foundation
@testable import SparkCore

extension SliderRadii {
static func mocked() -> SliderRadii {
return .init(
trackRadius: 0.123,
indicatorRadius: 49.3
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ enum SliderCreateValuesFromStepsUseCasableError: Error {

// sourcery: AutoMockable
protocol SliderCreateValuesFromStepsUseCasable {
func execute(from: CGFloat,
to: CGFloat,
steps: CGFloat) throws -> [CGFloat]
func execute(from: Float,
to: Float,
steps: Float) throws -> [Float]
}

final class SliderCreateValuesFromStepsUseCase: SliderCreateValuesFromStepsUseCasable {
func execute(from: CGFloat, to: CGFloat, steps: CGFloat) throws -> [CGFloat] {
func execute(from: Float, to: Float, steps: Float) throws -> [Float] {
guard from < to else { throw SliderCreateValuesFromStepsUseCasableError.invalidRange }
guard steps > .zero,
steps <= (to - from) else { throw SliderCreateValuesFromStepsUseCasableError.invalidStep }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation

// sourcery: AutoMockable
protocol SliderGetClosestValueUseCasable {
func execute(from: CGFloat, to: CGFloat, withSteps steps: CGFloat, fromValue value: CGFloat) -> CGFloat
func execute(from: Float, to: Float, withSteps steps: Float, fromValue value: Float) -> Float
}

final class SliderGetClosestValueUseCase: SliderGetClosestValueUseCasable {
Expand All @@ -21,7 +21,7 @@ final class SliderGetClosestValueUseCase: SliderGetClosestValueUseCasable {
}


func execute(from: CGFloat, to: CGFloat, withSteps steps: CGFloat, fromValue value: CGFloat) -> CGFloat {
func execute(from: Float, to: Float, withSteps steps: Float, fromValue value: Float) -> Float {
do {
let values = try self.createValuesFromStepsUseCase.execute(
from: from,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ final class SliderGetClosestValueUseCaseTests: XCTestCase {
func test_execute_createValuesFromStep_receivedValues() throws {
// GIVEN
let createValuesFromStepUseCaseMock = SliderCreateValuesFromStepsUseCasableGeneratedMock()
createValuesFromStepUseCaseMock._executeWithFromAndToAndSteps = { _, _ , _ throws -> [CGFloat] in
createValuesFromStepUseCaseMock._executeWithFromAndToAndSteps = { _, _ , _ throws -> [Float] in
return [1, 2, 3]
}
let sut = SliderGetClosestValueUseCase(createValuesFromStepsUseCase: createValuesFromStepUseCaseMock)
Expand All @@ -35,7 +35,7 @@ final class SliderGetClosestValueUseCaseTests: XCTestCase {
func test_execute_createValuesFromStep_throws_invalidRange() {
// GIVEN
let createValuesFromStepUseCaseMock = SliderCreateValuesFromStepsUseCasableGeneratedMock()
createValuesFromStepUseCaseMock._executeWithFromAndToAndSteps = { _, _ , _ throws -> [CGFloat] in
createValuesFromStepUseCaseMock._executeWithFromAndToAndSteps = { _, _ , _ throws -> [Float] in
throw SliderCreateValuesFromStepsUseCasableError.invalidRange
}
let sut = SliderGetClosestValueUseCase(createValuesFromStepsUseCase: createValuesFromStepUseCaseMock)
Expand All @@ -50,7 +50,7 @@ final class SliderGetClosestValueUseCaseTests: XCTestCase {
func test_execute_createValuesFromStep_throws_invalid() {
// GIVEN
let createValuesFromStepUseCaseMock = SliderCreateValuesFromStepsUseCasableGeneratedMock()
createValuesFromStepUseCaseMock._executeWithFromAndToAndSteps = { _, _ , _ throws -> [CGFloat] in
createValuesFromStepUseCaseMock._executeWithFromAndToAndSteps = { _, _ , _ throws -> [Float] in
throw SliderCreateValuesFromStepsUseCasableError.invalidStep
}
let sut = SliderGetClosestValueUseCase(createValuesFromStepsUseCase: createValuesFromStepUseCaseMock)
Expand All @@ -65,7 +65,7 @@ final class SliderGetClosestValueUseCaseTests: XCTestCase {
func test_execute_createValuesFromStep_empty_values() {
// GIVEN
let createValuesFromStepUseCaseMock = SliderCreateValuesFromStepsUseCasableGeneratedMock()
createValuesFromStepUseCaseMock._executeWithFromAndToAndSteps = { _, _ , _ throws -> [CGFloat] in
createValuesFromStepUseCaseMock._executeWithFromAndToAndSteps = { _, _ , _ throws -> [Float] in
return []
}
let sut = SliderGetClosestValueUseCase(createValuesFromStepsUseCase: createValuesFromStepUseCaseMock)
Expand All @@ -80,29 +80,29 @@ final class SliderGetClosestValueUseCaseTests: XCTestCase {
func test_execute_inbetween() {
// GIVEN
let createValuesFromStepUseCaseMock = SliderCreateValuesFromStepsUseCasableGeneratedMock()
createValuesFromStepUseCaseMock._executeWithFromAndToAndSteps = { _, _ , _ throws -> [CGFloat] in
createValuesFromStepUseCaseMock._executeWithFromAndToAndSteps = { _, _ , _ throws -> [Float] in
return [
0.0,
0.1,
0.2,
0.3,
0.4,
0.5
0,
10,
20,
30,
40,
50
]
}
let sut = SliderGetClosestValueUseCase(createValuesFromStepsUseCase: createValuesFromStepUseCaseMock)

// WHEN
let value = sut.execute(from: 0, to: 0.5, withSteps: 0.1, fromValue: 0.25)
let value = sut.execute(from: 0, to: 50, withSteps: 0.1, fromValue: 25)

// THEN
XCTAssertEqual(value, 0.3)
XCTAssertEqual(value, 30)
}

func test_execute_lower() {
// GIVEN
let createValuesFromStepUseCaseMock = SliderCreateValuesFromStepsUseCasableGeneratedMock()
createValuesFromStepUseCaseMock._executeWithFromAndToAndSteps = { _, _ , _ throws -> [CGFloat] in
createValuesFromStepUseCaseMock._executeWithFromAndToAndSteps = { _, _ , _ throws -> [Float] in
return [
0.0,
0.5
Expand All @@ -120,7 +120,7 @@ final class SliderGetClosestValueUseCaseTests: XCTestCase {
func test_execute_upper() {
// GIVEN
let createValuesFromStepUseCaseMock = SliderCreateValuesFromStepsUseCasableGeneratedMock()
createValuesFromStepUseCaseMock._executeWithFromAndToAndSteps = { _, _ , _ throws -> [CGFloat] in
createValuesFromStepUseCaseMock._executeWithFromAndToAndSteps = { _, _ , _ throws -> [Float] in
return [
0.0,
0.1,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// SliderGetColorsUseCasableGeneratedMock+ExtensionTests.swift
// SparkCoreUnitTests
//
// Created by louis.borlee on 06/12/2023.
// Copyright © 2023 Adevinta. All rights reserved.
//

import Foundation
@testable import SparkCore

extension SliderGetColorsUseCasableGeneratedMock {
static func mocked(returnedColors colors: SliderColors) -> SliderGetColorsUseCasableGeneratedMock {
let mock = SliderGetColorsUseCasableGeneratedMock()
mock._executeWithThemeAndIntent = { _, _ in
return colors
}
return mock
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,16 @@ import UIKit
// sourcery: AutoMockable
protocol SliderGetColorsUseCasable {
func execute(theme: Theme,
intent: SliderIntent,
isEnabled: Bool) -> SliderColors
intent: SliderIntent) -> SliderColors
}

final class SliderGetColorsUseCase: SliderGetColorsUseCasable {
func execute(theme: Theme,
intent: SliderIntent,
isEnabled: Bool) -> SliderColors {
intent: SliderIntent) -> SliderColors {
let colors = theme.colors
let dims = theme.dims

var sliderColors: SliderColors
let sliderColors: SliderColors
let trackColor = colors.base.onBackground.opacity(dims.dim4)
switch intent {
case .basic:
Expand Down Expand Up @@ -89,9 +87,6 @@ final class SliderGetColorsUseCase: SliderGetColorsUseCasable {
handleActiveIndicator: colors.feedback.infoContainer
)
}
if isEnabled == false {
sliderColors = sliderColors.withOpacity(dims.dim3)
}
return sliderColors
}
}
Loading

0 comments on commit 84cbbc2

Please sign in to comment.