diff --git a/.gitignore b/.gitignore index 14cfb73..9f83e22 100644 --- a/.gitignore +++ b/.gitignore @@ -61,4 +61,5 @@ Carthage/Build fastlane/report.xml fastlane/screenshots - +.idea +.DS_Store diff --git a/Class/PFStepper.swift b/Class/PFStepper.swift index 9b25e7c..63eab4f 100644 --- a/Class/PFStepper.swift +++ b/Class/PFStepper.swift @@ -8,21 +8,35 @@ import UIKit -open class PFStepper: UIControl { - open var value: Double = 0 { +@IBDesignable open class PFStepper: UIControl { + fileprivate var value: Double = 0 { didSet { value = min(maximumValue, max(minimumValue, value)) - let isInteger = floor(value) == value - + if needsAnimation { + let animation = CATransition() + animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + animation.type = kCATransitionFade + animation.duration = 0.75 + bottomButton.layer.add(animation, forKey: "kCATransitionFade") + topButton.layer.add(animation, forKey: "kCATransitionFade") + } if showIntegerIfDoubleIsInteger && isInteger { - topButton.setTitle(String(stringInterpolationSegment: Int(value)), for: UIControlState()) - bottomButton.setTitle(String(stringInterpolationSegment: Int(value + stepValue)), for: UIControlState()) + if value <= minimumValue { + topButton.setTitle("", for: UIControlState()) + bottomButton.setTitle(String(stringInterpolationSegment: Int(value)), for: UIControlState()) + } else if value > maximumValue { + bottomButton.setTitle("", for: UIControlState()) + topButton.setTitle(String(stringInterpolationSegment: Int(value)), for: UIControlState()) + } else { + topButton.setTitle(String(stringInterpolationSegment: Int(value - stepValue)), for: UIControlState()) + bottomButton.setTitle(String(stringInterpolationSegment: Int(value)), for: UIControlState()) + } } else { topButton.setTitle(String(stringInterpolationSegment: value), for: UIControlState()) bottomButton.setTitle(String(stringInterpolationSegment: value + stepValue), for: UIControlState()) } - + if oldValue != value { sendActions(for: .valueChanged) } @@ -33,22 +47,46 @@ open class PFStepper: UIControl { topButton.backgroundColor = UIColor(red: 238/255.0, green: 238/255.0, blue: 238/255.0, alpha: 1) topButton.alpha = 0.5 } - if value >= maximumValue { + if value > maximumValue { bottomButton.setTitle("", for: UIControlState()) - } else { } } } - open var minimumValue: Double = 0 - open var maximumValue: Double = 24 - open var stepValue: Double = 1 - open var autorepeat: Bool = true - open var showIntegerIfDoubleIsInteger: Bool = true - open var topButtonText: String = "" - open var bottomButtonText: String = "1" - open var buttonsTextColor: UIColor = UIColor(red: 0.0/255.0, green: 122.0/255.0, blue: 255.0/255.0, alpha: 1.0) - open var buttonsBackgroundColor: UIColor = UIColor.white - open var buttonsFont = UIFont(name: "AvenirNext-Bold", size: 20.0)! + @IBInspectable open var minimumValue: Double = 0 { + didSet { + if minimumValue > maximumValue { + maximumValue = minimumValue + } + initButtonValues() + } + } + @IBInspectable open var maximumValue: Double = 24 { + didSet { + if maximumValue < minimumValue { + minimumValue = maximumValue + } + initButtonValues() + } + } + @IBInspectable open var stepValue: Double = 1 + @IBInspectable open var autorepeat: Bool = true + @IBInspectable open var showIntegerIfDoubleIsInteger: Bool = true + @IBInspectable open var needsAnimation: Bool = false + fileprivate var topButtonText: String = "" + fileprivate var bottomButtonText: String = "" + @IBInspectable open var buttonsTextColor: UIColor = UIColor(red: 0.0 / 255.0, green: 122.0 / 255.0, blue: 255.0 / 255.0, alpha: 1.0) { + didSet { + bottomButton.setTitleColor(buttonsTextColor, for: UIControlState()) + topButton.setTitleColor(buttonsTextColor, for: UIControlState()) + } + } + @IBInspectable open var buttonsBackgroundColor: UIColor = UIColor.white { + didSet { + bottomButton.backgroundColor = buttonsBackgroundColor + topButton.backgroundColor = buttonsBackgroundColor + } + } + @IBInspectable open var buttonsFont = UIFont(name: "AvenirNext-Bold", size: 20.0)! lazy var topButton: UIButton = { let button = UIButton() button.setTitle(self.topButtonText, for: UIControlState()) @@ -74,11 +112,34 @@ open class PFStepper: UIControl { button.addTarget(self, action: #selector(PFStepper.buttonTouchUp(_:)), for: UIControlEvents.touchUpOutside) return button }() - + + func createBottomUpAnimateButton(_ value: Int) -> UIButton { + let button = UIButton() + button.setTitle(String(stringInterpolationSegment: Int(value)), for: UIControlState()) + button.setTitleColor(self.buttonsTextColor, for: UIControlState()) + button.backgroundColor = self.backgroundColor + button.titleLabel?.font = self.buttonsFont + button.frame = CGRect(x: 0, y: bounds.size.height / 2, width: bounds.size.width, + height: bounds.size.height / 2) + return button + } + + func createUpDownAnimateButton(_ value: Int) -> UIButton { + let button = UIButton() + button.setTitle(String(stringInterpolationSegment: Int(value)), for: UIControlState()) + button.setTitleColor(self.buttonsTextColor, for: UIControlState()) + button.backgroundColor = self.buttonsBackgroundColor + button.titleLabel?.font = self.buttonsFont + button.alpha = 0.5 + button.frame = CGRect(x: 0, y: 0, width: bounds.size.width, + height: bounds.size.height / 2) + return button + } + enum StepperState { case stable, shouldIncrease, shouldDecrease } - + var stepperState = StepperState.stable { didSet { if stepperState != .stable { @@ -89,10 +150,10 @@ open class PFStepper: UIControl { } } } - + let limitHitAnimationDuration = TimeInterval(0.1) var timer: Timer? - + /** When UIStepper reaches its top speed, it alters the value with a time interval of ~0.05 sec. The user pressing and holding on the stepper repeatedly: - First 2.5 sec, the stepper changes the value every 0.5 sec. @@ -100,7 +161,7 @@ open class PFStepper: UIControl { - Then, every 0.05 sec. */ let timerInterval = TimeInterval(0.05) - + /// 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. var timerFireCount = 0 var timerFireCountModulo: Int { @@ -112,32 +173,33 @@ open class PFStepper: UIControl { return 10 // 0.05 sec * 10 = 0.5 sec } } - + required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } - + public override init(frame: CGRect) { super.init(frame: frame) setup() } - + func setup() { + initButtonValues() addSubview(topButton) addSubview(bottomButton) - + backgroundColor = buttonsBackgroundColor NotificationCenter.default.addObserver(self, selector: #selector(PFStepper.reset), name: NSNotification.Name.UIApplicationWillResignActive, object: nil) } - + open override func layoutSubviews() { let buttonWidth = bounds.size.width - + topButton.frame = CGRect(x: 0, y: 0, width: buttonWidth, height: bounds.size.height / 2) bottomButton.frame = CGRect(x: 0, y: bounds.size.height / 2, width: buttonWidth, height: bounds.size.height / 2) } - + func updateValue() { if stepperState == .shouldIncrease { value += stepValue @@ -145,11 +207,52 @@ open class PFStepper: UIControl { value -= stepValue } } - + + func initButtonValues() { + value = min(maximumValue, minimumValue) + self.bottomButtonText = String(stringInterpolationSegment: Int(value)) + self.bottomButton.setTitle(bottomButtonText, for: UIControlState()) + } + + deinit { resetTimer() NotificationCenter.default.removeObserver(self) } + + func animateBottomUp(value: Int) { + let button = createBottomUpAnimateButton(value) + addSubview(button) + UIView.animate(withDuration: 0.5 + , delay: 0.2 + , options: UIViewAnimationOptions.curveEaseInOut + , animations: { + button.frame = CGRect(x: 0, y: 0, width: self.bounds.size.width, + height: self.bounds.size.height / 2) + button.backgroundColor = self.buttonsBackgroundColor + } + , completion: { finished in + button.alpha = 0.5 + button.removeFromSuperview() + }) + } + + func animateUpDown(value: Int) { + let button = createUpDownAnimateButton(value) + addSubview(button) + UIView.animate(withDuration: 0.5 + , delay: 0.2 + , options: UIViewAnimationOptions.curveEaseInOut + , animations: { + button.backgroundColor = self.buttonsBackgroundColor + button.frame = CGRect(x: 0, y: self.bounds.size.height / 2, width: self.bounds.size.width, + height: self.bounds.size.height / 2) + } + , completion: { finished in + button.alpha = 1.0 + button.removeFromSuperview() + }) + } } // MARK: - Button Events @@ -157,34 +260,40 @@ extension PFStepper { func reset() { stepperState = .stable resetTimer() - + topButton.isEnabled = true bottomButton.isEnabled = true } - + func topButtonTouchDown(_ button: UIButton) { bottomButton.isEnabled = false resetTimer() - - if value == minimumValue { - button.setTitle("", for: UIControlState()) - } else { + + if Int(value) - Int(stepValue) >= Int(minimumValue) { + if needsAnimation { + animateUpDown(value: Int(value)) + } stepperState = .shouldDecrease } - + } - + func bottomButtonTouchDown(_ button: UIButton) { topButton.isEnabled = false resetTimer() - - if value == maximumValue { - button.setTitle("", for: UIControlState()) - } else { + + if Int(value) <= Int(maximumValue) { + if needsAnimation { + animateBottomUp(value: Int(value)) + } stepperState = .shouldIncrease + } else if Int(value) + 2 * Int(stepValue) <= Int(maximumValue) { + if needsAnimation { + animateBottomUp(value: Int(value)) + } } } - + func buttonTouchUp(_ button: UIButton) { reset() } @@ -194,16 +303,16 @@ extension PFStepper { extension PFStepper { func handleTimerFire(_ timer: Timer) { timerFireCount += 1 - + if timerFireCount % timerFireCountModulo == 0 { updateValue() } } - + func scheduleTimer() { timer = Timer.scheduledTimer(timeInterval: timerInterval, target: self, selector: #selector(PFStepper.handleTimerFire(_:)), userInfo: nil, repeats: true) } - + func resetTimer() { if let timer = timer { timer.invalidate() diff --git a/PFStepperDemo/PFStepperDemo/Base.lproj/Main.storyboard b/PFStepperDemo/PFStepperDemo/Base.lproj/Main.storyboard index 3a2a49b..5455835 100644 --- a/PFStepperDemo/PFStepperDemo/Base.lproj/Main.storyboard +++ b/PFStepperDemo/PFStepperDemo/Base.lproj/Main.storyboard @@ -1,21 +1,58 @@ - - + + + + + - + + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 7b4739f..c00a26b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ It may be the most elegant stepper you have ever had! -![Sample](Sample.gif) +![Sample](pfstepper-sample-2.gif) ## Usage @@ -26,4 +26,4 @@ To be written. ## License -Released under the MIT License. \ No newline at end of file +Released under the MIT License. diff --git a/pfstepper-sample-2.gif b/pfstepper-sample-2.gif new file mode 100644 index 0000000..f7150a2 Binary files /dev/null and b/pfstepper-sample-2.gif differ