diff --git a/Package.swift b/Package.swift index adae348..e9ec50f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -14,5 +14,6 @@ let package = Package( targets: [ .target(name: "Sliders"), .testTarget(name: "SlidersTests", dependencies: ["Sliders"]) - ] + ], + swiftLanguageModes: [.version("6")] ) diff --git a/Sources/Sliders/Base/DefaultThumb.swift b/Sources/Sliders/Base/DefaultThumb.swift index 36e4eb9..de46136 100644 --- a/Sources/Sliders/Base/DefaultThumb.swift +++ b/Sources/Sliders/Base/DefaultThumb.swift @@ -1,7 +1,7 @@ import SwiftUI public struct DefaultThumb: View { - public init() {} + nonisolated public init() {} public var body: some View { Capsule() .foregroundColor(.white) diff --git a/Sources/Sliders/Base/LinearValueMath.swift b/Sources/Sliders/Base/LinearValueMath.swift index f8efe56..c6069e0 100644 --- a/Sources/Sliders/Base/LinearValueMath.swift +++ b/Sources/Sliders/Base/LinearValueMath.swift @@ -3,27 +3,78 @@ import SwiftUI /// Linear calculations -/// Calculates distance from zero in points -@inlinable func distanceFrom(value: CGFloat, availableDistance: CGFloat, bounds: ClosedRange = 0.0...1.0, leadingOffset: CGFloat = 0, trailingOffset: CGFloat = 0) -> CGFloat { +/// Calculates the distance in points from the leading edge based on a value within a range. +/// +/// This method computes a visual position (e.g., slider thumb) in a given layout space, taking into account offsets and layout direction. +/// +/// - Parameters: +/// - value: The input value to convert to a position, typically within `bounds`. +/// - availableDistance: The total space available for the layout, excluding leading/trailing offsets. +/// - bounds: The range representing the minimum and maximum values (default is 0.0...1.0). +/// - leadingOffset: The offset at the start of the available space. +/// - trailingOffset: The offset at the end of the available space. +/// - isRightToLeft: Whether the layout should be mirrored (right-to-left). +/// - Returns: The position in points corresponding to the value. +@inlinable +func distanceFrom( + value: CGFloat, + availableDistance: CGFloat, + bounds: ClosedRange = 0.0...1.0, + leadingOffset: CGFloat = 0, + trailingOffset: CGFloat = 0, + isRightToLeft: Bool = false +) -> CGFloat { guard availableDistance > leadingOffset + trailingOffset else { return 0 } - let boundsLenght = bounds.upperBound - bounds.lowerBound - let relativeValue = (value - bounds.lowerBound) / boundsLenght - let offset = (leadingOffset - ((leadingOffset + trailingOffset) * relativeValue)) - return offset + (availableDistance * relativeValue) + + let boundsLength = bounds.upperBound - bounds.lowerBound + let relativeValue = (value - bounds.lowerBound) / boundsLength + + let adjustedRelativeValue = isRightToLeft ? 1.0 - relativeValue : relativeValue + let offset = (leadingOffset - ((leadingOffset + trailingOffset) * adjustedRelativeValue)) + + return offset + (availableDistance * adjustedRelativeValue) } -/// Calculates value for relative point in bounds with step. -/// Example: For relative value 0.5 in range 2.0..4.0 produces 3.0 -@inlinable func valueFrom(distance: CGFloat, availableDistance: CGFloat, bounds: ClosedRange = 0.0...1.0, step: CGFloat = 0.001, leadingOffset: CGFloat = 0, trailingOffset: CGFloat = 0) -> CGFloat { +/// Calculates the value corresponding to a point along a layout axis, clamped to the provided bounds and snapped to the nearest step. +/// +/// This method is typically the inverse of `distanceFrom`, converting a position (e.g., user touch location) back to a value in a specified range. +/// +/// - Parameters: +/// - distance: The visual position in points, such as a touch location. +/// - availableDistance: The total space available for layout. +/// - bounds: The valid value range (default is 0.0...1.0). +/// - step: The smallest allowed step size when calculating the value (default is 0.001). +/// - leadingOffset: Offset before the usable area. +/// - trailingOffset: Offset after the usable area. +/// - isRightToLeft: Whether the layout is in a right-to-left direction. +/// - Returns: The value corresponding to the given distance, clamped and stepped. +@inlinable +func valueFrom( + distance: CGFloat, + availableDistance: CGFloat, + bounds: ClosedRange = 0.0...1.0, + step: CGFloat = 0.001, + leadingOffset: CGFloat = 0, + trailingOffset: CGFloat = 0, + isRightToLeft: Bool = false +) -> CGFloat { let relativeValue = (distance - leadingOffset) / (availableDistance - (leadingOffset + trailingOffset)) - let newValue = bounds.lowerBound + (relativeValue * (bounds.upperBound - bounds.lowerBound)) + let adjustedRelativeValue = isRightToLeft ? 1.0 - relativeValue : relativeValue + + let newValue = bounds.lowerBound + (adjustedRelativeValue * (bounds.upperBound - bounds.lowerBound)) let steppedNewValue = (round(newValue / step) * step) let validatedValue = min(bounds.upperBound, max(bounds.lowerBound, steppedNewValue)) + return validatedValue } +/// Clamps the current value to the specified closed range. +/// +/// - Parameter range: The closed range to clamp the value to. +/// - Returns: The value, clamped to the given range. extension Comparable { func clamped(to range: ClosedRange) -> Self { min(max(self, range.lowerBound), range.upperBound) } } + diff --git a/Sources/Sliders/PointSlider/Style/AnyPointSliderStyle.swift b/Sources/Sliders/PointSlider/Style/AnyPointSliderStyle.swift index 9fc795d..87ffde3 100644 --- a/Sources/Sliders/PointSlider/Style/AnyPointSliderStyle.swift +++ b/Sources/Sliders/PointSlider/Style/AnyPointSliderStyle.swift @@ -1,5 +1,6 @@ import SwiftUI +@MainActor public struct AnyPointSliderStyle: PointSliderStyle { private let styleMakeBody: (PointSliderStyle.Configuration) -> AnyView diff --git a/Sources/Sliders/PointSlider/Style/EnvironmentValues+PointSliderStyle.swift b/Sources/Sliders/PointSlider/Style/EnvironmentValues+PointSliderStyle.swift index 87869a1..9ee1331 100644 --- a/Sources/Sliders/PointSlider/Style/EnvironmentValues+PointSliderStyle.swift +++ b/Sources/Sliders/PointSlider/Style/EnvironmentValues+PointSliderStyle.swift @@ -11,7 +11,8 @@ public extension EnvironmentValues { } } -struct PointSliderStyleKey: EnvironmentKey { +@MainActor +struct PointSliderStyleKey: @preconcurrency EnvironmentKey { static let defaultValue: AnyPointSliderStyle = AnyPointSliderStyle( RectangularPointSliderStyle() ) diff --git a/Sources/Sliders/PointSlider/Style/PointSliderStyle.swift b/Sources/Sliders/PointSlider/Style/PointSliderStyle.swift index 376a7ff..9bfeb61 100644 --- a/Sources/Sliders/PointSlider/Style/PointSliderStyle.swift +++ b/Sources/Sliders/PointSlider/Style/PointSliderStyle.swift @@ -6,6 +6,7 @@ import SwiftUI /// To configure the current `PointSlider` for a view hiearchy, use the /// `.valueSliderStyle()` modifier. @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +@MainActor public protocol PointSliderStyle { /// A `View` representing the body of a `PointSlider`. associatedtype Body : View diff --git a/Sources/Sliders/PointSlider/Styles/PointSliderOptions.swift b/Sources/Sliders/PointSlider/Styles/PointSliderOptions.swift index a1ffe0a..4da69f6 100644 --- a/Sources/Sliders/PointSlider/Styles/PointSliderOptions.swift +++ b/Sources/Sliders/PointSlider/Styles/PointSliderOptions.swift @@ -3,8 +3,8 @@ import SwiftUI public struct PointSliderOptions: OptionSet { public let rawValue: Int - public static let interactiveTrack = PointSliderOptions(rawValue: 1 << 0) - public static let defaultOptions: PointSliderOptions = [] + @MainActor public static let interactiveTrack = PointSliderOptions(rawValue: 1 << 0) + @MainActor public static let defaultOptions: PointSliderOptions = [] public init(rawValue: Int) { self.rawValue = rawValue diff --git a/Sources/Sliders/PointTrack/EnvironmentValues+PointTrackConfiguration.swift b/Sources/Sliders/PointTrack/EnvironmentValues+PointTrackConfiguration.swift index 37961e9..c3b1b56 100644 --- a/Sources/Sliders/PointTrack/EnvironmentValues+PointTrackConfiguration.swift +++ b/Sources/Sliders/PointTrack/EnvironmentValues+PointTrackConfiguration.swift @@ -11,6 +11,7 @@ extension EnvironmentValues { } } -struct PointTrackConfigurationKey: EnvironmentKey { +@MainActor +struct PointTrackConfigurationKey: @preconcurrency EnvironmentKey { static let defaultValue: PointTrackConfiguration = .defaultConfiguration } diff --git a/Sources/Sliders/PointTrack/PointTrackConfiguration.swift b/Sources/Sliders/PointTrack/PointTrackConfiguration.swift index ec2ffd3..91412d1 100644 --- a/Sources/Sliders/PointTrack/PointTrackConfiguration.swift +++ b/Sources/Sliders/PointTrack/PointTrackConfiguration.swift @@ -1,5 +1,6 @@ import SwiftUI +@MainActor public struct PointTrackConfiguration { public static let defaultConfiguration = PointTrackConfiguration() diff --git a/Sources/Sliders/RangeSlider/Style/EnvironmentValues+RangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Style/EnvironmentValues+RangeSliderStyle.swift index e76d550..dd5d538 100644 --- a/Sources/Sliders/RangeSlider/Style/EnvironmentValues+RangeSliderStyle.swift +++ b/Sources/Sliders/RangeSlider/Style/EnvironmentValues+RangeSliderStyle.swift @@ -11,7 +11,8 @@ public extension EnvironmentValues { } } -struct RangeSliderStyleKey: EnvironmentKey { +@MainActor +struct RangeSliderStyleKey: @preconcurrency EnvironmentKey { static let defaultValue: AnyRangeSliderStyle = AnyRangeSliderStyle( HorizontalRangeSliderStyle() ) diff --git a/Sources/Sliders/RangeSlider/Style/RangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Style/RangeSliderStyle.swift index 8c102f9..5b11516 100644 --- a/Sources/Sliders/RangeSlider/Style/RangeSliderStyle.swift +++ b/Sources/Sliders/RangeSlider/Style/RangeSliderStyle.swift @@ -5,7 +5,8 @@ import SwiftUI /// /// To configure the current `RangeSlider` for a view hiearchy, use the /// `.valueSliderStyle()` modifier. -@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +@MainActor +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) // nonisolated public protocol RangeSliderStyle { /// A `View` representing the body of a `RangeSlider`. associatedtype Body : View diff --git a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift index 5ceb961..9d1da06 100644 --- a/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift +++ b/Sources/Sliders/RangeSlider/Styles/Horizontal/HorizontalRangeSliderStyle.swift @@ -1,5 +1,6 @@ import SwiftUI +@MainActor public struct HorizontalRangeSliderStyle: RangeSliderStyle { private let track: Track private let lowerThumb: LowerThumb diff --git a/Sources/Sliders/RangeSlider/Styles/RangeSliderOptions.swift b/Sources/Sliders/RangeSlider/Styles/RangeSliderOptions.swift index 3f96d61..0a67464 100644 --- a/Sources/Sliders/RangeSlider/Styles/RangeSliderOptions.swift +++ b/Sources/Sliders/RangeSlider/Styles/RangeSliderOptions.swift @@ -1,12 +1,13 @@ import SwiftUI +@MainActor public struct RangeSliderOptions: OptionSet { public let rawValue: Int public static let forceAdjacentValue = RangeSliderOptions(rawValue: 1 << 0) public static let defaultOptions: RangeSliderOptions = .forceAdjacentValue - public init(rawValue: Int) { + nonisolated public init(rawValue: Int) { self.rawValue = rawValue } } diff --git a/Sources/Sliders/RangeSlider/Styles/Vertical/VerticalRangeSliderStyle.swift b/Sources/Sliders/RangeSlider/Styles/Vertical/VerticalRangeSliderStyle.swift index 96da8be..e760185 100644 --- a/Sources/Sliders/RangeSlider/Styles/Vertical/VerticalRangeSliderStyle.swift +++ b/Sources/Sliders/RangeSlider/Styles/Vertical/VerticalRangeSliderStyle.swift @@ -1,5 +1,6 @@ import SwiftUI +@MainActor public struct VerticalRangeSliderStyle: RangeSliderStyle { private let track: Track private let lowerThumb: LowerThumb diff --git a/Sources/Sliders/RangeTrack/EnvironmentValues+RangeTrackConfiguration.swift b/Sources/Sliders/RangeTrack/EnvironmentValues+RangeTrackConfiguration.swift index 197f69a..e2779f6 100644 --- a/Sources/Sliders/RangeTrack/EnvironmentValues+RangeTrackConfiguration.swift +++ b/Sources/Sliders/RangeTrack/EnvironmentValues+RangeTrackConfiguration.swift @@ -11,6 +11,7 @@ extension EnvironmentValues { } } -struct RangeTrackConfigurationKey: EnvironmentKey { +@MainActor +struct RangeTrackConfigurationKey: @preconcurrency EnvironmentKey { static let defaultValue: RangeTrackConfiguration = .defaultConfiguration } diff --git a/Sources/Sliders/RangeTrack/RangeTrackConfiguration.swift b/Sources/Sliders/RangeTrack/RangeTrackConfiguration.swift index 2ce4310..fad5d85 100644 --- a/Sources/Sliders/RangeTrack/RangeTrackConfiguration.swift +++ b/Sources/Sliders/RangeTrack/RangeTrackConfiguration.swift @@ -1,5 +1,6 @@ import SwiftUI +@MainActor public struct RangeTrackConfiguration { public static let defaultConfiguration = RangeTrackConfiguration() @@ -10,7 +11,7 @@ public struct RangeTrackConfiguration { public let upperLeadingOffset: CGFloat public let upperTrailingOffset: CGFloat - public init(bounds: ClosedRange = 0.0...1.0, lowerLeadingOffset: CGFloat = 0, lowerTrailingOffset: CGFloat = 0, upperLeadingOffset: CGFloat = 0, upperTrailingOffset: CGFloat = 0) { + nonisolated public init(bounds: ClosedRange = 0.0...1.0, lowerLeadingOffset: CGFloat = 0, lowerTrailingOffset: CGFloat = 0, upperLeadingOffset: CGFloat = 0, upperTrailingOffset: CGFloat = 0) { self.bounds = bounds self.lowerLeadingOffset = lowerLeadingOffset self.lowerTrailingOffset = lowerTrailingOffset @@ -20,7 +21,7 @@ public struct RangeTrackConfiguration { } public extension RangeTrackConfiguration { - init(bounds: ClosedRange = 0.0...1.0, lowerOffset: CGFloat = 0, upperOffset: CGFloat = 0) { + nonisolated init(bounds: ClosedRange = 0.0...1.0, lowerOffset: CGFloat = 0, upperOffset: CGFloat = 0) { self.bounds = bounds self.lowerLeadingOffset = lowerOffset / 2 self.lowerTrailingOffset = lowerOffset / 2 + upperOffset @@ -30,7 +31,7 @@ public extension RangeTrackConfiguration { } public extension RangeTrackConfiguration { - init(bounds: ClosedRange = 0.0...1.0, offsets: CGFloat = 0) { + nonisolated init(bounds: ClosedRange = 0.0...1.0, offsets: CGFloat = 0) { self.bounds = bounds self.lowerLeadingOffset = offsets / 2 self.lowerTrailingOffset = offsets / 2 + offsets diff --git a/Sources/Sliders/ValueSlider/Style/EnvironmentValues+ValueSliderStyle.swift b/Sources/Sliders/ValueSlider/Style/EnvironmentValues+ValueSliderStyle.swift index 96cfa2c..a42e7e7 100644 --- a/Sources/Sliders/ValueSlider/Style/EnvironmentValues+ValueSliderStyle.swift +++ b/Sources/Sliders/ValueSlider/Style/EnvironmentValues+ValueSliderStyle.swift @@ -11,7 +11,8 @@ public extension EnvironmentValues { } } -struct ValueSliderStyleKey: EnvironmentKey { +@MainActor +struct ValueSliderStyleKey: @preconcurrency EnvironmentKey { static let defaultValue: AnyValueSliderStyle = AnyValueSliderStyle( HorizontalValueSliderStyle() ) diff --git a/Sources/Sliders/ValueSlider/Style/ValueSliderStyle.swift b/Sources/Sliders/ValueSlider/Style/ValueSliderStyle.swift index 87bf325..d976ba4 100644 --- a/Sources/Sliders/ValueSlider/Style/ValueSliderStyle.swift +++ b/Sources/Sliders/ValueSlider/Style/ValueSliderStyle.swift @@ -5,6 +5,8 @@ import SwiftUI /// /// To configure the current `ValueSlider` for a view hiearchy, use the /// `.valueSliderStyle()` modifier. +/// +@MainActor @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) public protocol ValueSliderStyle { /// A `View` representing the body of a `ValueSlider`. diff --git a/Sources/Sliders/ValueSlider/Style/ValueSliderStyleConfiguration.swift b/Sources/Sliders/ValueSlider/Style/ValueSliderStyleConfiguration.swift index 7113080..f44c2bb 100644 --- a/Sources/Sliders/ValueSlider/Style/ValueSliderStyleConfiguration.swift +++ b/Sources/Sliders/ValueSlider/Style/ValueSliderStyleConfiguration.swift @@ -6,7 +6,7 @@ public struct ValueSliderStyleConfiguration { public let step: CGFloat public let onEditingChanged: (Bool) -> Void public var dragOffset: Binding - + public init(value: Binding, bounds: ClosedRange, step: CGFloat, onEditingChanged: @escaping (Bool) -> Void, dragOffset: Binding) { self.value = value self.bounds = bounds diff --git a/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift b/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift index 5eb9d61..d50e1e4 100644 --- a/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift +++ b/Sources/Sliders/ValueSlider/Styles/Horizontal/HorizontalValueSliderStyle.swift @@ -1,13 +1,15 @@ import SwiftUI +@MainActor public struct HorizontalValueSliderStyle: ValueSliderStyle { private let track: Track private let thumb: Thumb private let thumbSize: CGSize private let thumbInteractiveSize: CGSize private let options: ValueSliderOptions + private let isRightToLeft: Bool - public func makeBody(configuration: Self.Configuration) -> some View { + public func makeBody(configuration: Self.Configuration) -> some View { let track = self.track .environment(\.trackValue, configuration.value.wrappedValue) .environment(\.valueTrackConfiguration, ValueTrackConfiguration( @@ -30,7 +32,8 @@ public struct HorizontalValueSliderStyle: ValueSliderS bounds: configuration.bounds, step: configuration.step, leadingOffset: self.thumbSize.width / 2, - trailingOffset: self.thumbSize.width / 2 + trailingOffset: self.thumbSize.width / 2, + isRightToLeft: isRightToLeft ) configuration.value.wrappedValue = computedValue } @@ -68,7 +71,8 @@ public struct HorizontalValueSliderStyle: ValueSliderS availableDistance: geometry.size.width, bounds: configuration.bounds, leadingOffset: self.thumbSize.width / 2, - trailingOffset: self.thumbSize.width / 2 + trailingOffset: self.thumbSize.width / 2, + isRightToLeft: isRightToLeft ) } @@ -78,7 +82,8 @@ public struct HorizontalValueSliderStyle: ValueSliderS bounds: configuration.bounds, step: configuration.step, leadingOffset: self.thumbSize.width / 2, - trailingOffset: self.thumbSize.width / 2 + trailingOffset: self.thumbSize.width / 2, + isRightToLeft: isRightToLeft ) configuration.value.wrappedValue = computedValue @@ -94,45 +99,93 @@ public struct HorizontalValueSliderStyle: ValueSliderS .frame(minHeight: self.thumbInteractiveSize.height) } - public init(track: Track, thumb: Thumb, thumbSize: CGSize = CGSize(width: 27, height: 27), thumbInteractiveSize: CGSize = CGSize(width: 44, height: 44), options: ValueSliderOptions = .defaultOptions) { + public init(track: Track, thumb: Thumb, thumbSize: CGSize = CGSize(width: 27, height: 27), thumbInteractiveSize: CGSize = CGSize(width: 44, height: 44), options: ValueSliderOptions = .defaultOptions, isRightToLeft: Bool = false) { self.track = track self.thumb = thumb self.thumbSize = thumbSize self.thumbInteractiveSize = thumbInteractiveSize self.options = options + self.isRightToLeft = isRightToLeft } } extension HorizontalValueSliderStyle where Track == DefaultHorizontalValueTrack { - public init(thumb: Thumb, thumbSize: CGSize = CGSize(width: 27, height: 27), thumbInteractiveSize: CGSize = CGSize(width: 44, height: 44), options: ValueSliderOptions = .defaultOptions) { + + /// Creates a `HorizontalValueSliderStyle` with a custom thumb and default horizontal track. + /// + /// - Parameters: + /// - thumb: A custom view representing the thumb. + /// - thumbSize: The visual size of the thumb. Default is `27x27`. + /// - thumbInteractiveSize: The tappable area around the thumb for user interaction. Default is `44x44`. + /// - options: Slider options to customize behavior (e.g., stepping, animations). Default is `.defaultOptions`. + /// - isRightToLeft: Whether the slider should be drawn right-to-left. Default is `false`. + public init( + thumb: Thumb, + thumbSize: CGSize = CGSize(width: 27, height: 27), + thumbInteractiveSize: CGSize = CGSize(width: 44, height: 44), + options: ValueSliderOptions = .defaultOptions, + isRightToLeft: Bool = false + ) { self.track = DefaultHorizontalValueTrack() self.thumb = thumb self.thumbSize = thumbSize self.thumbInteractiveSize = thumbInteractiveSize self.options = options + self.isRightToLeft = isRightToLeft } } extension HorizontalValueSliderStyle where Thumb == DefaultThumb { - public init(track: Track, thumbSize: CGSize = CGSize(width: 27, height: 27), thumbInteractiveSize: CGSize = CGSize(width: 44, height: 44), options: ValueSliderOptions = .defaultOptions) { + + /// Creates a `HorizontalValueSliderStyle` with a custom track and default thumb. + /// + /// - Parameters: + /// - track: A custom view representing the slider's track. + /// - thumbSize: The visual size of the default thumb. Default is `27x27`. + /// - thumbInteractiveSize: The tappable area around the thumb for user interaction. Default is `44x44`. + /// - options: Slider options to customize behavior (e.g., stepping, animations). Default is `.defaultOptions`. + /// - isRightToLeft: Whether the slider should be drawn right-to-left. Default is `false`. + public init( + track: Track, + thumbSize: CGSize = CGSize(width: 27, height: 27), + thumbInteractiveSize: CGSize = CGSize(width: 44, height: 44), + options: ValueSliderOptions = .defaultOptions, + isRightToLeft: Bool = false + ) { self.track = track self.thumb = DefaultThumb() self.thumbSize = thumbSize self.thumbInteractiveSize = thumbInteractiveSize self.options = options + self.isRightToLeft = isRightToLeft } } extension HorizontalValueSliderStyle where Thumb == DefaultThumb, Track == DefaultHorizontalValueTrack { - public init(thumbSize: CGSize = CGSize(width: 27, height: 27), thumbInteractiveSize: CGSize = CGSize(width: 44, height: 44), options: ValueSliderOptions = .defaultOptions) { + + /// Creates a `HorizontalValueSliderStyle` with the default thumb and default horizontal track. + /// + /// - Parameters: + /// - thumbSize: The visual size of the thumb. Default is `27x27`. + /// - thumbInteractiveSize: The tappable area around the thumb for user interaction. Default is `44x44`. + /// - options: Slider options to customize behavior (e.g., stepping, animations). Default is `.defaultOptions`. + /// - isRightToLeft: Whether the slider should be drawn right-to-left. Default is `false`. + public init( + thumbSize: CGSize = CGSize(width: 27, height: 27), + thumbInteractiveSize: CGSize = CGSize(width: 44, height: 44), + options: ValueSliderOptions = .defaultOptions, + isRightToLeft: Bool = false + ) { self.track = DefaultHorizontalValueTrack() self.thumb = DefaultThumb() self.thumbSize = thumbSize self.thumbInteractiveSize = thumbInteractiveSize self.options = options + self.isRightToLeft = isRightToLeft } } + public struct DefaultHorizontalValueTrack: View { public init() {} public var body: some View { diff --git a/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift b/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift index 755b78f..7b799b6 100644 --- a/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift +++ b/Sources/Sliders/ValueSlider/Styles/ValueSliderOptions.swift @@ -1,12 +1,13 @@ import SwiftUI +@MainActor public struct ValueSliderOptions: OptionSet { public let rawValue: Int public static let interactiveTrack = ValueSliderOptions(rawValue: 1 << 0) public static let defaultOptions: ValueSliderOptions = [] - public init(rawValue: Int) { + nonisolated public init(rawValue: Int) { self.rawValue = rawValue } } diff --git a/Sources/Sliders/ValueTrack/EnviromnentValues+ValueTrackConfiguration.swift b/Sources/Sliders/ValueTrack/EnviromnentValues+ValueTrackConfiguration.swift index 95b9a49..87910b7 100644 --- a/Sources/Sliders/ValueTrack/EnviromnentValues+ValueTrackConfiguration.swift +++ b/Sources/Sliders/ValueTrack/EnviromnentValues+ValueTrackConfiguration.swift @@ -11,6 +11,7 @@ public extension EnvironmentValues { } } -struct ValueTrackConfigurationKey: EnvironmentKey { +@MainActor +struct ValueTrackConfigurationKey: @preconcurrency EnvironmentKey { static let defaultValue: ValueTrackConfiguration = .defaultConfiguration } diff --git a/Sources/Sliders/ValueTrack/ValueTrackConfiguration.swift b/Sources/Sliders/ValueTrack/ValueTrackConfiguration.swift index 459a23e..408271c 100644 --- a/Sources/Sliders/ValueTrack/ValueTrackConfiguration.swift +++ b/Sources/Sliders/ValueTrack/ValueTrackConfiguration.swift @@ -1,5 +1,6 @@ import SwiftUI +@MainActor public struct ValueTrackConfiguration { public static let defaultConfiguration = ValueTrackConfiguration() diff --git a/Tests/SlidersTests/DistanceFromValueTests.swift b/Tests/SlidersTests/DistanceFromValueTests.swift index 6306ba8..8586bc4 100644 --- a/Tests/SlidersTests/DistanceFromValueTests.swift +++ b/Tests/SlidersTests/DistanceFromValueTests.swift @@ -18,6 +18,21 @@ class DistanceFromValueTests: XCTestCase { XCTAssert(fullDistance == 100) } + func testDistanceFromValueForRTLLanguage() { + + /// Zero value distance without offsets should be 0 + let zeroDistance = distanceFrom(value: 0.0, availableDistance: 100, isRightToLeft: true) + XCTAssert(zeroDistance == 100) + + /// Middle value distance without offsets should be half of overall length + let middleDistance = distanceFrom(value: 0.5, availableDistance: 100, isRightToLeft: true) + XCTAssert(middleDistance == 50) + + /// Largest value distance without offsets should be full overall length + let fullDistance = distanceFrom(value: 1.0, availableDistance: 100, isRightToLeft: true) + XCTAssert(fullDistance == 0) + } + func testDistanceFromValueWithNonUnitIntervalBounds() { /// Smallest value point distance without offsets should be 0 @@ -33,6 +48,21 @@ class DistanceFromValueTests: XCTestCase { XCTAssert(fullDistance == 100) } + func testDistanceFromValueWithNonUnitIntervalBoundsWitRTLLanguage() { + + /// Smallest value point distance without offsets should be 0 + let zeroDistance = distanceFrom(value: 0.25, availableDistance: 100, bounds: 0.25...1.25, isRightToLeft: true) + XCTAssert(zeroDistance == 100) + + /// Middle value distance without offsets should be half of overall length + let middleDistance = distanceFrom(value: 3.0, availableDistance: 100, bounds: 2.0...4.0, isRightToLeft: true) + XCTAssert(middleDistance == 50) + + /// Largest value distance without offsets should be full overall length + let fullDistance = distanceFrom(value: 1.0, availableDistance: 100, bounds: -1.0...1.0, isRightToLeft: true) + XCTAssert(fullDistance == 0) + } + func testDistanceFromValueWithOffsets() { /// Zero value distance with start offset 5 should be 5 @@ -48,6 +78,21 @@ class DistanceFromValueTests: XCTestCase { XCTAssert(fullDistance == 95) } + func testDistanceFromValueWithOffsetsForRTLLanguage() { + + /// Zero value distance with start offset 5 should be 5 + let zeroDistance = distanceFrom(value: 0.0, availableDistance: 100, leadingOffset: 5, trailingOffset: 0, isRightToLeft: true) + XCTAssert(zeroDistance == 100) + + /// Middle value distance with start and end offset of 10 should be half of overall length + let middleDistance = distanceFrom(value: 0.5, availableDistance: 100, leadingOffset: 10, trailingOffset: 10, isRightToLeft: true) + XCTAssert(middleDistance == 50) + + /// Largest value distance with end offset of 5 should be full overall length minus end offset + let fullDistance = distanceFrom(value: 1.0, availableDistance: 100, leadingOffset: 0, trailingOffset: 5, isRightToLeft: true) + XCTAssert(fullDistance == 0) + } + func testDistanceFromValueWithNonUnitIntervalBoundsWithOffsets() { /// Smallest value point distance with start offset 5 should be 5 @@ -79,5 +124,36 @@ class DistanceFromValueTests: XCTestCase { XCTAssert(fullDistance2 == 95) } + func testDistanceFromValueWithNonUnitIntervalBoundsWithOffsetsForRTLLanguage() { + + /// Smallest value point distance with start offset 5 should be 5 + let zeroDistance1 = distanceFrom(value: 0.25, availableDistance: 100, bounds: 0.25...1.25, leadingOffset: 5, trailingOffset: 0, isRightToLeft: true) + XCTAssert(zeroDistance1 == 100) + + /// Smallest value point distance with start offset 5 and end offset of 5 should be 5 + let zeroDistance2 = distanceFrom(value: 0.25, availableDistance: 100, bounds: 0.25...1.25, leadingOffset: 5, trailingOffset: 5, isRightToLeft: true) + XCTAssert(zeroDistance2 == 95) + + /// Middle value distance with equal offsets should be half of overall length + let middleDistance1 = distanceFrom(value: 3.0, availableDistance: 100, bounds: 2.0...4.0, leadingOffset: 10, trailingOffset: 10, isRightToLeft: true) + XCTAssert(middleDistance1 == 50) + + /// Middle value distance with different offsets should be half of overall length minus center point of these offsets + let middleDistance2 = distanceFrom(value: 3.0, availableDistance: 100, bounds: 2.0...4.0, leadingOffset: 13, trailingOffset: 7, isRightToLeft: true) + XCTAssert(middleDistance2 == 53) + + /// Middle value distance with different offsets should be half of overall length minus center point of these offsets + let middleDistance3 = distanceFrom(value: 3.0, availableDistance: 100, bounds: 2.0...4.0, leadingOffset: 2, trailingOffset: 18, isRightToLeft: true) + XCTAssert(middleDistance3 == 42) + + /// Largest value distance with end offset of 5 should be full overall length minus end offset + let fullDistance1 = distanceFrom(value: 1.0, availableDistance: 100, bounds: -1.0...1.0, leadingOffset: 0, trailingOffset: 5, isRightToLeft: true) + XCTAssert(fullDistance1 == 0) + + /// Largest value distance with both offsets of 5 should be full overall length minus end offset + let fullDistance2 = distanceFrom(value: 1.0, availableDistance: 100, bounds: -1.0...1.0, leadingOffset: 5, trailingOffset: 5, isRightToLeft: true) + XCTAssert(fullDistance2 == 5) + } + } diff --git a/Tests/SlidersTests/ValueFromDistanceTests.swift b/Tests/SlidersTests/ValueFromDistanceTests.swift index 74b6412..c37b86e 100644 --- a/Tests/SlidersTests/ValueFromDistanceTests.swift +++ b/Tests/SlidersTests/ValueFromDistanceTests.swift @@ -16,6 +16,19 @@ class ValueFromDistanceTests: XCTestCase { XCTAssert(fullValue == 1.0) } + func testValueFromDistanceForRTLanguage() { + let zeroValue = valueFrom(distance: 0.0, availableDistance: 100, isRightToLeft: true) + XCTAssert(zeroValue == 1.0) + + /// Unit interval value should be itself + let middleValue = valueFrom(distance: 50, availableDistance: 100, isRightToLeft: true) + XCTAssert(middleValue == 0.5) + + /// Unit interval value should be itself + let fullValue = valueFrom(distance: 100, availableDistance: 100, isRightToLeft: true) + XCTAssert(fullValue == 0.0) + } + func testValueFromDistanceWithBounds() { let zeroValue = valueFrom(distance: 0.0, availableDistance: 100, bounds: 0.25...1.25) XCTAssert(zeroValue == 0.25) @@ -27,7 +40,18 @@ class ValueFromDistanceTests: XCTestCase { XCTAssert(fullValue == 3.0) } - func ttestValueFromDistanceWithBoundsAndStep() { + func testValueFromDistanceWithBoundsWForRTLLanguage() { + let zeroValue = valueFrom(distance: 0.0, availableDistance: 100, bounds: 0.25...1.25, isRightToLeft: true) + XCTAssert(zeroValue == 1.25) + + let middleValue = valueFrom(distance: 50, availableDistance: 100, bounds: -1.0...1.0, isRightToLeft: true) + XCTAssert(middleValue == 0.0) + + let fullValue = valueFrom(distance: 100, availableDistance: 100, bounds: -3.0...3.0, isRightToLeft: true) + XCTAssert(fullValue == -3.0) + } + + func testValueFromDistanceWithBoundsAndStep() { let zeroValue = valueFrom(distance: 0.0, availableDistance: 100, bounds: 25...125, step: 10) XCTAssert(zeroValue == 30) @@ -38,6 +62,17 @@ class ValueFromDistanceTests: XCTestCase { XCTAssert(fullValue == 3.0) } + func testValueFromDistanceWithBoundsAndStepForRTLLanguage() { + let zeroValue = valueFrom(distance: 0.0, availableDistance: 100, bounds: 25...125, step: 10, isRightToLeft: true) + XCTAssert(zeroValue == 125) + + let middleValue = valueFrom(distance: 50, availableDistance: 100, bounds: -1.0...1.0, step: 1, isRightToLeft: true) + XCTAssert(middleValue == 0.0) + + let fullValue = valueFrom(distance: 100, availableDistance: 100, bounds: -3.0...3.0, step: 0.5, isRightToLeft: true) + XCTAssert(fullValue == -3.0) + } + func testValueFromDistanceWithOffsets() { let zeroValue = valueFrom(distance: 0.0, availableDistance: 100, leadingOffset: 0, trailingOffset: 0) XCTAssert(zeroValue == 0.0) @@ -57,4 +92,24 @@ class ValueFromDistanceTests: XCTestCase { let fullValueHigherThanLeadingOffset = valueFrom(distance: 100, availableDistance: 100, leadingOffset: 10, trailingOffset: 10) XCTAssert(fullValueHigherThanLeadingOffset == 1.0) } + + func testValueFromDistanceWithOffsetsForRTLLanguage() { + let zeroValue = valueFrom(distance: 0.0, availableDistance: 100, leadingOffset: 0, trailingOffset: 0, isRightToLeft: true) + XCTAssert(zeroValue == 1.0) + + let zeroValueWithLeadingOffset = valueFrom(distance: 10, availableDistance: 100, leadingOffset: 10, trailingOffset: 0, isRightToLeft: true) + XCTAssert(zeroValueWithLeadingOffset == 1.0) + + let fullValueWithTrailingOffset = valueFrom(distance: 90, availableDistance: 100, leadingOffset: 0, trailingOffset: 10, isRightToLeft: true) + XCTAssert(fullValueWithTrailingOffset == 0.0) + + let halfValueWithOffsets = valueFrom(distance: 50, availableDistance: 100, leadingOffset: 10, trailingOffset: 10, isRightToLeft: true) + XCTAssert(halfValueWithOffsets == 0.5) + + let zeroValueLowerThanLeadingOffset = valueFrom(distance: 0, availableDistance: 100, leadingOffset: 10, trailingOffset: 10, isRightToLeft: true) + XCTAssert(zeroValueLowerThanLeadingOffset == 1.0) + + let fullValueHigherThanLeadingOffset = valueFrom(distance: 100, availableDistance: 100, leadingOffset: 10, trailingOffset: 10, isRightToLeft: true) + XCTAssert(fullValueHigherThanLeadingOffset == 0.0) + } }