Skip to content

Commit

Permalink
PM-15891: Updated slider (#1219)
Browse files Browse the repository at this point in the history
  • Loading branch information
ezimet-livefront authored Dec 20, 2024
1 parent 92d101f commit fe4ba4b
Show file tree
Hide file tree
Showing 18 changed files with 378 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xDC",
"green" : "0x5D",
"red" : "0x17"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xAB",
"red" : "0x65"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x33",
"green" : "0x27",
"red" : "0x20"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xEF",
"green" : "0xE9",
"red" : "0xE6"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x6A",
"green" : "0x5B",
"red" : "0x52"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
137 changes: 137 additions & 0 deletions BitwardenShared/UI/Platform/Application/Views/BitwardenSlider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import SwiftUI

/// A custom slider view that allows for custom styling and accessibility.
///
struct BitwardenSlider: View {
// MARK: Private Properties

/// The size of the thumb view.
@SwiftUI.State private var thumbSize: CGSize = .zero

// MARK: Properties

/// A closure containing the action to take when the slider begins or ends editing.
let onEditingChanged: (Bool) -> Void

/// The range of allowable values for the slider.
let range: ClosedRange<Double>

/// The distance between each valid value.
let step: Double

/// The current value of the slider.
@Binding var value: Double

// MARK: Default Colors

/// The color of the slider track.
var trackColor: Color = Asset.Colors.sliderTrack.swiftUIColor

/// The color of the filled portion of the slider track.
var filledTrackColor: Color = Asset.Colors.sliderFilled.swiftUIColor

var body: some View {
GeometryReader { geometry in
let thumbPosition = thumbPosition(in: geometry.size)
ZStack {
Rectangle()
.fill(trackColor)
.frame(height: 4)
.cornerRadius(2)
.overlay(
Rectangle()
.fill(filledTrackColor)
.frame(width: thumbPosition, height: 4)
.cornerRadius(2),
alignment: .leading
)

Circle()
.fill(Asset.Colors.sliderFilled.swiftUIColor)
.frame(width: 18, height: 18)
.overlay(
Circle()
.stroke(Asset.Colors.sliderThumbBorder.swiftUIColor, lineWidth: 2)
)
.onSizeChanged { size in
thumbSize = size
}
.position(x: max(0, thumbPosition), y: geometry.size.height / 2)
.gesture(
DragGesture()
.onChanged { value in
self.value = valueFrom(position: value.location.x, in: geometry.size)
onEditingChanged(true)
}
.onEnded { _ in
onEditingChanged(false)
}
)
}
}
.frame(height: 44)
.accessibilityElement()
.accessibilityValue(Text("\(Int(value))"))
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
let newValue = min(value + step, range.upperBound)
value = newValue
onEditingChanged(true)
onEditingChanged(false)
case .decrement:
let newValue = max(value - step, range.lowerBound)
value = newValue
onEditingChanged(true)
onEditingChanged(false)
default:
break
}
}
}

// MARK: Initialization

/// Initialize a `BitwardenSlider`.
///
/// - Parameters:
/// - value: The current value of the slider.
/// - range: The range of allowable values for the slider.
/// - step: The distance between each valid value.
/// - onEditingChanged: A closure containing the action to take when the slider begins or ends editing.
/// - trackColor: The color of the slider track.
/// - filledTrackColor: The color of the filled portion of the slider track.
///
init(
value: Binding<Double>,
in range: ClosedRange<Double>,
step: Double,
onEditingChanged: @escaping (Bool) -> Void,
trackColor: Color = Asset.Colors.sliderTrack.swiftUIColor,
filledTrackColor: Color = Asset.Colors.sliderFilled.swiftUIColor
) {
_value = value
self.range = range
self.step = step
self.onEditingChanged = onEditingChanged
self.trackColor = trackColor
self.filledTrackColor = filledTrackColor
}

// MARK: private methods

/// Calculate the position of the thumb view based on the current `value`.
private func thumbPosition(in size: CGSize) -> CGFloat {
let availableWidth = size.width - thumbSize.width // Adjust for thumb size
let relativeValue = (value - range.lowerBound) / (range.upperBound - range.lowerBound)
return availableWidth * CGFloat(relativeValue) + thumbSize.width / 2 // Adjust for thumb size
}

