4 |
5 | [](http://cocoadocs.org/docsets/SnappingStepper/) [](http://cocoadocs.org/docsets/SnappingStepper/) [](http://cocoadocs.org/docsets/SnappingStepper/) [](https://github.com/Carthage/Carthage)
6 | [](https://travis-ci.org/yannickl/SnappingStepper) [](http://codecov.io/github/yannickl/SnappingStepper?branch=master) [](https://codebeat.co/projects/github-com-yannickl-snappingstepper)
7 |
8 | An elegant alternative to the `UIStepper` in Swift with a thumb slider addition to control the value update with more flexibility.
9 |
10 |
11 |
12 |
13 |
14 | # Usage
15 |
16 | ```swift
17 | let stepper = SnappingStepper(frame: CGRect(x: 0, y: 0, width: 100, height: 40))
18 |
19 | // Configure the stepper like any other UIStepper. For example:
20 | //
21 | // stepper.continuous = true
22 | // stepper.autorepeat = true
23 | // stepper.wraps = false
24 | // stepper.minimumValue = 0
25 | // stepper.maximumValue = 100
26 | // stepper.stepValue = 1
27 |
28 | stepper.symbolFont = UIFont(name: "TrebuchetMS-Bold", size: 20)
29 | stepper.symbolFontColor = .black
30 | stepper.backgroundColor = UIColor(hex: 0xc0392b)
31 | stepper.thumbWidthRatio = 0.5
32 | stepper.thumbText = ""
33 | stepper.thumbFont = UIFont(name: "TrebuchetMS-Bold", size: 20)
34 | stepper.thumbBackgroundColor = UIColor(hex: 0xe74c3c)
35 | stepper.thumbTextColor = .black
36 |
37 | stepper.addTarget(self, action: "valueChanged:", forControlEvents: .valueChanged)
38 |
39 | // If you don't want using the traditional `addTarget:action:` pattern you can use
40 | // the `valueChangedBlock`
41 | // snappingStepper.valueChangeBlock = { (value: Double) in
42 | // println("value: \(value)")
43 | // }
44 |
45 | func valueChanged(sender: AnyObject) {
46 | // Retrieve the value: stepper.value
47 | }
48 | ```
49 |
50 | To go further, take a look at the example project.
51 |
52 | # Installation
53 |
54 | ## CocoaPods
55 |
56 | Install CocoaPods if not already available:
57 |
58 | ``` bash
59 | $ [sudo] gem install cocoapods
60 | $ pod setup
61 | ```
62 | Go to the directory of your Xcode project, and Create and Edit your Podfile and add _SnappingStepper_:
63 |
64 | ``` bash
65 | $ cd /path/to/MyProject
66 | $ touch Podfile
67 | $ edit Podfile
68 | source 'https://github.com/CocoaPods/Specs.git'
69 | platform :ios, '8.0'
70 |
71 | use_frameworks!
72 | pod 'SnappingStepper', '~> 3.0.0'
73 | ```
74 |
75 | Install into your project:
76 |
77 | ``` bash
78 | $ pod install
79 | ```
80 |
81 | Open your project in Xcode from the .xcworkspace file (not the usual project file):
82 |
83 | ``` bash
84 | $ open MyProject.xcworkspace
85 | ```
86 |
87 | You can now `import SnappingStepper` framework into your files.
88 |
89 | ## Carthage
90 |
91 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that automates the process of adding frameworks to your Cocoa application.
92 |
93 | You can install Carthage with [Homebrew](http://brew.sh/) using the following command:
94 |
95 | ```bash
96 | $ brew update
97 | $ brew install carthage
98 | ```
99 |
100 | To integrate `SnappingStepper` into your Xcode project using Carthage, specify it in your `Cartfile` file:
101 |
102 | ```ogdl
103 | github "yannickl/SnappingStepper" >= 3.0.0
104 | ```
105 |
106 | ## Swift Package Manager
107 | You can use [The Swift Package Manager](https://swift.org/package-manager) to install `SnappingStepper` by adding the proper description to your `Package.swift` file:
108 |
109 | ```swift
110 | import PackageDescription
111 |
112 | let package = Package(
113 | name: "YOUR_PROJECT_NAME",
114 | targets: [],
115 | dependencies: [
116 | .Package(url: "https://github.com/yannickl/SnappingStepper.git", versions: "3.0.0" ..< Version.max)
117 | ]
118 | )
119 | ```
120 |
121 | Note that the [Swift Package Manager](https://swift.org/package-manager) (SPM) is still in early design and development, for more infomation checkout its [GitHub Page](https://github.com/apple/swift-package-manager).
122 |
123 | ## Manually
124 |
125 | [Download](https://github.com/YannickL/SnappingStepper/archive/master.zip) the project and copy the `SnappingStepper` folder into your project to use it in.
126 |
127 | ## Contribution
128 |
129 | Contributions are welcomed and encouraged *♡*.
130 |
131 | # Contact
132 |
133 | Yannick Loriot
134 | - [https://21.co/yannickl/](https://21.co/yannickl/)
135 | - [https://twitter.com/yannickloriot](https://twitter.com/yannickloriot)
136 |
137 | # License (MIT)
138 |
139 | Copyright (c) 2015-present - Yannick Loriot
140 |
141 | Permission is hereby granted, free of charge, to any person obtaining a copy
142 | of this software and associated documentation files (the "Software"), to deal
143 | in the Software without restriction, including without limitation the rights
144 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
145 | copies of the Software, and to permit persons to whom the Software is
146 | furnished to do so, subject to the following conditions:
147 |
148 | The above copyright notice and this permission notice shall be included in
149 | all copies or substantial portions of the Software.
150 |
151 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
152 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
153 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
154 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
155 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
156 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
157 | THE SOFTWARE.
158 |
--------------------------------------------------------------------------------
/SnappingStepper.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'SnappingStepper'
3 | s.version = '3.0.0'
4 | s.license = 'MIT'
5 | s.summary = 'An elegant alternative to the UIStepper written in Swift'
6 | s.homepage = 'https://github.com/yannickl/SnappingStepper.git'
7 | s.social_media_url = 'https://twitter.com/yannickloriot'
8 | s.authors = { 'Yannick Loriot' => 'contact@yannickloriot.com' }
9 | s.source = { :git => 'https://github.com/yannickl/SnappingStepper.git', :tag => s.version }
10 | s.screenshot = 'http://yannickloriot.com/resources/snappingstepper-screenshot.png'
11 | s.swift_version = '4.2'
12 |
13 | s.ios.deployment_target = '8.0'
14 |
15 | s.dependency 'DynamicColor', '~> 4.0'
16 |
17 | s.framework = 'UIKit'
18 | s.source_files = 'Sources/*.swift'
19 | s.requires_arc = true
20 | end
21 |
--------------------------------------------------------------------------------
/Sources/AutoRepeatHelper.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * SnappingStepper
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | import Foundation
28 |
29 | final class AutoRepeatHelper {
30 | private var timer: Timer?
31 | private var autorepeatCount = 0
32 | private var tickBlock: (() -> Void)?
33 |
34 | deinit {
35 | stop()
36 | }
37 |
38 | // MARK: - Managing Autorepeat
39 |
40 | func stop() {
41 | timer?.invalidate()
42 | }
43 |
44 | func start(autorepeatCount count: Int = 0, tickBlock block: @escaping () -> Void) {
45 | // if let _timer = timer where _timer.valid {
46 | // return
47 | // }
48 |
49 | autorepeatCount = count
50 | tickBlock = block
51 |
52 | repeatTick(sender: nil)
53 |
54 | let newTimer = Timer(timeInterval: 0.1, target: self, selector: #selector(AutoRepeatHelper.repeatTick), userInfo: nil, repeats: true)
55 | timer = newTimer
56 |
57 | RunLoop.current.add(newTimer, forMode: .common)
58 | }
59 |
60 | @objc func repeatTick(sender: AnyObject?) {
61 | let needsIncrement: Bool
62 |
63 | if autorepeatCount < 35 {
64 | if autorepeatCount < 10 {
65 | needsIncrement = autorepeatCount % 5 == 0
66 | }
67 | else if autorepeatCount < 20 {
68 | needsIncrement = autorepeatCount % 4 == 0
69 | }
70 | else if autorepeatCount < 25 {
71 | needsIncrement = autorepeatCount % 3 == 0
72 | }
73 | else if autorepeatCount < 30 {
74 | needsIncrement = autorepeatCount % 2 == 0
75 | }
76 | else {
77 | needsIncrement = autorepeatCount % 1 == 0
78 | }
79 |
80 | autorepeatCount += 1
81 | }
82 | else {
83 | needsIncrement = true
84 | }
85 |
86 | if needsIncrement {
87 | tickBlock?()
88 | }
89 | }
90 | }
91 |
92 |
--------------------------------------------------------------------------------
/Sources/CustomShapeLayer.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * CustomShapeLayer
3 | * Created by Martin Rehder.
4 | *
5 | * SnappingStepper
6 | *
7 | * Copyright 2015-present Yannick Loriot.
8 | * http://yannickloriot.com
9 | *
10 | * Permission is hereby granted, free of charge, to any person obtaining a copy
11 | * of this software and associated documentation files (the "Software"), to deal
12 | * in the Software without restriction, including without limitation the rights
13 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | * copies of the Software, and to permit persons to whom the Software is
15 | * furnished to do so, subject to the following conditions:
16 | *
17 | * The above copyright notice and this permission notice shall be included in
18 | * all copies or substantial portions of the Software.
19 | *
20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 | * THE SOFTWARE.
27 | *
28 | */
29 |
30 | import UIKit
31 |
32 | final class CustomShapeLayer {
33 | static func createShape(style: ShapeStyle, bounds: CGRect, color: UIColor) -> CAShapeLayer {
34 | let shape = CAShapeLayer()
35 |
36 | let path = CustomShapeLayer.shapePathForStyle(style: style, bounds: bounds)
37 | shape.path = path.cgPath
38 | shape.fillColor = color.cgColor
39 |
40 | return shape
41 | }
42 |
43 | static func createShape(style: ShapeStyle, bounds: CGRect, color: UIColor, borderColor: UIColor, borderWidth: CGFloat) -> CAShapeLayer {
44 | let shape = CAShapeLayer()
45 |
46 | let path = CustomShapeLayer.shapePathForStyle(style: style, bounds: bounds)
47 | shape.path = path.cgPath
48 | shape.fillColor = color.cgColor
49 | shape.strokeColor = borderColor.cgColor
50 | shape.lineWidth = borderWidth
51 |
52 | return shape
53 | }
54 |
55 | static func shapePathForStyle(style: ShapeStyle, bounds: CGRect) -> UIBezierPath {
56 | var path = UIBezierPath()
57 |
58 | switch style {
59 | case .box, .none:
60 | path = UIBezierPath(rect: bounds)
61 | case .rounded:
62 | path = UIBezierPath(roundedRect: bounds, cornerRadius: max(1.0, min(bounds.size.width, bounds.size.height) * 0.2))
63 | case .roundedFixed(let cornerRadius):
64 | path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
65 | case .thumb:
66 | let s = min(bounds.size.width, bounds.size.height)
67 | let xOff = (bounds.size.width - s) * 0.5
68 | path = UIBezierPath(ovalIn: CGRect(x: bounds.origin.x + xOff, y: bounds.origin.y, width: s, height: s))
69 | case .tube:
70 | path = UIBezierPath(roundedRect: bounds, cornerRadius: max(1.0, min(bounds.size.width, bounds.size.height) * 0.5))
71 | case .custom(let cpath):
72 | path = CustomShapeLayer.getScaledPath(path: cpath.cgPath, size: bounds.size)
73 | }
74 |
75 | return path
76 | }
77 |
78 | static func getScaledPath(path: CGPath, size: CGSize) -> UIBezierPath {
79 | let rect = CGRect(origin:CGPoint(x:0, y:0), size:CGSize(width: size.width, height: size.height))
80 | let boundingBox = path.boundingBox
81 |
82 | let scaleFactorX = rect.width / boundingBox.width
83 | let scaleFactorY = rect.height / boundingBox.height
84 |
85 | var scaleTransform = CGAffineTransform.identity
86 | scaleTransform = scaleTransform.scaledBy(x: scaleFactorX, y: scaleFactorY)
87 | scaleTransform = scaleTransform.translatedBy(x: -boundingBox.minX, y: -boundingBox.minY)
88 |
89 |
90 | let scaledSize = boundingBox.size.applying(CGAffineTransform(scaleX: scaleFactorX, y: scaleFactorY))
91 | let centerOffset = CGSize(width: (rect.width - scaledSize.width) / (scaleFactorX * 2.0), height:(rect.height - scaledSize.height) / (scaleFactorY * 2.0))
92 | scaleTransform = scaleTransform.translatedBy(x: centerOffset.width, y: centerOffset.height)
93 |
94 | let scaledPath = path.copy(using: &scaleTransform)!
95 |
96 | return UIBezierPath(cgPath: scaledPath)
97 | }
98 |
99 | static func createHintShapeLayer(label: StyledLabel, fillColor: CGColor?) {
100 | let shape = CAShapeLayer()
101 | let cp1 = CGPoint(x: label.bounds.width * 0.35, y: label.bounds.height)
102 | let cp2 = CGPoint(x: label.bounds.width * 0.65, y: label.bounds.height)
103 | let cpc = CGPoint(x: label.bounds.width / 2.0, y: label.bounds.height * 1.25)
104 | let sp = CGPoint(x: label.bounds.width / 2.0, y: label.bounds.height * 1.5)
105 |
106 | let myBezier = UIBezierPath()
107 | myBezier.move(to: sp)
108 | myBezier.addCurve(to: CGPoint(x: label.bounds.width * 0.2, y: label.bounds.height), controlPoint1: cpc, controlPoint2: cp1)
109 | myBezier.addLine(to: CGPoint(x: label.bounds.width * 0.8, y: label.bounds.height))
110 | myBezier.addCurve(to: sp, controlPoint1: cp2, controlPoint2: cpc)
111 | myBezier.close()
112 |
113 | shape.path = myBezier.cgPath
114 | shape.fillColor = fillColor
115 |
116 | label.layer.addSublayer(shape)
117 | }
118 | }
119 |
120 |
--------------------------------------------------------------------------------
/Sources/ShapeStyle.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * ShapeStyle
3 | * Created by Martin Rehder.
4 | *
5 | * SnappingStepper
6 | *
7 | * Copyright 2015-present Yannick Loriot.
8 | * http://yannickloriot.com
9 | *
10 | * Permission is hereby granted, free of charge, to any person obtaining a copy
11 | * of this software and associated documentation files (the "Software"), to deal
12 | * in the Software without restriction, including without limitation the rights
13 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | * copies of the Software, and to permit persons to whom the Software is
15 | * furnished to do so, subject to the following conditions:
16 | *
17 | * The above copyright notice and this permission notice shall be included in
18 | * all copies or substantial portions of the Software.
19 | *
20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 | * THE SOFTWARE.
27 | *
28 | */
29 |
30 | import UIKit
31 |
32 | /// Specifies the shape style of the snapping stepper.
33 | public enum ShapeStyle {
34 | /// No shape
35 | case none
36 | /// A box shape.
37 | case box
38 | /// A round shape.
39 | case rounded
40 | /// A round shape with given corner radius.
41 | case roundedFixed(cornerRadius: CGFloat)
42 | /// A thumb shape.
43 | case thumb
44 | /// A tube shape.
45 | case tube
46 | /// A custom shape.
47 | case custom(path: UIBezierPath)
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/SnappingStepper+Internal.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * SnappingStepper
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | import UIKit
28 |
29 | extension SnappingStepper {
30 | // MARK: - Managing the Components
31 |
32 | func initComponents() {
33 | self.layer.addSublayer(styleLayer)
34 |
35 | hintLabel.font = thumbFont
36 | hintLabel.textColor = thumbTextColor
37 |
38 | minusSymbolLabel.text = "−"
39 | minusSymbolLabel.font = symbolFont
40 | minusSymbolLabel.textColor = symbolFontColor
41 |
42 | addSubview(minusSymbolLabel)
43 |
44 | plusSymbolLabel.text = "+"
45 | plusSymbolLabel.font = symbolFont
46 | plusSymbolLabel.textColor = symbolFontColor
47 | addSubview(plusSymbolLabel)
48 |
49 | thumbLabel.font = thumbFont
50 | thumbLabel.textColor = thumbTextColor
51 | addSubview(thumbLabel)
52 | }
53 |
54 | func setupGestures() {
55 | let panGesture = UIPanGestureRecognizer(target: self, action: #selector(SnappingStepper.sliderPanned))
56 | thumbLabel.addGestureRecognizer(panGesture)
57 |
58 | let touchGesture = UITouchGestureRecognizer(target: self, action: #selector(SnappingStepper.stepperTouched))
59 | touchGesture.require(toFail: panGesture)
60 | addGestureRecognizer(touchGesture)
61 | }
62 |
63 | func layoutComponents() {
64 | let bw = self.direction.principalSize(size: bounds.size)
65 | let bh = self.direction.nonPrincipalSize(size: bounds.size)
66 | let thumbWidth = bw * thumbWidthRatio
67 | let symbolWidth = (bw - thumbWidth) / 2
68 |
69 | // It makes most sense to have the + on the top of the view, when the direction is vertical
70 | let mpPosM: CGFloat
71 | let mpPosP: CGFloat
72 | if self.direction == .horizontal {
73 | mpPosM = 0
74 | mpPosP = symbolWidth + thumbWidth
75 | }
76 | else {
77 | mpPosM = symbolWidth + thumbWidth
78 | mpPosP = 0
79 | }
80 |
81 | let minusSymbolLabelFrame = CGRect(x: mpPosM, y: 0, width: symbolWidth, height: bh)
82 | let plusSymbolLabelFrame = CGRect(x: mpPosP, y: 0, width: symbolWidth, height: bh)
83 | let thumbLabelFrame = CGRect(x: symbolWidth, y: 0, width: thumbWidth, height: bh)
84 | let hintLabelFrame = CGRect(x: symbolWidth, y: -bounds.height * 1.5, width: thumbWidth, height: bh)
85 |
86 | minusSymbolLabel.frame = CGRect(origin: self.direction.getPosition(p: minusSymbolLabelFrame.origin), size: self.direction.getSize(size: minusSymbolLabelFrame.size))
87 | plusSymbolLabel.frame = CGRect(origin: self.direction.getPosition(p: plusSymbolLabelFrame.origin), size: self.direction.getSize(size: plusSymbolLabelFrame.size))
88 | thumbLabel.frame = CGRect(origin: self.direction.getPosition(p: thumbLabelFrame.origin), size: self.direction.getSize(size: thumbLabelFrame.size))
89 |
90 | // The hint label is not direction dependent
91 | hintLabel.frame = hintLabelFrame
92 |
93 | snappingBehavior = SnappingStepperBehavior(item: thumbLabel, snapToPoint: CGPoint(x: bounds.size.width * 0.5, y: bounds.size.height * 0.5))
94 |
95 | CustomShapeLayer.createHintShapeLayer(label: hintLabel, fillColor: thumbBackgroundColor?.lighter().cgColor)
96 |
97 | applyThumbStyle(style: thumbStyle)
98 | applyStyle(style: style)
99 | applyHintStyle(style: hintStyle)
100 | }
101 |
102 | func applyThumbStyle(style: ShapeStyle) {
103 | thumbLabel.style = style
104 | thumbLabel.borderColor = thumbBorderColor
105 | thumbLabel.borderWidth = thumbBorderWidth
106 | }
107 |
108 | func applyHintStyle(style: ShapeStyle) {
109 | hintLabel.style = style
110 | }
111 |
112 | func applyStyle(style: ShapeStyle) {
113 | let bgColor: UIColor = .clear
114 | let sLayer: CAShapeLayer
115 |
116 | if let borderColor = borderColor {
117 | sLayer = CustomShapeLayer.createShape(style: style, bounds: bounds, color: bgColor, borderColor: borderColor, borderWidth: borderWidth)
118 | }
119 | else {
120 | sLayer = CustomShapeLayer.createShape(style: style, bounds: bounds, color: bgColor)
121 | }
122 |
123 | if styleLayer.superlayer != nil {
124 | layer.replaceSublayer(styleLayer, with: sLayer)
125 | }
126 |
127 | styleLayer = sLayer
128 | }
129 |
130 | // MARK: - Responding to Gesture Events
131 |
132 | @objc func stepperTouched(sender: UITouchGestureRecognizer) {
133 | let touchLocation = sender.location(in: self)
134 | let hitView = hitTest(touchLocation, with: nil)
135 |
136 | factorValue = hitView == minusSymbolLabel ? -1 : 1
137 |
138 | switch (sender.state, hitView) {
139 | case (.began, .some(let v)) where v == minusSymbolLabel || v == plusSymbolLabel:
140 | if autorepeat {
141 | startAutorepeat()
142 | }
143 | else {
144 | let value = _value + stepValue * factorValue
145 |
146 | updateValue(value: value, finished: true)
147 | }
148 |
149 | v.backgroundColor = backgroundColor?.darkened()
150 | case (.changed, .some(let v)):
151 | if v == minusSymbolLabel || v == plusSymbolLabel {
152 | v.backgroundColor = backgroundColor?.darkened()
153 |
154 | if autorepeat {
155 | startAutorepeat()
156 | }
157 | }
158 | else {
159 | minusSymbolLabel.backgroundColor = backgroundColor
160 | plusSymbolLabel.backgroundColor = backgroundColor
161 |
162 | autorepeatHelper.stop()
163 | }
164 | default:
165 | minusSymbolLabel.backgroundColor = backgroundColor
166 | plusSymbolLabel.backgroundColor = backgroundColor
167 |
168 | if autorepeat {
169 | autorepeatHelper.stop()
170 |
171 | factorValue = 0
172 |
173 | updateValue(value: _value, finished: true)
174 | }
175 | }
176 | }
177 |
178 | @objc func sliderPanned(sender: UIPanGestureRecognizer) {
179 | switch sender.state {
180 | case .began:
181 | if case .none = hintStyle {} else {
182 | hintLabel.alpha = 0
183 | hintLabel.center = CGPoint(x: center.x, y: center.y - (bounds.size.height * 0.5 + hintLabel.bounds.height))
184 |
185 | superview?.addSubview(hintLabel)
186 |
187 | UIView.animate(withDuration: 0.2) {
188 | self.hintLabel.alpha = 1.0
189 | }
190 | }
191 |
192 | touchesBeganPoint = self.direction.getPosition(p: sender.translation(in: thumbLabel))
193 | dynamicButtonAnimator.removeBehavior(snappingBehavior)
194 |
195 | thumbLabel.backgroundColor = thumbBackgroundColor?.lighter()
196 | hintLabel.backgroundColor = thumbBackgroundColor?.lighter()
197 |
198 | if autorepeat {
199 | startAutorepeat(autorepeatCount: Int.max)
200 | }
201 | else {
202 | initialValue = _value
203 | }
204 | case .changed:
205 | let translationInView = self.direction.getPosition(p: sender.translation(in: thumbLabel))
206 | let bw = self.direction.principalSize(size: bounds.size)
207 | let tbw = self.direction.principalSize(size: thumbLabel.bounds.size)
208 | let tcenter = self.direction.getPosition(p: thumbLabel.center)
209 |
210 | var centerX = (bw * 0.5) + ((touchesBeganPoint.x + translationInView.x) * 0.4)
211 | centerX = max(tbw / 2, min(centerX, bw - tbw / 2))
212 |
213 | thumbLabel.center = self.direction.getPosition(p: CGPoint(x: centerX, y: tcenter.y))
214 |
215 | let locationRatio: CGFloat
216 | if self.direction == .horizontal {
217 | locationRatio = (tcenter.x - bounds.midX) / ((bounds.width - thumbLabel.bounds.width) / 2)
218 | }
219 | else {
220 | // The + is on top of the control in vertical layout, so the locationRatio must be reversed!
221 | locationRatio = (bounds.midY - tcenter.x) / ((bounds.height - thumbLabel.bounds.height) / 2)
222 | }
223 |
224 | let ratio = Double(Int(locationRatio * 10)) / 10
225 | let factorValue = ((maximumValue - minimumValue) / 100) * ratio
226 |
227 | if autorepeat {
228 | self.factorValue = factorValue
229 | }
230 | else {
231 | _value = initialValue + stepValue * factorValue
232 |
233 | updateValue(value: _value, finished: true)
234 | }
235 | case .ended, .failed, .cancelled:
236 | if case .none = hintStyle {} else {
237 | UIView.animate(withDuration: 0.2, animations: {
238 | self.hintLabel.alpha = 0.0
239 | }) { _ in
240 | self.hintLabel.removeFromSuperview()
241 | }
242 | }
243 |
244 | dynamicButtonAnimator.addBehavior(snappingBehavior)
245 |
246 | thumbLabel.backgroundColor = thumbBackgroundColor ?? backgroundColor?.lighter()
247 |
248 | if autorepeat {
249 | autorepeatHelper.stop()
250 |
251 | factorValue = 0
252 |
253 | updateValue(value: _value, finished: true)
254 | }
255 | case .possible:
256 | break
257 | }
258 | }
259 |
260 | // MARK: - Updating the Value
261 |
262 | func startAutorepeat(autorepeatCount count: Int = 0) {
263 | autorepeatHelper.start(autorepeatCount: count) { [weak self] in
264 | if let weakSelf = self {
265 | let value = weakSelf._value + weakSelf.stepValue * weakSelf.factorValue
266 |
267 | weakSelf.updateValue(value: value, finished: false)
268 | }
269 | }
270 | }
271 |
272 | func updateValue(value: Double, finished: Bool = true) {
273 | if !wraps {
274 | _value = max(minimumValue, min(value, maximumValue))
275 | }
276 | else if value < minimumValue {
277 | _value = maximumValue
278 | }
279 | else if value > maximumValue {
280 | _value = minimumValue
281 | }
282 |
283 | if (continuous || finished) && oldValue != _value {
284 | oldValue = _value
285 |
286 | sendActions(for: .valueChanged)
287 |
288 | if let _valueChangedBlock = valueChangedBlock {
289 | _valueChangedBlock(_value)
290 | }
291 | }
292 | }
293 |
294 | func valueAsText() -> String {
295 | return (value.truncatingRemainder(dividingBy: 1) == 0) ? "\(Int(value))" : "\(value)"
296 | }
297 | }
298 |
299 |
--------------------------------------------------------------------------------
/Sources/SnappingStepper.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * SnappingStepper
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | import DynamicColor
28 | import UIKit
29 |
30 | /**
31 | A stepper control provides a user interface for incrementing or decrementing a value.
32 |
33 | The `SnappingStepper` addings a thumb in the middle to allow the user to update the value by sliding it either to the left or the right side. It can also be customizable to display the current value or custom text.
34 | */
35 | @IBDesignable public final class SnappingStepper: UIControl {
36 | // MARK: - Preparing and Sending Messages using Blocks
37 |
38 | /**
39 | Block to be notify when the value of the stepper change.
40 |
41 | This is a convenient alternative to the `addTarget:Action:forControlEvents:` method of the `UIControl`.
42 | */
43 | public var valueChangedBlock: ((_ value: Double) -> Void)?
44 |
45 | // MARK: - Configuring the Stepper
46 |
47 | /**
48 | The continuous vs. noncontinuous state of the stepper.
49 |
50 | If true, value change events are sent immediately when the value changes during user interaction. If false, a value change event is sent when user interaction ends.
51 |
52 | The default value for this property is true.
53 | */
54 | @IBInspectable public var continuous: Bool = true
55 |
56 | /**
57 | The automatic vs. nonautomatic repeat state of the stepper.
58 |
59 | If true, the user pressing and holding on the stepper repeatedly alters value.
60 |
61 | The default value for this property is true.
62 | */
63 | @IBInspectable public var autorepeat: Bool = true
64 |
65 | /**
66 | The wrap vs. no-wrap state of the stepper.
67 |
68 | If true, incrementing beyond maximumValue sets value to minimumValue; likewise, decrementing below minimumValue sets value to maximumValue. If false, the stepper does not increment beyond maximumValue nor does it decrement below minimumValue but rather holds at those values.
69 |
70 | The default value for this property is false.
71 | */
72 | @IBInspectable public var wraps: Bool = false
73 |
74 | /**
75 | The direction of the control
76 |
77 | The default is horizontal
78 | */
79 | @IBInspectable public var direction: StyledControlDirection = .horizontal {
80 | didSet {
81 | self.layoutComponents()
82 | }
83 | }
84 |
85 | /**
86 | The lowest possible numeric value for the stepper.
87 |
88 | Must be numerically less than maximumValue. If you attempt to set a value equal to or greater than maximumValue, the system raises an NSInvalidArgumentException exception.
89 |
90 | The default value for this property is 0.
91 | */
92 | @IBInspectable public var minimumValue: Double = 0 {
93 | didSet {
94 | if minimumValue > maximumValue {
95 | maximumValue = minimumValue
96 | }
97 |
98 | updateValue(value: max(_value, minimumValue), finished: true)
99 | }
100 | }
101 |
102 | /**
103 | The highest possible numeric value for the stepper.
104 |
105 | Must be numerically greater than minimumValue. If you attempt to set a value equal to or lower than minimumValue, the system raises an NSInvalidArgumentException exception.
106 |
107 | The default value of this property is 100.
108 | */
109 | @IBInspectable public var maximumValue: Double = 100 {
110 | didSet {
111 | if maximumValue < minimumValue {
112 | minimumValue = maximumValue
113 | }
114 |
115 | updateValue(value: min(_value, maximumValue), finished: true)
116 | }
117 | }
118 |
119 | /**
120 | The step, or increment, value for the stepper.
121 |
122 | Must be numerically greater than 0. If you attempt to set this property’s value to 0 or to a negative number, the system raises an NSInvalidArgumentException exception.
123 |
124 | The default value for this property is 1.
125 | */
126 | @IBInspectable public var stepValue: Double = 1
127 |
128 | // MARK: - Accessing the Stepper’s Value
129 |
130 | /**
131 | The numeric value of the snapping stepper.
132 |
133 | When the value changes, the stepper sends the UIControlEventValueChanged flag to its target (see addTarget:action:forControlEvents:). Refer to the description of the continuous property for information about whether value change events are sent continuously or when user interaction ends.
134 |
135 | The default value for this property is 0. This property is clamped at its lower extreme to minimumValue and is clamped at its upper extreme to maximumValue.
136 | */
137 | @IBInspectable public var value: Double {
138 | get {
139 | return _value
140 | }
141 | set (newValue) {
142 | updateValue(value: newValue, finished: true)
143 | }
144 | }
145 |
146 | var _value: Double = 0 {
147 | didSet {
148 | if thumbText == nil {
149 | thumbLabel.text = valueAsText()
150 | }
151 |
152 | hintLabel.text = valueAsText()
153 | }
154 | }
155 |
156 | // MARK: - Setting the Stepper Visual Appearance
157 |
158 | /// The font of the text symbols (`minus` and `plus`).
159 | @IBInspectable public var symbolFont = UIFont(name: "TrebuchetMS-Bold", size: 20) {
160 | didSet {
161 | minusSymbolLabel.font = symbolFont
162 | plusSymbolLabel.font = symbolFont
163 | }
164 | }
165 |
166 | /// The color of the text symbols (`minus` and `plus`).
167 | @IBInspectable public var symbolFontColor: UIColor = .black {
168 | didSet {
169 | minusSymbolLabel.textColor = symbolFontColor
170 | plusSymbolLabel.textColor = symbolFontColor
171 | }
172 | }
173 |
174 | /// The thumb width represented as a ratio of the component width. For example if the width of the stepper is 30px and the ratio is 0.5, the thumb width will be equal to 15px. Defaults to 0.5.
175 | @IBInspectable public var thumbWidthRatio: CGFloat = 0.5 {
176 | didSet {
177 | layoutComponents()
178 | }
179 | }
180 |
181 | /// The font of the thumb label.
182 | @IBInspectable public var thumbFont = UIFont(name: "TrebuchetMS-Bold", size: 20) {
183 | didSet {
184 | thumbLabel.font = thumbFont
185 | }
186 | }
187 |
188 | /// The thumb's background color. If nil the thumb color will be lighter than the background color. Defaults to nil.
189 | @IBInspectable public var thumbBackgroundColor: UIColor? {
190 | didSet {
191 | thumbLabel.backgroundColor = thumbBackgroundColor
192 | }
193 | }
194 |
195 | /// The thumb's text color. Default's to black
196 | @IBInspectable public var thumbTextColor: UIColor = .black {
197 | didSet {
198 | thumbLabel.textColor = thumbTextColor
199 | }
200 | }
201 |
202 | /// The thumb's style. Default's to box
203 | public var thumbStyle: ShapeStyle = .box {
204 | didSet {
205 | self.applyThumbStyle(style: thumbStyle)
206 | }
207 | }
208 |
209 | /// The view's style. Default's to box.
210 | public var style: ShapeStyle = .box {
211 | didSet {
212 | self.applyStyle(style: style)
213 | }
214 | }
215 |
216 | /// The hint's style. Default's to none, so no hint will be displayed.
217 | public var hintStyle: ShapeStyle = .none {
218 | didSet {
219 | self.applyHintStyle(style: hintStyle)
220 | }
221 | }
222 |
223 | /// The view's border color.
224 | @IBInspectable public var borderColor: UIColor? {
225 | didSet {
226 | self.applyStyle(style: style)
227 | }
228 | }
229 |
230 | /// The thumbs's border color.
231 | @IBInspectable public var thumbBorderColor: UIColor? {
232 | didSet {
233 | self.applyThumbStyle(style: thumbStyle)
234 | }
235 | }
236 |
237 | /// The view's border width. Default's to 1.0
238 | @IBInspectable public var borderWidth: CGFloat = 1.0 {
239 | didSet {
240 | self.applyStyle(style: style)
241 | }
242 | }
243 |
244 | /// The thumbs's border width. Default's to 1.0
245 | @IBInspectable public var thumbBorderWidth: CGFloat = 1.0 {
246 | didSet {
247 | self.applyThumbStyle(style: thumbStyle)
248 | }
249 | }
250 |
251 | /// The view’s background color.
252 | override public var backgroundColor: UIColor? {
253 | didSet {
254 | self.styleColor = backgroundColor
255 | self.applyStyle(style: self.style)
256 |
257 | if thumbBackgroundColor == nil {
258 | thumbLabel.backgroundColor = backgroundColor?.lighter()
259 | }
260 | }
261 | }
262 |
263 | // MARK: - Displaying Thumb Text
264 |
265 | /// The thumb text to display. If the text is nil it will display the current value of the stepper. Defaults with empty string.
266 | @IBInspectable public var thumbText: String? = "" {
267 | didSet {
268 | if thumbText == nil {
269 | thumbLabel.text = valueAsText()
270 | }
271 | else {
272 | thumbLabel.text = thumbText
273 | }
274 |
275 | hintLabel.text = valueAsText()
276 | }
277 | }
278 |
279 | // MARK: - Deallocating Snappinf Stepper
280 |
281 | deinit {
282 | autorepeatHelper.stop()
283 | }
284 |
285 | // MARK: - Initializing a Snapping Stepper
286 |
287 | /// Initializes and returns a newly allocated view object with the specified frame rectangle.
288 | override public init(frame: CGRect) {
289 | super.init(frame: frame)
290 |
291 | initComponents()
292 | setupGestures()
293 | }
294 |
295 | /// Returns an object initialized from data in a given unarchiver.
296 | required public init?(coder aDecoder: NSCoder) {
297 | super.init(coder: aDecoder)
298 |
299 | initComponents()
300 | setupGestures()
301 | }
302 |
303 | // MARK: - Laying out Subviews
304 |
305 | /// Lays out subviews
306 | public override func layoutSubviews() {
307 | super.layoutSubviews()
308 |
309 | layoutComponents()
310 | }
311 |
312 | // MARK: - Internal Properties
313 |
314 | /// The value label that represents the thumb button
315 | lazy var thumbLabel: StyledLabel = UIBuilder.defaultStyledLabel()
316 |
317 | /// The hint label
318 | lazy var hintLabel: StyledLabel = UIBuilder.defaultStyledLabel()
319 |
320 | /// The minus label
321 | lazy var minusSymbolLabel: UILabel = UIBuilder.defaultLabel()
322 |
323 | /// The plus label
324 | lazy var plusSymbolLabel: UILabel = UIBuilder.defaultLabel()
325 |
326 | let autorepeatHelper = AutoRepeatHelper()
327 | let dynamicButtonAnimator = UIDynamicAnimator()
328 | var snappingBehavior = SnappingStepperBehavior(item: nil, snapToPoint: CGPoint.zero)
329 |
330 | var styleLayer = CAShapeLayer()
331 | var styleColor: UIColor? = .clear
332 |
333 | var touchesBeganPoint = CGPoint.zero
334 | var initialValue: Double = -1
335 | var factorValue: Double = 0
336 | var oldValue = Double.infinity * -1
337 | }
338 |
339 |
--------------------------------------------------------------------------------
/Sources/SnappingStepperBehavior.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * SnappingStepper
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | import UIKit
28 |
29 | /// The snapping dynamic behavic.
30 | final internal class SnappingStepperBehavior: UIDynamicBehavior {
31 | init(item: UIDynamicItem?, snapToPoint point: CGPoint) {
32 | super.init()
33 |
34 | if let _item = item {
35 | let dynamicItemBehavior = UIDynamicItemBehavior(items: [_item])
36 | dynamicItemBehavior.allowsRotation = false
37 |
38 | let snapBehavior = UISnapBehavior(item: _item, snapTo: point)
39 | snapBehavior.damping = 0.25
40 |
41 | addChildBehavior(dynamicItemBehavior)
42 | addChildBehavior(snapBehavior)
43 | }
44 | }
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/Sources/StyledControlDirection.swift:
--------------------------------------------------------------------------------
1 | // ORIGINAL COPYRIGHT NOTE / ORIGINATES FROM MJRFlexStyleComponents
2 | //
3 | // StyledControlDirection.swift
4 | // MJRFlexStyleComponents
5 | //
6 | // Created by Martin Rehder on 16.07.16.
7 | /*
8 | * Copyright 2016-present Martin Jacob Rehder.
9 | * http://www.rehsco.com
10 | *
11 | * Permission is hereby granted, free of charge, to any person obtaining a copy
12 | * of this software and associated documentation files (the "Software"), to deal
13 | * in the Software without restriction, including without limitation the rights
14 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 | * copies of the Software, and to permit persons to whom the Software is
16 | * furnished to do so, subject to the following conditions:
17 | *
18 | * The above copyright notice and this permission notice shall be included in
19 | * all copies or substantial portions of the Software.
20 | *
21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27 | * THE SOFTWARE.
28 | *
29 | */
30 |
31 | import UIKit
32 |
33 | @objc public enum StyledControlDirection: Int {
34 | case horizontal
35 | case vertical
36 |
37 | /**
38 | The principal size is the axis of the direction. Width when the direction is horizontal and height when the direction is vertical.
39 | */
40 | func principalSize(size: CGSize) -> CGFloat {
41 | switch self {
42 | case .horizontal:
43 | return size.width
44 | case .vertical:
45 | return size.height
46 | }
47 | }
48 |
49 | /**
50 | The non-principal size is the perpendicular axis of the direction. Height when the direction is horizontal and width when the direction is vertical.
51 | */
52 | func nonPrincipalSize(size: CGSize) -> CGFloat {
53 | switch self {
54 | case .horizontal:
55 | return size.height
56 | case .vertical:
57 | return size.width
58 | }
59 | }
60 |
61 | /**
62 | The principal size of the direction is applied. Vertical direction will flip the width and the height in the size.
63 | */
64 | func getSize(size: CGSize) -> CGSize {
65 | switch self {
66 | case .horizontal:
67 | return size
68 | case .vertical:
69 | return CGSize(width: size.width, height: size.height)
70 | }
71 | }
72 |
73 | /**
74 | The principal position of the direction is applied. Vertical direction will flip the X and the Y in the point.
75 | */
76 | func getPosition(p: CGPoint) -> CGPoint {
77 | switch self {
78 | case .horizontal:
79 | return p
80 | case .vertical:
81 | return CGPoint(x: p.y, y: p.x)
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/StyledLabel.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * StyledLabel
3 | * Created by Martin Rehder.
4 | *
5 | * SnappingStepper
6 | *
7 | * Copyright 2015-present Yannick Loriot.
8 | * http://yannickloriot.com
9 | *
10 | * Permission is hereby granted, free of charge, to any person obtaining a copy
11 | * of this software and associated documentation files (the "Software"), to deal
12 | * in the Software without restriction, including without limitation the rights
13 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | * copies of the Software, and to permit persons to whom the Software is
15 | * furnished to do so, subject to the following conditions:
16 | *
17 | * The above copyright notice and this permission notice shall be included in
18 | * all copies or substantial portions of the Software.
19 | *
20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 | * THE SOFTWARE.
27 | *
28 | */
29 |
30 | import UIKit
31 |
32 | /// The `StyledLabel` object is an `UILabel` with a custom shape.
33 | class StyledLabel: UIView {
34 | var label = UILabel()
35 | var styleColor: UIColor? = .clear
36 | var shapeLayer = CAShapeLayer()
37 |
38 | var style: ShapeStyle = .box {
39 | didSet {
40 | applyStyle()
41 | }
42 | }
43 |
44 | var text: String? {
45 | didSet {
46 | label.text = text
47 | }
48 | }
49 |
50 | var textColor: UIColor = .black {
51 | didSet {
52 | label.textColor = textColor
53 | }
54 | }
55 |
56 | override var backgroundColor: UIColor? {
57 | get {
58 | return .clear
59 | }
60 | set {
61 | styleColor = newValue
62 |
63 | applyStyle()
64 | }
65 | }
66 |
67 | var borderColor: UIColor? {
68 | didSet {
69 | applyStyle()
70 | }
71 | }
72 |
73 | var borderWidth: CGFloat = 1.0 {
74 | didSet {
75 | applyStyle()
76 | }
77 | }
78 |
79 | var font: UIFont? {
80 | didSet {
81 | self.label.font = font
82 | }
83 | }
84 |
85 | var textAlignment: NSTextAlignment = .center {
86 | didSet {
87 | label.textAlignment = textAlignment
88 | }
89 | }
90 |
91 | var rotationInRadians: CGFloat = 0 {
92 | didSet {
93 | self.setNeedsLayout()
94 | }
95 | }
96 |
97 | init() {
98 | super.init(frame: CGRect.zero)
99 |
100 | self.layer.addSublayer(self.shapeLayer)
101 | }
102 |
103 | required init?(coder aDecoder: NSCoder) {
104 | fatalError("init(coder:) has not been implemented")
105 | }
106 |
107 | override func layoutSubviews() {
108 | super.layoutSubviews()
109 |
110 | self.applyStyle()
111 | label.removeFromSuperview()
112 |
113 | self.label.frame = bounds
114 | self.label.transform = CGAffineTransform(rotationAngle: self.rotationInRadians)
115 | self.label.frame = bounds
116 | self.addSubview(label)
117 | }
118 |
119 | func applyStyle() {
120 | let bgColor = styleColor ?? UIColor.clear
121 | let sLayer: CAShapeLayer
122 |
123 | if let borderColor = borderColor {
124 | sLayer = CustomShapeLayer.createShape(style: style, bounds: bounds, color: bgColor, borderColor: borderColor, borderWidth: borderWidth)
125 | }
126 | else {
127 | sLayer = CustomShapeLayer.createShape(style: style, bounds: bounds, color: bgColor)
128 | }
129 |
130 | if self.shapeLayer.superlayer != nil {
131 | self.layer.replaceSublayer(shapeLayer, with: sLayer)
132 | }
133 |
134 | self.shapeLayer = sLayer
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Sources/UIBuilder.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * SnappingStepper
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | import UIKit
28 |
29 | final class UIBuilder {
30 | static func defaultLabel() -> UILabel {
31 | let label = UILabel()
32 | label.textAlignment = .center
33 | label.isUserInteractionEnabled = true
34 |
35 | return label
36 | }
37 |
38 | static func defaultStyledLabel() -> StyledLabel {
39 | let label = StyledLabel()
40 | label.textAlignment = .center
41 | label.text = ""
42 |
43 | return label
44 | }
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/Sources/UITouchGestureRecognizer.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * SnappingStepper
3 | *
4 | * Copyright 2015-present Yannick Loriot.
5 | * http://yannickloriot.com
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | *
25 | */
26 |
27 | import UIKit
28 | import UIKit.UIGestureRecognizerSubclass
29 |
30 | /// Gesture to know whether the touch is inside the view
31 | final class UITouchGestureRecognizer: UIGestureRecognizer {
32 | var isTouchInside = true
33 |
34 | override func touchesBegan(_ touches: Set, with event: UIEvent) {
35 | if state == .possible {
36 | state = .began
37 | }
38 | }
39 |
40 | override func touchesMoved(_ touches: Set, with event: UIEvent) {
41 | if let touch = touches.first, let view = view {
42 | let touchLocation = touch.location(in: view)
43 | let touchAreaRect = view.bounds.insetBy(dx: -10, dy: -10)
44 |
45 | let isInside = touchAreaRect.contains(touchLocation)
46 |
47 | if !isTouchInside && isInside {
48 | isTouchInside = true
49 |
50 | state = .changed
51 | }
52 | else if isTouchInside && !isInside {
53 | isTouchInside = false
54 |
55 | state = .changed
56 | }
57 | }
58 | }
59 |
60 | override func touchesEnded(_ touches: Set, with event: UIEvent) {
61 | state = .failed
62 | }
63 |
64 | override func touchesCancelled(_ touches: Set, with event: UIEvent) {
65 | state = .failed
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Tests/SnappingStepperTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SnappingStepperTests.swift
3 | // SnappingStepperExample
4 | //
5 | // Created by Yannick LORIOT on 29/05/15.
6 | // Copyright (c) 2015 Yannick Loriot. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import XCTest
11 |
12 | class SnappingStepperTests: XCTestCase {
13 | func testDefaultValues() {
14 | let stepper = SnappingStepper()
15 |
16 | XCTAssert(stepper.continuous, "'continuous' attributes should be false by default")
17 | XCTAssert(stepper.autorepeat, "'autorepeat' attributes should be false by default")
18 | XCTAssert(!stepper.wraps, "'wraps' attributes should be false by default")
19 | XCTAssert(stepper.minimumValue == 0, "'minimumValue' attributes should be equal to 0 by default")
20 | XCTAssert(stepper.maximumValue == 100, "'maximumValue' attributes should be equal to 100 by default")
21 | XCTAssert(stepper.stepValue == 1, "'stepValue' attributes should be equal to 1 by default")
22 | XCTAssert(stepper.value == 0, "'value' attributes should be equal to 0 by default")
23 | }
24 |
25 | func testMinimumValue() {
26 | let stepper = SnappingStepper()
27 |
28 | XCTAssert(stepper.minimumValue <= stepper.maximumValue, "'minimum' value should always be lower or equal than the 'maximum' value")
29 |
30 | stepper.maximumValue = 50
31 | stepper.minimumValue = 50
32 |
33 | XCTAssert(stepper.minimumValue <= stepper.maximumValue, "'minimum' value should always be lower or equal than the 'maximum' value")
34 | XCTAssert(stepper.minimumValue <= stepper.value, "value should not be lower than the 'minimum' value")
35 |
36 | stepper.maximumValue = -10
37 |
38 | XCTAssert(stepper.minimumValue <= stepper.maximumValue, "'minimum' value should always be lower or equal than the 'maximum' value")
39 | XCTAssert(stepper.minimumValue == -10, "'minimum' value should be equal to -10")
40 | XCTAssert(stepper.minimumValue <= stepper.value, "value should not be lower than the 'minimum' value")
41 |
42 | stepper.value = -200
43 |
44 | XCTAssert(stepper.minimumValue <= stepper.value, "value should not be lower than the 'minimum' value")
45 |
46 | stepper.minimumValue = 0
47 |
48 | XCTAssert(stepper.minimumValue <= stepper.value, "value should not be lower than the 'minimum' value")
49 | }
50 |
51 | func testMaximumValue() {
52 | let stepper = SnappingStepper()
53 |
54 | XCTAssert(stepper.maximumValue >= stepper.minimumValue, "'maximum' value should always be greater or equal than the 'minimum' value")
55 |
56 | stepper.minimumValue = 50
57 | stepper.maximumValue = 40
58 |
59 | XCTAssert(stepper.maximumValue >= stepper.minimumValue, "'maximum' value should always be greater or equal than the 'minimum' value")
60 | XCTAssert(stepper.value <= stepper.maximumValue, "value should not be greater than the 'maximum' value")
61 |
62 | stepper.minimumValue = 200
63 |
64 | XCTAssert(stepper.maximumValue >= stepper.minimumValue, "'maximum' value should always be greater or equal than the 'minimum' value")
65 | XCTAssert(stepper.maximumValue == 200, "'maximum' value should be equal to 200")
66 | XCTAssert(stepper.value <= stepper.maximumValue, "value should not be greater than the 'maximum' value")
67 |
68 | stepper.value = 300
69 |
70 | XCTAssert(stepper.value <= stepper.maximumValue, "value should not be greater than the 'maximum' value")
71 |
72 | stepper.maximumValue = -10
73 |
74 | XCTAssert(stepper.value <= stepper.maximumValue, "value should not be greater than the 'maximum' value")
75 | }
76 |
77 | func testWrap() {
78 | let stepper = SnappingStepper()
79 |
80 | stepper.wraps = false
81 | stepper.maximumValue = 100
82 | stepper.minimumValue = 0
83 | stepper.value = 105
84 |
85 | XCTAssert(stepper.value == 100, "'value' should be equal to the 'maximum' value")
86 |
87 | stepper.value = -4
88 |
89 | XCTAssert(stepper.value == 0, "'value' should be equal to the 'minimum' value")
90 |
91 | stepper.wraps = true
92 | stepper.value = 105
93 |
94 | XCTAssert(stepper.value == 0, "'value' should be equal to the 'minimum' value")
95 |
96 | stepper.value = -4
97 |
98 | XCTAssert(stepper.value == 100, "'value' should be equal to the 'maximum' value")
99 | }
100 |
101 | func testContinuous() {
102 | let expect = expectation(description: "Value changed")
103 |
104 | let stepper = SnappingStepper()
105 | var changeCount = 0
106 |
107 | stepper.continuous = true
108 | stepper.valueChangedBlock = { (value) in
109 | changeCount += 1
110 |
111 | if changeCount == 2 {
112 | expect.fulfill()
113 | }
114 | }
115 |
116 | stepper.value = 10
117 | stepper.value = 10
118 | stepper.value = 11
119 |
120 | waitForExpectations(timeout: 0.1) { (error) in }
121 | }
122 |
123 | func testNonContinuous() {
124 | let expect = expectation(description: "Value changed")
125 |
126 | let stepper = SnappingStepper()
127 | var changeCount = 0
128 |
129 | stepper.continuous = false
130 | stepper.valueChangedBlock = { (value) in
131 | changeCount += 1
132 |
133 | if changeCount == 2 {
134 | expect.fulfill()
135 | }
136 | }
137 |
138 | stepper.updateValue(value: 10, finished: false)
139 | stepper.updateValue(value: 10, finished: true)
140 | stepper.updateValue(value: 11, finished: false)
141 | stepper.updateValue(value: 12, finished: false)
142 | stepper.updateValue(value: 13, finished: false)
143 | stepper.updateValue(value: 14, finished: true)
144 |
145 | waitForExpectations(timeout: 0.1) { (error) in }
146 | }
147 |
148 | func testThumbLabelTextValue() {
149 | let stepper = SnappingStepper()
150 | stepper.value = 100
151 |
152 | XCTAssert(stepper.value == 100, "'value' should be equal to the 'maximum' value")
153 | XCTAssert(stepper.thumbLabel.text == "", "'thumbLabel.text' should be equal to empty string")
154 |
155 | stepper.thumbText = nil
156 | XCTAssert(stepper.thumbLabel.text == "100", "'thumbLabel.text' should be equal to \"100\"")
157 |
158 | stepper.value = 50
159 | XCTAssert(stepper.thumbLabel.text == "50", "'thumbLabel.text' should be equal to \"50\"")
160 |
161 | stepper.value = 50.0
162 | XCTAssert(stepper.thumbLabel.text == "50", "'thumbLabel.text' should be equal to \"50\"")
163 |
164 | stepper.value = 50.2
165 | XCTAssert(stepper.thumbLabel.text == "50.2", "'thumbLabel.text' should be equal to \"50.2\"")
166 |
167 | stepper.value = 150
168 | XCTAssert(stepper.thumbLabel.text == "100", "'thumbLabel.text' should be equal to \"100\"")
169 |
170 | stepper.thumbText = "Move Me!"
171 | XCTAssert(stepper.thumbLabel.text == "Move Me!", "'thumbLabel.text' should be equal to \"Move Me!\"")
172 |
173 | stepper.value = 50
174 | XCTAssert(stepper.thumbLabel.text == "Move Me!", "'thumbLabel.text' should be equal to \"Move Me!\"")
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | ignore:
3 | - Example/*
4 | - Tests/*
5 |
--------------------------------------------------------------------------------