on master help sign in
Refactoring and bug fixes
9619704Gunay Mert Karadogan
GMStepper/GMStepper.swift+136−100
@@ -11,7 +11,7 @@ import UIKit
11@IBDesignable public class GMStepper: UIControl {
11@IBDesignable public class GMStepper: UIControl {
12
12
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 {
15 didSet {
15 didSet {
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 {
27 didSet {
27 didSet {
28− value = 0
28+ value = min(maximumValue, max(minimumValue, value))
29 }
29 }
30 }
30 }
31
31
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 {
34 didSet {
34 didSet {
35− value = 0
35+ value = min(maximumValue, max(minimumValue, value))
36 }
36 }
37 }
37 }
38
38
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
41
41
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
118 }
118 }
119 }
119 }
120
120
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)
123+
121 /**
124 /**
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.
123 */
126 */
124− private let labelSlideLength: CGFloat = 5
127+ let labelSlideLength: CGFloat = 5
125
128
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)
131+
132+ /// Duration of the animation when the value hits the limit.
133+ let limitHitAnimationDuration = NSTimeInterval(0.1)
134+
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)
144+ return button
145+ }()
128
146
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)
156+ return button
157+ }()
131
158
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)
170+ return label
171+ }()
134
172
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!
138
176
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
142
181
143− private enum LabelPanState {
182+ enum StepperState {
144− case Stale, HitRightEdge, HitLeftEdge
183+ case Stable, ShouldIncrease, ShouldDecrease
184+ }
185+ var stepperState = StepperState.Stable {
186+ didSet {
187+ if stepperState != .Stable {
188+ updateValue()
189+ if autorepeat {
190+ scheduleTimer()
191+ }
192+ }
193+ }
145 }
194 }
146− private var panState = LabelPanState.Stale
147
195
196+ /// Timer used for autorepeat option
148− private var timer: NSTimer?
197+ var timer: NSTimer?
149
198
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.
155 */
204 */
156− private let timerInterval = NSTimeInterval(0.05)
205+ let timerInterval = NSTimeInterval(0.05)
157
206
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
167 }
216 }
168 }
217 }
169
218
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?
172
222
@@ -189,39 +239,14 @@ import UIKit
189 setup()
239 setup()
190 }
240 }
191
241
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)
201−
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)
210−
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)
217
246
218 backgroundColor = buttonsBackgroundColor
247 backgroundColor = buttonsBackgroundColor
219 layer.cornerRadius = cornerRadius
248 layer.cornerRadius = cornerRadius
220 clipsToBounds = true
249 clipsToBounds = true
221−
222− let panRecognizer = UIPanGestureRecognizer(target: self, action: "handlePan:")
223− label.userInteractionEnabled = true
224− label.addGestureRecognizer(panRecognizer)
225 }
250 }
226
251
227 public override func layoutSubviews() {
252 public override func layoutSubviews() {
@@ -237,9 +262,21 @@ import UIKit
237 labelOriginalCenter = label.center
262 labelOriginalCenter = label.center
238 }
263 }
239
264
265+ func updateValue() {
266+ if stepperState == .ShouldIncrease {
267+ value += 1
268+ } else if stepperState == .ShouldDecrease {
269+ value -= 1
270+ }
271+ }
272+}
240
273
274+// MARK: Pan Gesture
275+extension GMStepper {
276+ func handlePan(gesture: UIPanGestureRecognizer) {
277+ leftButton.enabled = false
278+ rightButton.enabled = false
241
279
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
257
294
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
266 }
301 }
267−
302+
268− if value == maximumValue {
303+ animateLimitHitIfNeeded()
269− animateLimitHitForButton(rightButton)
270− }
271 } else if label.center.x == labelMinimumCenterX {
304 } else if label.center.x == labelMinimumCenterX {
272−
273 if panState != .HitLeftEdge {
305 if panState != .HitLeftEdge {
274− value -= 1
306+ stepperState = .ShouldDecrease
275− scheduleTimerWithUserInfo(leftButton)
276 panState = .HitLeftEdge
307 panState = .HitLeftEdge
277 }
308 }
278
309
279− if value == minimumValue {
310+ animateLimitHitIfNeeded()
280− animateLimitHitForButton(leftButton)
281− }
282 } else {
311 } else {
283− reset()
312+ panState = .Stable
313+ stepperState = .Stable
314+ resetTimer()
315+
316+ self.rightButton.backgroundColor = self.buttonsBackgroundColor
317+ self.leftButton.backgroundColor = self.buttonsBackgroundColor
284 }
318 }
285
319
286 case .Ended:
320 case .Ended:
@@ -290,9 +324,12 @@ import UIKit
290 }
324 }
291 }
325 }
292
326
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
296
333
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
304
341
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
347+ resetTimer()
348+
308 if value == minimumValue {
349 if value == minimumValue {
309− animateLimitHitForButton(leftButton)
350+ animateLimitHitIfNeeded()
310 } else {
351 } else {
311− value -= 1
352+ stepperState = .ShouldDecrease
312− if autorepeat {
313− scheduleTimerWithUserInfo(leftButton)
314− }
315 animateSlideLeft()
353 animateSlideLeft()
316 }
354 }
317− }
318
355
319− @objc private func leftButtonTouchUp(button: UIButton) {
320− reset()
321− stopTimer()
322 }
356 }
323
357
324− @objc private func rightButtonTapped(button: UIButton) {
358+ func rightButtonTouchDown(button: UIButton) {
359+ leftButton.enabled = false
360+ label.userInteractionEnabled = false
361+ resetTimer()
362+
325 if value == maximumValue {
363 if value == maximumValue {
326− animateLimitHitForButton(rightButton)
364+ animateLimitHitIfNeeded()
327 } else {
365 } else {
328− value += 1
366+ stepperState = .ShouldIncrease
329− if autorepeat {
330− scheduleTimerWithUserInfo(rightButton)
331− }
332 animateSlideRight()
367 animateSlideRight()
333 }
368 }
334 }
369 }
335
370
336− @objc private func rightButtonTouchUp(button: UIButton) {
371+ func buttonTouchUp(button: UIButton) {
337 reset()
372 reset()
338− stopTimer()
373+ label.userInteractionEnabled = true
339 }
374 }
340}
375}
341
376
342// MARK: Animations
377// MARK: Animations
343extension GMStepper {
378extension GMStepper {
344
379
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
348 }
383 }
349 }
384 }
350
385
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
354 }
389 }
355 }
390 }
356
391
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 {
362 }
397 }
363 }
398 }
364
399
400+ func animateLimitHitIfNeeded() {
401+ if value == minimumValue {
402+ animateLimitHitForButton(leftButton)
403+ } else if value == maximumValue {
404+ animateLimitHitForButton(rightButton)
405+ }
406+ }
407+
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
368 }
411 }
369 }
412 }
370−
371}
413}
372
414
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 {
378
419
379− if let button = timer.userInfo as? UIButton {
420+ if timerFireCount % timerFireCountModulo == 0 {
380− if button == leftButton {
421+ updateValue()
381− value -= 1
382− } else {
383− value += 1
384− }
385− }
386 }
422 }
387 }
423 }
388
424
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)
391 }
427 }
392
428
393 func resetTimer() {
429 func resetTimer() {