/// Calculate the `value` based on the position of the thumb view.
private func valueFrom(position: CGFloat, in size: CGSize) -> Double {
let availableWidth = size.width - thumbSize.width // Adjust for thumb size
let relativePosition = (position - thumbSize.width / 2) / availableWidth // Adjust for thumb size
let newValue = Double(relativePosition) * (range.upperBound - range.lowerBound) + range.lowerBound
return min(max(newValue, range.lowerBound), range.upperBound)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import SnapshotTesting
import XCTest

@testable import BitwardenShared

// MARK: - BitwardenSliderTests

class BitwardenSliderTests: BitwardenTestCase {
// MARK: Tests

/// Test a snapshot of the slider with a value of 0.
func test_snapshot_slider_minValue() {
let subject = BitwardenSlider(
value: .constant(0),
in: 0...50,
step: 1,
onEditingChanged: { _ in }
)
assertSnapshots(
of: subject,
as: [.defaultPortrait, .defaultPortraitDark]
)
}

/// Test a snapshot of the slider with a value of 25.
func test_snapshot_slider_midValue() {
let subject = BitwardenSlider(
value: .constant(25),
in: 0...50,
step: 1,
onEditingChanged: { _ in }
)
assertSnapshots(
of: subject,
as: [.defaultPortrait, .defaultPortraitDark]
)
}

/// Test a snapshot of the slider with a value of 50.
func test_snapshot_slider_maxValue() {
let subject = BitwardenSlider(
value: .constant(50),
in: 0...50,
step: 1,
onEditingChanged: { _ in }
)
assertSnapshots(
of: subject,
as: [.defaultPortrait, .defaultPortraitDark]
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,31 +53,22 @@ struct SliderFieldView<State>: View {
/// A closure containing the action to take when a new value is selected.
let onValueChanged: (Double) -> Void

var body: some View {
VStack(spacing: 8) {
HStack {
Text(field.title)
.styleGuide(.body)
.foregroundColor(Asset.Colors.textPrimary.swiftUIColor)

Spacer()

Text(String(Int(field.value)))
.styleGuide(.body, monoSpacedDigit: true)
.foregroundColor(Asset.Colors.textSecondary.swiftUIColor)
.accessibilityIdentifier(field.sliderValueAccessibilityId ?? field.id)
}
.accessibilityHidden(true)
/// The width of the three digit text "000" based on the current font.
@SwiftUI.State private var minTextWidth: CGFloat = 14

Divider()
var body: some View {
HStack(alignment: .center, spacing: 16) {
Text(field.title)
.styleGuide(.body)
.foregroundColor(Asset.Colors.textPrimary.swiftUIColor)
.accessibilityHidden(true)

Slider(
BitwardenSlider(
value: Binding(get: { field.value }, set: onValueChanged),
in: field.range,
step: field.step,
onEditingChanged: onEditingChanged
)
.tint(Asset.Colors.tintPrimary.swiftUIColor)
.accessibilityLabel(field.title)
.accessibilityIdentifier(field.sliderAccessibilityId ?? field.title)
.apply { view in
Expand All @@ -94,11 +85,21 @@ struct SliderFieldView<State>: View {
view
}
}

Text(String(Int(field.value)))
.styleGuide(.body, monoSpacedDigit: true)
.foregroundColor(Asset.Colors.textSecondary.swiftUIColor)
.accessibilityIdentifier(field.sliderValueAccessibilityId ?? field.id)
.accessibilityHidden(true)
.frame(minWidth: minTextWidth)
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Asset.Colors.backgroundSecondary.swiftUIColor)
.cornerRadius(10)
.background {
calculateMinTextWidth()
}
}

// MARK: Initialization
Expand All @@ -119,4 +120,19 @@ struct SliderFieldView<State>: View {
self.onEditingChanged = onEditingChanged
self.onValueChanged = onValueChanged
}

// MARK: Private methods

/// Calculate the width of the text "000" based on the current font.
private func calculateMinTextWidth() -> some View {
Text("000")
.styleGuide(.body, monoSpacedDigit: true)
.hidden()
.background(GeometryReader { geometry in
Color.clear
.onAppear {
minTextWidth = geometry.size.width
}
})
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit fe4ba4b

Please sign in to comment.