Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ Carthage/Build

fastlane/report.xml
fastlane/screenshots

.idea
.DS_Store
205 changes: 157 additions & 48 deletions Class/PFStepper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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())
Expand All @@ -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 {
Expand All @@ -89,18 +150,18 @@ 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.
- For the next 1.5 sec, it changes the value every 0.1 sec.
- 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 {
Expand All @@ -112,79 +173,127 @@ 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
} else if stepperState == .shouldDecrease {
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
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()
}
Expand All @@ -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()
Expand Down
Loading