@@ -11,7 +11,7 @@ import UIKit
11@IBDesignable public class GMStepper: UIControl {
11@IBDesignable public class GMStepper: UIControl {
13 /// Current value of the stepper. Defaults to 0.
13 /// Current value of the stepper. Defaults to 0.
14− public var value = 0 {
14+ @IBInspectable public var value = 0 {
16 value = min(maximumValue, max(minimumValue, value))
16 value = min(maximumValue, max(minimumValue, value))
17 label.text = String(value)
17 label.text = String(value)
@@ -25,18 +25,18 @@ import UIKit
25 /// Minimum value. Must be less than maximumValue. Defaults to 0.
25 /// Minimum value. Must be less than maximumValue. Defaults to 0.
26 @IBInspectable public var minimumValue: Int = 0 {
26 @IBInspectable public var minimumValue: Int = 0 {
28− value = 0
28+ value = min(maximumValue, max(minimumValue, value))
32 /// Maximum value. Must be more than minimumValue. Defaults to 10.
32 /// Maximum value. Must be more than minimumValue. Defaults to 10.
33 @IBInspectable public var maximumValue: Int = 10 {
33 @IBInspectable public var maximumValue: Int = 10 {
35− value = 0
35+ value = min(maximumValue, max(minimumValue, value))
39− /// The same as UIStepper's autorepeat. If true, holding on the buttons or the pan gesture repeatedly alters value. Defaults to true.
39+ /// The same as UIStepper's autorepeat. If true, holding on the buttons or keeping the pan gesture alters the value repeatedly. Defaults to true.
40 @IBInspectable public var autorepeat: Bool = true
40 @IBInspectable public var autorepeat: Bool = true
42 /// Text on the left button. Be sure that it fits in the button. Defaults to "-".
42 /// Text on the left button. Be sure that it fits in the button. Defaults to "-".
@@ -118,34 +118,83 @@ import UIKit
121+ /// Color of the flashing animation on the buttons in case the value hit the limit.
122+ @IBInspectable public var limitHitAnimationColor: UIColor = UIColor(red:0.26, green:0.6, blue:0.87, alpha:1)
122 Width of the sliding animation. When buttons clicked, the middle label does a slide animation towards to the clicked button. Defaults to 5.
125 Width of the sliding animation. When buttons clicked, the middle label does a slide animation towards to the clicked button. Defaults to 5.
124− private let labelSlideLength: CGFloat = 5
127+ let labelSlideLength: CGFloat = 5
126 /// Duration of the sliding animation
129 /// Duration of the sliding animation
127− private let labelSlideDuration = NSTimeInterval(0.1)
130+ let labelSlideDuration = NSTimeInterval(0.1)
132+ /// Duration of the animation when the value hits the limit.
133+ let limitHitAnimationDuration = NSTimeInterval(0.1)
135+ lazy var leftButton: UIButton = {
136+ let button = UIButton()
137+ button.setTitle(self.leftButtonText, forState: .Normal)
138+ button.setTitleColor(self.buttonsTextColor, forState: .Normal)
139+ button.backgroundColor = self.buttonsBackgroundColor
140+ button.titleLabel?.font = self.buttonsFont
141+ button.addTarget(self, action: "leftButtonTouchDown:", forControlEvents: .TouchDown)
142+ button.addTarget(self, action: "buttonTouchUp:", forControlEvents: UIControlEvents.TouchUpInside)
143+ button.addTarget(self, action: "buttonTouchUp:", forControlEvents: UIControlEvents.TouchUpOutside)
129− /// Duration of the flashing animation when the value hits the limit.
147+ lazy var rightButton: UIButton = {
130− private let limitHitAnimationDuration = NSTimeInterval(0.1)
148+ let button = UIButton()
149+ button.setTitle(self.rightButtonText, forState: .Normal)
150+ button.setTitleColor(self.buttonsTextColor, forState: .Normal)
151+ button.backgroundColor = self.buttonsBackgroundColor
152+ button.titleLabel?.font = self.buttonsFont
153+ button.addTarget(self, action: "rightButtonTouchDown:", forControlEvents: .TouchDown)
154+ button.addTarget(self, action: "buttonTouchUp:", forControlEvents: UIControlEvents.TouchUpInside)
155+ button.addTarget(self, action: "buttonTouchUp:", forControlEvents: UIControlEvents.TouchUpOutside)
132− /// Color of the flashing animation on the buttons in case the value hit the limit.
159+ lazy var label: UILabel = {
133− @IBInspectable public var limitHitAnimationColor: UIColor = UIColor(red:0.26, green:0.6, blue:0.87, alpha:1)
160+ let label = UILabel()
161+ label.textAlignment = .Center
162+ label.text = String(self.value)
163+ label.textColor = self.labelTextColor
164+ label.backgroundColor = self.labelBackgroundColor
165+ label.font = self.labelFont
166+ label.userInteractionEnabled = true
167+ let panRecognizer = UIPanGestureRecognizer(target: self, action: "handlePan:")
168+ panRecognizer.maximumNumberOfTouches = 1
169+ label.addGestureRecognizer(panRecognizer)
135− private let leftButton = UIButton()
173+ var labelOriginalCenter: CGPoint!
136− private let rightButton = UIButton()
174+ var labelMaximumCenterX: CGFloat!
137− private let label = UILabel()
175+ var labelMinimumCenterX: CGFloat!
139− private var labelOriginalCenter: CGPoint!
177+ enum LabelPanState {
140− private var labelMaximumCenterX: CGFloat!
178+ case Stable, HitRightEdge, HitLeftEdge
141− private var labelMinimumCenterX: CGFloat!
179+ }
180+ var panState = LabelPanState.Stable
143− private enum LabelPanState {
182+ enum StepperState {
144− case Stale, HitRightEdge, HitLeftEdge
183+ case Stable, ShouldIncrease, ShouldDecrease
185+ var stepperState = StepperState.Stable {
187+ if stepperState != .Stable {
146− private var panState = LabelPanState.Stale
196+ /// Timer used for autorepeat option
148− private var timer: NSTimer?
197+ var timer: NSTimer?
150 /** When UIStepper reaches its top speed, it alters the value with a time interval of ~0.05 sec.
199 /** When UIStepper reaches its top speed, it alters the value with a time interval of ~0.05 sec.
151 The user pressing and holding on the stepper repeatedly:
200 The user pressing and holding on the stepper repeatedly:
@@ -153,11 +202,11 @@ import UIKit
153 - For the next 1.5 sec, it changes the value every 0.1 sec.
202 - For the next 1.5 sec, it changes the value every 0.1 sec.
154 - Then, every 0.05 sec.
203 - Then, every 0.05 sec.
156− private let timerInterval = NSTimeInterval(0.05)
205+ let timerInterval = NSTimeInterval(0.05)
158 /// Check the handleTimerFire: function. While it is counting the number of fires, it decreases the mod value so that the value is altered more frequently.
207 /// Check the handleTimerFire: function. While it is counting the number of fires, it decreases the mod value so that the value is altered more frequently.
159− private var timerFireCount = 0
208+ var timerFireCount = 0
160− private var timerFireCountModulo: Int {
209+ var timerFireCountModulo: Int {
161 if timerFireCount > 80 {
210 if timerFireCount > 80 {
162 return 1 // 0.05 sec * 1 = 0.05 sec
211 return 1 // 0.05 sec * 1 = 0.05 sec
163 } else if timerFireCount > 50 {
212 } else if timerFireCount > 50 {
@@ -167,6 +216,7 @@ import UIKit
219+ /// Useful for logging the timer interval. You can call this in the timer handler to test the autorepeat option.
170 lazy var printTimerGaps: () -> () = {
220 lazy var printTimerGaps: () -> () = {
171 var prevTime: CFAbsoluteTime?
221 var prevTime: CFAbsoluteTime?
@@ -189,39 +239,14 @@ import UIKit
192− private func setup() {
242+ func setup() {
193− leftButton.setTitle(leftButtonText, forState: .Normal)
194− leftButton.setTitleColor(buttonsTextColor, forState: .Normal)
195− leftButton.backgroundColor = buttonsBackgroundColor
196− leftButton.titleLabel?.font = buttonsFont
197− leftButton.addTarget(self, action: "leftButtonTouchDown:", forControlEvents: .TouchDown)
198− leftButton.addTarget(self, action: "leftButtonTouchUp:", forControlEvents: UIControlEvents.TouchUpInside)
199− leftButton.addTarget(self, action: "leftButtonTouchUp:", forControlEvents: UIControlEvents.TouchUpOutside)
200 addSubview(leftButton)
243 addSubview(leftButton)
202− rightButton.setTitle(rightButtonText, forState: .Normal)
203− rightButton.setTitleColor(buttonsTextColor, forState: .Normal)
204− rightButton.backgroundColor = buttonsBackgroundColor
205− rightButton.titleLabel?.font = buttonsFont
206− rightButton.addTarget(self, action: "rightButtonTapped:", forControlEvents: .TouchDown)
207− rightButton.addTarget(self, action: "rightButtonTouchUp:", forControlEvents: UIControlEvents.TouchUpInside)
208− rightButton.addTarget(self, action: "rightButtonTouchUp:", forControlEvents: UIControlEvents.TouchUpOutside)
209 addSubview(rightButton)
244 addSubview(rightButton)
211− label.textAlignment = .Center
212− label.text = String(value)
213− label.textColor = labelTextColor
214− label.backgroundColor = labelBackgroundColor
215− label.font = labelFont
216 addSubview(label)
245 addSubview(label)
218 backgroundColor = buttonsBackgroundColor
247 backgroundColor = buttonsBackgroundColor
219 layer.cornerRadius = cornerRadius
248 layer.cornerRadius = cornerRadius
220 clipsToBounds = true
249 clipsToBounds = true
222− let panRecognizer = UIPanGestureRecognizer(target: self, action: "handlePan:")
223− label.userInteractionEnabled = true
224− label.addGestureRecognizer(panRecognizer)
227 public override func layoutSubviews() {
252 public override func layoutSubviews() {
@@ -237,9 +262,21 @@ import UIKit
237 labelOriginalCenter = label.center
262 labelOriginalCenter = label.center
265+ func updateValue() {
266+ if stepperState == .ShouldIncrease {
268+ } else if stepperState == .ShouldDecrease {
275+extension GMStepper {
276+ func handlePan(gesture: UIPanGestureRecognizer) {
277+ leftButton.enabled = false
278+ rightButton.enabled = false
242− @objc private func handlePan(gesture: UIPanGestureRecognizer) {
243 switch gesture.state {
280 switch gesture.state {
244 case .Changed:
281 case .Changed:
245 var translation = gesture.translationInView(label)
282 var translation = gesture.translationInView(label)
@@ -257,30 +294,27 @@ import UIKit
258 // When the label hits the edges, increase/decrease value and change button backgrounds
295 // When the label hits the edges, increase/decrease value and change button backgrounds
259 if label.center.x == labelMaximumCenterX {
296 if label.center.x == labelMaximumCenterX {
260−
297+ // If not hit the right edge before, increase the value and start the timer. If already hit the edge, do nothing. Timer will handle it.
261− // Increase only once for each pan gesture
262 if panState != .HitRightEdge {
298 if panState != .HitRightEdge {
263− value += 1
299+ stepperState = .ShouldIncrease
264− scheduleTimerWithUserInfo(rightButton)
265 panState = .HitRightEdge
300 panState = .HitRightEdge
268− if value == maximumValue {
303+ animateLimitHitIfNeeded()
269− animateLimitHitForButton(rightButton)
271 } else if label.center.x == labelMinimumCenterX {
304 } else if label.center.x == labelMinimumCenterX {
273 if panState != .HitLeftEdge {
305 if panState != .HitLeftEdge {
274− value -= 1
306+ stepperState = .ShouldDecrease
275− scheduleTimerWithUserInfo(leftButton)
276 panState = .HitLeftEdge
307 panState = .HitLeftEdge
279− if value == minimumValue {
310+ animateLimitHitIfNeeded()
280− animateLimitHitForButton(leftButton)
283− reset()
312+ panState = .Stable
313+ stepperState = .Stable
316+ self.rightButton.backgroundColor = self.buttonsBackgroundColor
317+ self.leftButton.backgroundColor = self.buttonsBackgroundColor
286 case .Ended:
320 case .Ended:
@@ -290,9 +324,12 @@ import UIKit
293− private func reset() {
327+ func reset() {
294− panState = .Stale
328+ panState = .Stable
329+ stepperState = .Stable
295 resetTimer()
330 resetTimer()
331+ leftButton.enabled = true
332+ rightButton.enabled = true
297 UIView.animateWithDuration(self.labelSlideDuration, animations: {
334 UIView.animateWithDuration(self.labelSlideDuration, animations: {
298 self.label.center = self.labelOriginalCenter
335 self.label.center = self.labelOriginalCenter
@@ -304,57 +341,55 @@ import UIKit
305// MARK: Button Events
342// MARK: Button Events
306extension GMStepper {
343extension GMStepper {
307− @objc private func leftButtonTouchDown(button: UIButton) {
344+ func leftButtonTouchDown(button: UIButton) {
345+ rightButton.enabled = false
346+ label.userInteractionEnabled = false
308 if value == minimumValue {
349 if value == minimumValue {
309− animateLimitHitForButton(leftButton)
350+ animateLimitHitIfNeeded()
311− value -= 1
352+ stepperState = .ShouldDecrease
313− scheduleTimerWithUserInfo(leftButton)
315 animateSlideLeft()
353 animateSlideLeft()
319− @objc private func leftButtonTouchUp(button: UIButton) {
324− @objc private func rightButtonTapped(button: UIButton) {
358+ func rightButtonTouchDown(button: UIButton) {
359+ leftButton.enabled = false
360+ label.userInteractionEnabled = false
325 if value == maximumValue {
363 if value == maximumValue {
326− animateLimitHitForButton(rightButton)
364+ animateLimitHitIfNeeded()
328− value += 1
366+ stepperState = .ShouldIncrease
330− scheduleTimerWithUserInfo(rightButton)
332 animateSlideRight()
367 animateSlideRight()
336− @objc private func rightButtonTouchUp(button: UIButton) {
371+ func buttonTouchUp(button: UIButton) {
338− stopTimer()
373+ label.userInteractionEnabled = true
342// MARK: Animations
377// MARK: Animations
343extension GMStepper {
378extension GMStepper {
345− private func animateSlideLeft() {
380+ func animateSlideLeft() {
346 UIView.animateWithDuration(labelSlideDuration) {
381 UIView.animateWithDuration(labelSlideDuration) {
347 self.label.center.x -= self.labelSlideLength
382 self.label.center.x -= self.labelSlideLength
351− private func animateSlideRight() {
386+ func animateSlideRight() {
352 UIView.animateWithDuration(labelSlideDuration) {
387 UIView.animateWithDuration(labelSlideDuration) {
353 self.label.center.x += self.labelSlideLength
388 self.label.center.x += self.labelSlideLength
357− private func animateToOriginalPosition() {
392+ func animateToOriginalPosition() {
358 if self.label.center != self.labelOriginalCenter {
393 if self.label.center != self.labelOriginalCenter {
359 UIView.animateWithDuration(labelSlideDuration) {
394 UIView.animateWithDuration(labelSlideDuration) {
360 self.label.center = self.labelOriginalCenter
395 self.label.center = self.labelOriginalCenter
@@ -362,32 +397,33 @@ extension GMStepper {
400+ func animateLimitHitIfNeeded() {
401+ if value == minimumValue {
402+ animateLimitHitForButton(leftButton)
403+ } else if value == maximumValue {
404+ animateLimitHitForButton(rightButton)
365− private func animateLimitHitForButton(button: UIButton){
408+ func animateLimitHitForButton(button: UIButton){
366 UIView.animateWithDuration(limitHitAnimationDuration) {
409 UIView.animateWithDuration(limitHitAnimationDuration) {
367 button.backgroundColor = self.limitHitAnimationColor
410 button.backgroundColor = self.limitHitAnimationColor
373// MARK: Timer
415// MARK: Timer
374extension GMStepper {
416extension GMStepper {
375 func handleTimerFire(timer: NSTimer) {
417 func handleTimerFire(timer: NSTimer) {
376 timerFireCount += 1
418 timerFireCount += 1
377− if timerFireCount % timerFireCountModulo == 0 {
379− if let button = timer.userInfo as? UIButton {
420+ if timerFireCount % timerFireCountModulo == 0 {
380− if button == leftButton {
421+ updateValue()
389− func scheduleTimerWithUserInfo(userInfo: AnyObject?) {
425+ func scheduleTimer() {
390− timer = NSTimer.scheduledTimerWithTimeInterval(timerInterval, target: self, selector: "handleTimerFire:", userInfo: userInfo, repeats: true)
426+ timer = NSTimer.scheduledTimerWithTimeInterval(timerInterval, target: self, selector: "handleTimerFire:", userInfo: nil, repeats: true)
393 func resetTimer() {
429 func resetTimer() {