Skip to content

Commit 376c511

Browse files
committed
Initial commit
0 parents  commit 376c511

File tree

4 files changed

+318
-0
lines changed

4 files changed

+318
-0
lines changed

.gitignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
## OS X
2+
.DS_Store
3+
4+
## User settings
5+
xcuserdata/
6+
7+
## Obj-C/Swift specific
8+
*.hmap
9+
10+
## App packaging
11+
*.ipa
12+
*.dSYM.zip
13+
*.dSYM
14+
15+
## Playgrounds
16+
timeline.xctimeline
17+
playground.xcworkspace
18+
19+
## Swift Package Manager
20+
.build/
21+
22+
## CocoaPods
23+
Pods/
24+
25+
## Carthage
26+
Carthage/Checkouts
27+
Carthage/Build/

Package.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// swift-tools-version:5.5
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "SliderKit",
7+
products: [
8+
.library(
9+
name: "SliderKit",
10+
targets: ["SliderKit"]),
11+
],
12+
targets: [
13+
.target(
14+
name: "SliderKit",
15+
dependencies: [])
16+
]
17+
)

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Installation
2+
3+
SliderKit is available through [Swift Package Manager](https://www.swift.org/package-manager/)
4+
5+
# Usage
6+
7+
### Initializing
8+
```swift
9+
let data = SliderData(
10+
externalRange: [
11+
50...79,
12+
80...120,
13+
121...200
14+
],
15+
internalRange: [
16+
0...199,
17+
200...800,
18+
801...1000
19+
],
20+
thumbImages: [
21+
50...64: UIImage(named: "red_thumb")!,
22+
65...79: UIImage(named: "orange_thumb")!,
23+
80...120: UIImage(named: "blue_thumb")!,
24+
121...159: UIImage(named: "orange_thumb")!,
25+
160...200: UIImage(named: "red_thumb")!
26+
]
27+
)
28+
var slider = ScaledSlider(data: data)
29+
slider.tracklineImage = UIImage(named: "track_layer")
30+
slider.debouncesIncrementChanges = true
31+
slider.debouncingDuration = 0.45
32+
```
33+
34+
### Updating slider value
35+
```swift
36+
slider.update(sliderValue: value)
37+
```
38+
39+
#### OR
40+
```swift
41+
slider.changeValue(by: 5)
42+
```
43+
44+
### Callback listeners
45+
```swift
46+
slider.onValueChanged = { value in
47+
print(value)
48+
}
49+
50+
slider.onValueUpdated = { value in
51+
print(value)
52+
}
53+
```
54+
# License
55+
SliderKit is available under the MIT license. See the LICENSE file for more information.

Sources/SliderKit/ScaledSlider.swift

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
//
2+
// ScaledSlider.swift
3+
//
4+
// MIT License
5+
//
6+
// Copyright (c) 2022 Tigran Gishyan
7+
//
8+
// Permission is hereby granted, free of charge, to any person obtaining a copy
9+
// of this software and associated documentation files (the "Software"), to deal
10+
// in the Software without restriction, including without limitation the rights
11+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
// copies of the Software, and to permit persons to whom the Software is
13+
// furnished to do so, subject to the following conditions:
14+
//
15+
// The above copyright notice and this permission notice shall be included in all
16+
// copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
// SOFTWARE.
25+
26+
import UIKit
27+
28+
public struct SliderData {
29+
30+
/// Range to be shown in user's side
31+
public var externalRange: [ClosedRange<Int>]
32+
33+
/// Range that should be used for internal calculations only
34+
public var internalRange: [ClosedRange<Int>]
35+
36+
/// Slider thumb images for each range
37+
/// The default value is nil
38+
public var thumbImages: [ClosedRange<Int>: UIImage]?
39+
40+
public init(
41+
externalRange: [ClosedRange<Int>],
42+
internalRange: [ClosedRange<Int>],
43+
thumbImages: [ClosedRange<Int>: UIImage]? = nil
44+
) {
45+
self.externalRange = externalRange
46+
self.internalRange = internalRange
47+
self.thumbImages = thumbImages
48+
}
49+
}
50+
51+
/// Custom subtype of slider with non-linear behaviour. The whole track layer could be divided
52+
/// into several small parts with it's own density of points
53+
open class ScaledSlider: UISlider {
54+
55+
/// Indicator that shows does `changeValue` function needs to be debounced
56+
open var debouncesIncrementChanges: Bool = true
57+
58+
/// Debouncing duration. Default value is 0.35
59+
open var debouncingDuration: Double = 0.35
60+
var valueUpdateWorkItem: DispatchWorkItem?
61+
62+
/// Slider thumb's position value
63+
open var externalValue: Int = 0
64+
65+
/// Trackline custom image
66+
/// Default value is nil
67+
open var tracklineImage: UIImage? {
68+
get { minimumTrackImage(for: .normal) }
69+
set {
70+
setMinimumTrackImage(newValue, for: .normal)
71+
setMaximumTrackImage(newValue, for: .normal)
72+
}
73+
}
74+
75+
/// Callback function
76+
open var onValueChanged: ((Int) -> Void)?
77+
78+
/// Callback function
79+
open var onValueUpdated: ((Int) -> Void)?
80+
81+
/// Initial slider data passed with initializer
82+
var data: SliderData
83+
84+
public init(data: SliderData) {
85+
self.data = data
86+
87+
super.init(frame: .zero)
88+
commonInit()
89+
}
90+
91+
required public init?(coder: NSCoder) {
92+
fatalError("init(coder:) has not been implemented")
93+
}
94+
95+
func commonInit() {
96+
maximumValue = 1000
97+
98+
addTarget(self, action: #selector(valueChanged), for: .valueChanged)
99+
addTarget(self, action: #selector(valueUpdated), for: .touchCancel)
100+
addTarget(self, action: #selector(valueUpdated), for: .touchUpInside)
101+
addTarget(self, action: #selector(valueUpdated), for: .touchUpOutside)
102+
103+
check(data: data)
104+
}
105+
106+
/// Check data to meet conditions written in assert functions
107+
func check(data: SliderData) {
108+
109+
let externalRangeBounds = data.externalRange.endIndex - data.externalRange.startIndex
110+
let internalRangeBounds = data.internalRange.endIndex - data.internalRange.startIndex
111+
112+
assert(
113+
externalRangeBounds == internalRangeBounds,
114+
"External range should have the same size as internal for appropriate mapping"
115+
)
116+
117+
let lastValue = data.internalRange.last!.upperBound
118+
assert(
119+
lastValue == Int(maximumValue),
120+
"Slider maximum value should be same as internal range last value"
121+
)
122+
}
123+
124+
/// Updates thumb position eather by calling this function or by sliding thumb in trackline.
125+
/// - Parameter sliderValue: External slider value that should be transformed to internal thumb position
126+
open func update(sliderValue: Int) {
127+
externalValue = sliderValue
128+
129+
if let thumbImages = data.thumbImages {
130+
for (range, image) in thumbImages {
131+
if range ~= sliderValue {
132+
setThumbImage(image, for: .normal)
133+
break
134+
}
135+
}
136+
}
137+
138+
for (index, range) in data.externalRange.enumerated() {
139+
let internalValue = map(
140+
range: range,
141+
domain: data.internalRange[index],
142+
value: externalValue
143+
)
144+
145+
if internalValue > -1 {
146+
onValueChanged?(Int(externalValue))
147+
value = Float(internalValue)
148+
break
149+
}
150+
}
151+
}
152+
153+
/// Changes thumb's position by given amount of value
154+
/// - Parameters:
155+
/// - amount: Specified amount of points
156+
open func changeValue(by amount: Int) {
157+
158+
let newValue = value + Float(amount)
159+
if newValue > minimumValue && newValue < maximumValue {
160+
update(sliderValue: externalValue + amount)
161+
}
162+
163+
onValueChanged?(Int(externalValue))
164+
updateWorkItem()
165+
}
166+
167+
/// Transformation function that changes internal range to external and vice versa.
168+
/// - Parameters:
169+
/// - range: Range that should be transformed
170+
/// - domain: Range into which transformation function should be done
171+
/// - value: Current position of thumb in external or internal coordinate system
172+
/// - Returns: Transformed current position of thumb in external or internal coordinate system
173+
func map(range: ClosedRange<Int>, domain: ClosedRange<Int>, value: Int) -> Int {
174+
if range ~= value {
175+
return domain.lowerBound +
176+
(domain.upperBound - domain.lowerBound) *
177+
(value - range.lowerBound) /
178+
(range.upperBound - range.lowerBound)
179+
} else {
180+
return -1
181+
}
182+
}
183+
184+
/// Creates `DispatchWorkItem` if it doesn't created yet and debouncing `onValueUpdated` function's calling by given amount of time.
185+
func updateWorkItem() {
186+
if debouncesIncrementChanges {
187+
valueUpdateWorkItem?.cancel()
188+
valueUpdateWorkItem = DispatchWorkItem { [weak self] in
189+
guard let self = self else { return }
190+
191+
self.onValueUpdated?(Int(self.externalValue))
192+
}
193+
194+
DispatchQueue.main.asyncAfter(
195+
deadline: .now() + debouncingDuration,
196+
execute: valueUpdateWorkItem!
197+
)
198+
} else {
199+
onValueUpdated?(Int(externalValue))
200+
}
201+
}
202+
203+
@objc func valueChanged() {
204+
for (index, range) in data.internalRange.enumerated() {
205+
let val = map(
206+
range: range,
207+
domain: data.externalRange[index], value: Int(value)
208+
)
209+
if val > -1 {
210+
update(sliderValue: val)
211+
break
212+
}
213+
}
214+
}
215+
216+
@objc func valueUpdated() {
217+
onValueUpdated?(Int(externalValue))
218+
}
219+
}

0 commit comments

Comments
 (0)