-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #739 from adevinta/204-component-slider
Added Slider component for UIKit and SwiftUI
- Loading branch information
Showing
47 changed files
with
3,297 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
core/Sources/Components/Slider/AccessibilityIdentifiier/SliderAccessibilityIdentifier.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
15
core/Sources/Components/Slider/Constant/SliderConstants.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
49
core/Sources/Components/Slider/Handle/View/SliderHandle.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
113
core/Sources/Components/Slider/Handle/View/SliderHandleUIControl.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
core/Sources/Components/Slider/Handle/ViewModel/SliderHandleViewModel.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
core/Sources/Components/Slider/Properties/Private/SliderColors+ExtensionTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
core/Sources/Components/Slider/Properties/Private/SliderColors.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
core/Sources/Components/Slider/Properties/Private/SliderColorsTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
|
||
} |
19 changes: 19 additions & 0 deletions
19
core/Sources/Components/Slider/Properties/Private/SliderRadii+ExtensionTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
core/Sources/Components/Slider/Properties/Private/SliderRadii.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
23 changes: 23 additions & 0 deletions
23
core/Sources/Components/Slider/Properties/Public/SliderIntent.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.