Skip to content

Commit

Permalink
Merge pull request #739 from adevinta/204-component-slider
Browse files Browse the repository at this point in the history
Added Slider component for UIKit and SwiftUI
  • Loading branch information
aycil-alican authored Jan 19, 2024
2 parents 74c132e + 99a92de commit 824d4c8
Show file tree
Hide file tree
Showing 47 changed files with 3,297 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import UIKit

extension UIView {

/// CGColors need to be refreshed on trait changes
func setBorderColor(from colorToken: any ColorToken) {
self.layer.borderColor = colorToken.uiColor.cgColor
}
Expand Down
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"
}
15 changes: 15 additions & 0 deletions core/Sources/Components/Slider/Constant/SliderConstants.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// SliderConstants.swift
// Spark
//
// Created by louis.borlee on 23/11/2023.
// Copyright © 2023 Adevinta. All rights reserved.
//

import Foundation

enum SliderConstants {
static let handleSize = CGSize(width: 24, height: 24)
static let activeIndicatorSize = CGSize(width: 32, height: 32)
static let barHeight: CGFloat = 4.0
}
49 changes: 49 additions & 0 deletions core/Sources/Components/Slider/Handle/View/SliderHandle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// SliderHandle.swift
// SparkCore
//
// Created by louis.borlee on 13/12/2023.
// Copyright © 2023 Adevinta. All rights reserved.
//

import SwiftUI

struct SliderHandle: View {

@ObservedObject var viewModel: SliderHandleViewModel

@Binding var isEditing: Bool

init(viewModel: SliderHandleViewModel,
isEditing: Binding<Bool>) {
self.viewModel = viewModel
_isEditing = isEditing
}

var body: some View {
ZStack(alignment: .center) {
if self.isEditing {
self.activeIndicatorStroke()
self.activeIndicatorHalo()
}
Circle()
.fill()
.foregroundColor(self.viewModel.color.color)
.frame(width: SliderConstants.handleSize.width, height: SliderConstants.handleSize.height)
}
}

@ViewBuilder
private func activeIndicatorStroke() -> some View {
Circle()
.strokeBorder(self.viewModel.color.color, lineWidth: 1.0)
.frame(width: SliderConstants.activeIndicatorSize.width, height: SliderConstants.activeIndicatorSize.height)
}

@ViewBuilder
private func activeIndicatorHalo() -> some View {
Circle()
.fill(self.viewModel.activeIndicatorColor)
.frame(width: SliderConstants.activeIndicatorSize.width - 2, height: SliderConstants.activeIndicatorSize.height - 2)
}
}
113 changes: 113 additions & 0 deletions core/Sources/Components/Slider/Handle/View/SliderHandleUIControl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//
// 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)
}
}

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
// CGColors need to be refreshed on trait changes
if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
self.activeIndicatorView.setBorderColor(from: self.viewModel.color)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// SliderHandleViewModel.swift
// SparkCore
//
// Created by louis.borlee on 23/11/2023.
// Copyright © 2023 Adevinta. All rights reserved.
//

import Foundation
import Combine

final class SliderHandleViewModel: ObservableObject {

@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,32 @@
//
// SliderColors.swift
// SparkCore
//
// Created by louis.borlee on 23/11/2023.
// Copyright © 2023 Adevinta. All rights reserved.
//

import Foundation

struct SliderColors: Equatable {
let track: any ColorToken
let indicator: any ColorToken
let handle: any ColorToken
let handleActiveIndicator: any ColorToken

func withOpacity(_ opacity: CGFloat) -> SliderColors {
return .init(
track: self.track.opacity(opacity),
indicator: self.indicator.opacity(opacity),
handle: self.handle.opacity(opacity),
handleActiveIndicator: self.handleActiveIndicator.opacity(opacity)
)
}

static func == (lhs: SliderColors, rhs: SliderColors) -> Bool {
return lhs.track.equals(rhs.track) &&
lhs.indicator.equals(rhs.indicator) &&
lhs.handle.equals(rhs.handle) &&
lhs.handleActiveIndicator.equals(rhs.handleActiveIndicator)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// SliderColorsTests.swift
// SparkCoreUnitTests
//
// Created by louis.borlee on 23/11/2023.
// Copyright © 2023 Adevinta. All rights reserved.
//

import XCTest
@testable import SparkCore

final class SliderColorsTests: XCTestCase {

private let colors: Colors = ColorsGeneratedMock.mocked()
private let dims: Dims = DimsGeneratedMock.mocked()

func test_withOpacity() {
// GIVEN
let track = self.colors.feedback.alertContainer
let indicator = self.colors.states.accentVariantPressed.opacity(self.dims.dim5)
let handle = self.colors.main.onMainContainer
let handleActiveIndicator = self.colors.base.surface
let sut = SliderColors(
track: track,
indicator: indicator,
handle: handle,
handleActiveIndicator: handleActiveIndicator
)
let dim = self.dims.dim3
let expectedResult = SliderColors(
track: track.opacity(dim),
indicator: indicator.opacity(dim),
handle: handle.opacity(dim),
handleActiveIndicator: handleActiveIndicator.opacity(dim)
)

// WHEN
let result = sut.withOpacity(dim)

// THEN
XCTAssertEqual(result, expectedResult)
}

}
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
@@ -0,0 +1,14 @@
//
// SliderRadii.swift
// SparkCore
//
// Created by louis.borlee on 23/11/2023.
// Copyright © 2023 Adevinta. All rights reserved.
//

import Foundation

struct SliderRadii: Equatable {
let trackRadius: CGFloat
let indicatorRadius: CGFloat
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// SliderIntent.swift
// SparkCore
//
// Created by louis.borlee on 23/11/2023.
// Copyright © 2023 Adevinta. All rights reserved.
//

import Foundation

/// The various intents of sliders.
@frozen
public enum SliderIntent: CaseIterable {
case basic
case success
case error
case alert
case accent
case main
case neutral
case support
case info
}
Loading

0 comments on commit 824d4c8

Please sign in to comment.