-
-
Notifications
You must be signed in to change notification settings - Fork 92
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 #1 from swift-extensions/styles
Experimental slider
- Loading branch information
Showing
11 changed files
with
310 additions
and
12 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
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
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,61 @@ | ||
import SwiftUI | ||
|
||
public struct ValueSlider<Track>: View where Track: View { | ||
@Environment(\.valueSliderStyle) private var style | ||
@State private var dragOffset: CGFloat? | ||
|
||
private var configuration: ValueSliderStyleConfiguration | ||
|
||
public var body: some View { | ||
self.style.makeBody(configuration: | ||
self.configuration.with(dragOffset: self.$dragOffset) | ||
) | ||
} | ||
} | ||
|
||
extension ValueSlider { | ||
init(_ configuration: ValueSliderStyleConfiguration) { | ||
self.configuration = configuration | ||
} | ||
} | ||
|
||
extension ValueSlider where Track == ValueSliderStyleConfiguration.Track { | ||
public init<V>(value: Binding<V>, in bounds: ClosedRange<V> = 0.0...1.0, step: V.Stride = 0.001, onEditingChanged: @escaping (Bool) -> Void = { _ in }) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint { | ||
|
||
self.init( | ||
ValueSliderStyleConfiguration( | ||
value: Binding(get: { CGFloat(value.wrappedValue) }, set: { value.wrappedValue = V($0) }), | ||
bounds: CGFloat(bounds.lowerBound)...CGFloat(bounds.upperBound), | ||
step: CGFloat(step), | ||
onEditingChanged: onEditingChanged, | ||
dragOffset: .constant(0), | ||
track: .init(view: DefaultHorizontalValueTrack(value: CGFloat(value.wrappedValue))), | ||
thumb: .init(view: DefaultThumb()) | ||
) | ||
) | ||
} | ||
} | ||
|
||
extension ValueSlider { | ||
public init<V>(value: Binding<V>, in bounds: ClosedRange<V> = 0.0...1.0, step: V.Stride = 0.001, track: Track, onEditingChanged: @escaping (Bool) -> Void = { _ in }) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint { | ||
|
||
self.init( | ||
ValueSliderStyleConfiguration( | ||
value: Binding(get: { CGFloat(value.wrappedValue) }, set: { value.wrappedValue = V($0) }), | ||
bounds: CGFloat(bounds.lowerBound)...CGFloat(bounds.upperBound), | ||
step: CGFloat(step), | ||
onEditingChanged: onEditingChanged, | ||
dragOffset: .constant(0), | ||
track: .init(view: track), | ||
thumb: .init(view: DefaultThumb()) | ||
) | ||
) | ||
} | ||
} | ||
|
||
struct ValueSlider_Previews: PreviewProvider { | ||
static var previews: some View { | ||
ValueSlider(value: .constant(0.3)) | ||
.previewLayout(.fixed(width: 300, height: 100)) | ||
} | ||
} |
105 changes: 105 additions & 0 deletions
105
Sources/Sliders/ValueSlider/ValueSliderStyles/HorizontalValueSliderStyle.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,105 @@ | ||
import SwiftUI | ||
|
||
public struct HorizontalValueSliderStyle<Track: View>: ValueSliderStyle { | ||
private let track: (CGFloat) -> Track | ||
private let thumbSize: CGSize | ||
private let thumbInteractiveSize: CGSize | ||
private let options: HorizontalValueSliderOptions | ||
|
||
public func makeBody(configuration: Self.Configuration) -> some View { | ||
GeometryReader { geometry in | ||
ZStack(alignment: .leading) { | ||
if self.options.contains(.interactiveTrack) { | ||
self.track(configuration.value.wrappedValue) | ||
.gesture( | ||
DragGesture(minimumDistance: 0) | ||
.onChanged { gestureValue in | ||
let computedValue = valueFrom( | ||
distance: gestureValue.location.x, | ||
availableDistance: geometry.size.width, | ||
bounds: configuration.bounds, | ||
step: configuration.step, | ||
leadingOffset: self.thumbSize.width / 2, | ||
trailingOffset: self.thumbSize.width / 2 | ||
) | ||
configuration.value.wrappedValue = computedValue | ||
configuration.onEditingChanged(true) | ||
} | ||
.onEnded { _ in | ||
configuration.onEditingChanged(false) | ||
} | ||
) | ||
} else { | ||
self.track(configuration.value.wrappedValue) | ||
} | ||
|
||
ZStack { | ||
configuration.thumb | ||
.frame(width: self.thumbSize.width, height: self.thumbSize.height) | ||
} | ||
.frame(minWidth: self.thumbInteractiveSize.width, minHeight: self.thumbInteractiveSize.height) | ||
|
||
.position( | ||
x: distanceFrom( | ||
value: configuration.value.wrappedValue, | ||
availableDistance: geometry.size.width, | ||
bounds: configuration.bounds, | ||
leadingOffset: self.thumbSize.width / 2, | ||
trailingOffset: self.thumbSize.width / 2 | ||
), | ||
y: geometry.size.height / 2 | ||
) | ||
.gesture( | ||
DragGesture() | ||
.onChanged { gestureValue in | ||
if configuration.dragOffset.wrappedValue == nil { | ||
configuration.dragOffset.wrappedValue = gestureValue.startLocation.x - distanceFrom( | ||
value: configuration.value.wrappedValue, | ||
availableDistance: geometry.size.width, | ||
bounds: configuration.bounds, | ||
leadingOffset: self.thumbSize.width / 2, | ||
trailingOffset: self.thumbSize.width / 2 | ||
) | ||
} | ||
|
||
let computedValue = valueFrom( | ||
distance: gestureValue.location.x - (configuration.dragOffset.wrappedValue ?? 0), | ||
availableDistance: geometry.size.width, | ||
bounds: configuration.bounds, | ||
step: configuration.step, | ||
leadingOffset: self.thumbSize.width / 2, | ||
trailingOffset: self.thumbSize.width / 2 | ||
) | ||
|
||
configuration.value.wrappedValue = computedValue | ||
configuration.onEditingChanged(true) | ||
} | ||
.onEnded { _ in | ||
configuration.dragOffset.wrappedValue = nil | ||
configuration.onEditingChanged(false) | ||
} | ||
) | ||
} | ||
|
||
} | ||
.frame(minHeight: self.thumbInteractiveSize.height) | ||
} | ||
|
||
public init(@ViewBuilder track: @escaping (CGFloat) -> Track, thumbSize: CGSize = CGSize(width: 32, height: 32), thumbInteractiveSize: CGSize = CGSize(width: 44, height: 44), options: HorizontalValueSliderOptions = .defaultOptions) { | ||
self.track = track | ||
self.thumbSize = thumbSize | ||
self.thumbInteractiveSize = thumbInteractiveSize | ||
self.options = options | ||
} | ||
} | ||
|
||
public struct HorizontalValueSliderOptions: OptionSet { | ||
public let rawValue: Int | ||
|
||
public static let interactiveTrack = HorizontalValueSliderOptions(rawValue: 1 << 0) | ||
public static let defaultOptions: HorizontalValueSliderOptions = [] | ||
|
||
public init(rawValue: Int) { | ||
self.rawValue = rawValue | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
Sources/Sliders/ValueSlider/ValueSliderStyles/ValueSliderStyle/AnyValueSliderStyle.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 @@ | ||
import SwiftUI | ||
|
||
struct AnyValueSliderStyle: ValueSliderStyle { | ||
private let styleMakeBody: (ValueSliderStyle.Configuration) -> AnyView | ||
|
||
init<S: ValueSliderStyle>(_ style: S) { | ||
self.styleMakeBody = style.makeTypeErasedBody | ||
} | ||
|
||
func makeBody(configuration: ValueSliderStyle.Configuration) -> AnyView { | ||
self.styleMakeBody(configuration) | ||
} | ||
} | ||
|
||
fileprivate extension ValueSliderStyle { | ||
func makeTypeErasedBody(configuration: ValueSliderStyle.Configuration) -> AnyView { | ||
AnyView(makeBody(configuration: configuration)) | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
...s/ValueSlider/ValueSliderStyles/ValueSliderStyle/EnvironmentValues+ValueSliderStyle.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,12 @@ | ||
import SwiftUI | ||
|
||
extension EnvironmentValues { | ||
var valueSliderStyle: AnyValueSliderStyle { | ||
get { | ||
return self[ValueSliderStyleKey.self] | ||
} | ||
set { | ||
self[ValueSliderStyleKey.self] = newValue | ||
} | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
Sources/Sliders/ValueSlider/ValueSliderStyles/ValueSliderStyle/ValueSliderStyle.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,24 @@ | ||
import SwiftUI | ||
|
||
/// Defines the implementation of all `ValueSlider` instances within a view | ||
/// hierarchy. | ||
/// | ||
/// To configure the current `ValueSlider` for a view hiearchy, use the | ||
/// `.valueSliderStyle()` modifier. | ||
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) | ||
public protocol ValueSliderStyle { | ||
/// A `View` representing the body of a `ValueSlider`. | ||
associatedtype Body : View | ||
|
||
/// Creates a `View` representing the body of a `ValueSlider`. | ||
/// | ||
/// - Parameter configuration: The properties of the value slider instance being | ||
/// created. | ||
/// | ||
/// This method will be called for each instance of `ValueSlider` created within | ||
/// a view hierarchy where this style is the current `ValueSliderStyle`. | ||
func makeBody(configuration: Self.Configuration) -> Self.Body | ||
|
||
/// The properties of a `ValueSlider` instance being created. | ||
typealias Configuration = ValueSliderStyleConfiguration | ||
} |
45 changes: 45 additions & 0 deletions
45
...liders/ValueSlider/ValueSliderStyles/ValueSliderStyle/ValueSliderStyleConfiguration.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,45 @@ | ||
import SwiftUI | ||
|
||
public struct ValueSliderStyleConfiguration { | ||
public let value: Binding<CGFloat> | ||
public let bounds: ClosedRange<CGFloat> | ||
public let step: CGFloat | ||
public let onEditingChanged: (Bool) -> Void | ||
public var dragOffset: Binding<CGFloat?> | ||
public let track: ValueSliderStyleConfiguration.Track | ||
public let thumb: ValueSliderStyleConfiguration.Thumb | ||
|
||
func with(dragOffset: Binding<CGFloat?>) -> Self { | ||
var mutSelf = self | ||
mutSelf.dragOffset = dragOffset | ||
return mutSelf | ||
} | ||
} | ||
|
||
public extension ValueSliderStyleConfiguration { | ||
struct Track: View { | ||
let typeErasedTrack: AnyView | ||
|
||
init<T: View>(view: T) { | ||
self.typeErasedTrack = AnyView(view) | ||
} | ||
|
||
public var body: some View { | ||
self.typeErasedTrack | ||
} | ||
} | ||
} | ||
|
||
public extension ValueSliderStyleConfiguration { | ||
struct Thumb: View { | ||
let typeErasedThumb: AnyView | ||
|
||
init<T: View>(view: T) { | ||
self.typeErasedThumb = AnyView(view) | ||
} | ||
|
||
public var body: some View { | ||
self.typeErasedThumb | ||
} | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
Sources/Sliders/ValueSlider/ValueSliderStyles/ValueSliderStyle/ValueSliderStyleKey.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,5 @@ | ||
import SwiftUI | ||
|
||
struct ValueSliderStyleKey: EnvironmentKey { | ||
static let defaultValue: AnyValueSliderStyle = AnyValueSliderStyle(HorizontalValueSliderStyle(track: { _ in Capsule() })) | ||
} |
8 changes: 8 additions & 0 deletions
8
Sources/Sliders/ValueSlider/ValueSliderStyles/ValueSliderStyle/View+ValueSliderStyle.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,8 @@ | ||
import SwiftUI | ||
|
||
extension View { | ||
/// Sets the style for `ValueSlider` within the environment of `self`. | ||
public func valueSliderStyle<S>(_ style: S) -> some View where S : ValueSliderStyle { | ||
self.environment(\.valueSliderStyle, AnyValueSliderStyle(style)) | ||
} | ||
} |
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,8 @@ | ||
// | ||
// File.swift | ||
// | ||
// | ||
// Created by Alex on 2019-11-07. | ||
// | ||
|
||
import Foundation |