on master help sign in
Improve display to avoid rounding imprecision when using non-integer step values. Fix for issue #36.
338fa1eBrent Whitman
GMStepper/GMStepper.swift+47−41
@@ -15,26 +15,26 @@ import UIKit
15 didSet {
15 didSet {
16 value = min(maximumValue, max(minimumValue, value))
16 value = min(maximumValue, max(minimumValue, value))
17
17
18− let isInteger = floor(value) == value
19−
20− //
21− // If we have items, we will display them as steps
22− //
23−
24− if isInteger && stepValue == 1.0 && items.count > 0 {
25− label.text = items[Int(value)]
18+ label.text = formattedValue
26− }
27− else if showIntegerIfDoubleIsInteger && isInteger {
28− label.text = String(stringInterpolationSegment: Int(value))
29− } else {
30− label.text = String(stringInterpolationSegment: value)
31− }
32
19
33 if oldValue != value {
20 if oldValue != value {
34 sendActions(for: .valueChanged)
21 sendActions(for: .valueChanged)
35 }
22 }
36 }
23 }
37 }
24 }
25+
26+ private var formattedValue: String? {
27+ let isInteger = Decimal(value).exponent >= 0
28+
29+ // If we have items, we will display them as steps
30+ if isInteger && stepValue == 1.0 && items.count > 0 {
31+ return items[Int(value)]
32+ }
33+ else {
34+ return formatter.string(from: NSNumber(value: value))
35+ }
36+ }
37+
38
38
39 /// Minimum value. Must be less than maximumValue. Defaults to 0.
39 /// Minimum value. Must be less than maximumValue. Defaults to 0.
40 @objc @IBInspectable public var minimumValue: Double = 0 {
40 @objc @IBInspectable public var minimumValue: Double = 0 {
@@ -51,13 +51,21 @@ import UIKit
51 }
51 }
52
52
53 /// Step/Increment value as in UIStepper. Defaults to 1.
53 /// Step/Increment value as in UIStepper. Defaults to 1.
54− @objc @IBInspectable public var stepValue: Double = 1
54+ @objc @IBInspectable public var stepValue: Double = 1 {
55+ didSet {
56+ setupNumberFormatter()
57+ }
58+ }
55
59
56 /// The same as UIStepper's autorepeat. If true, holding on the buttons or keeping the pan gesture alters the value repeatedly. Defaults to true.
60 /// The same as UIStepper's autorepeat. If true, holding on the buttons or keeping the pan gesture alters the value repeatedly. Defaults to true.
57 @objc @IBInspectable public var autorepeat: Bool = true
61 @objc @IBInspectable public var autorepeat: Bool = true
58
62
59 /// If the value is integer, it is shown without floating point.
63 /// If the value is integer, it is shown without floating point.
60− @objc @IBInspectable public var showIntegerIfDoubleIsInteger: Bool = true
64+ @objc @IBInspectable public var showIntegerIfDoubleIsInteger: Bool = true {
65+ didSet {
66+ setupNumberFormatter()
67+ }
68+ }
61
69
62 /// Text on the left button. Be sure that it fits in the button. Defaults to "−".
70 /// Text on the left button. Be sure that it fits in the button. Defaults to "−".
63 @objc @IBInspectable public var leftButtonText: String = "−" {
71 @objc @IBInspectable public var leftButtonText: String = "−" {
@@ -164,6 +172,9 @@ import UIKit
164 /// Color of the flashing animation on the buttons in case the value hit the limit.
172 /// Color of the flashing animation on the buttons in case the value hit the limit.
165 @objc @IBInspectable public var limitHitAnimationColor: UIColor = UIColor(red:0.26, green:0.6, blue:0.87, alpha:1)
173 @objc @IBInspectable public var limitHitAnimationColor: UIColor = UIColor(red:0.26, green:0.6, blue:0.87, alpha:1)
166
174
175+ /// Formatter for displaying the current value
176+ let formatter = NumberFormatter()
177+
167 /**
178 /**
168 Width of the sliding animation. When buttons clicked, the middle label does a slide animation towards to the clicked button. Defaults to 5.
179 Width of the sliding animation. When buttons clicked, the middle label does a slide animation towards to the clicked button. Defaults to 5.
169 */
180 */
@@ -204,11 +215,7 @@ import UIKit
204 lazy var label: UILabel = {
215 lazy var label: UILabel = {
205 let label = UILabel()
216 let label = UILabel()
206 label.textAlignment = .center
217 label.textAlignment = .center
207− if self.showIntegerIfDoubleIsInteger && floor(self.value) == self.value {
218+ label.text = formattedValue
208− label.text = String(stringInterpolationSegment: Int(self.value))
209− } else {
210− label.text = String(stringInterpolationSegment: self.value)
211− }
212 label.textColor = self.labelTextColor
219 label.textColor = self.labelTextColor
213 label.backgroundColor = self.labelBackgroundColor
220 label.backgroundColor = self.labelBackgroundColor
214 label.font = self.labelFont
221 label.font = self.labelFont
@@ -247,24 +254,7 @@ import UIKit
247
254
248 @objc public var items : [String] = [] {
255 @objc public var items : [String] = [] {
249 didSet {
256 didSet {
250− let isInteger = floor(value) == value
251−
252− //
253− // If we have items, we will display them as steps
254− //
255−
256− if isInteger && stepValue == 1.0 && items.count > 0 {
257−
258− var value = Int(self.value)
259−
260− if value >= items.count {
261− value = items.count - 1
262− self.value = Double(value)
263− }
264− else {
265− label.text = items[value]
257+ label.text = formattedValue
266− }
267− }
268 }
258 }
269 }
259 }
270
260
@@ -301,7 +291,7 @@ import UIKit
301 setup()
291 setup()
302 }
292 }
303
293
304− func setup() {
294+ fileprivate func setup() {
305 addSubview(leftButton)
295 addSubview(leftButton)
306 addSubview(rightButton)
296 addSubview(rightButton)
307 addSubview(label)
297 addSubview(label)
@@ -311,8 +301,18 @@ import UIKit
311 clipsToBounds = true
301 clipsToBounds = true
312 labelOriginalCenter = label.center
302 labelOriginalCenter = label.center
313
303
304+ setupNumberFormatter()
305+
314 NotificationCenter.default.addObserver(self, selector: #selector(GMStepper.reset), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
306 NotificationCenter.default.addObserver(self, selector: #selector(GMStepper.reset), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
315 }
307 }
308+
309+ func setupNumberFormatter() {
310+ let decValue = Decimal(stepValue)
311+ let digits = decValue.significantFractionalDecimalDigits
312+ formatter.minimumIntegerDigits = 1
313+ formatter.minimumFractionDigits = showIntegerIfDoubleIsInteger ? 0 : digits
314+ formatter.maximumFractionDigits = digits
315+ }
316
316
317 public override func layoutSubviews() {
317 public override func layoutSubviews() {
318 let buttonWidth = bounds.size.width * ((1 - labelWidthWeight) / 2)
318 let buttonWidth = bounds.size.width * ((1 - labelWidthWeight) / 2)
@@ -332,8 +332,9 @@ import UIKit
332 value += stepValue
332 value += stepValue
333 } else if stepperState == .ShouldDecrease {
333 } else if stepperState == .ShouldDecrease {
334 value -= stepValue
334 value -= stepValue
335− }
335+ }
336 }
336 }
337+
337
338
338 deinit {
339 deinit {
339 resetTimer()
340 resetTimer()
@@ -516,6 +517,11 @@ extension GMStepper {
516 timerFireCount = 0
517 timerFireCount = 0
517 }
518 }
518 }
519 }
520+}
519
521
520
522
523+extension Decimal {
524+ var significantFractionalDecimalDigits: Int {
525+ return max(-exponent, 0)
526+ }
521}
527}
GMStepperExample/GMStepperExample/Base.lproj/Main.storyboard+1−1
@@ -56,7 +56,7 @@
56 <color key="value" red="0.90823972230000005" green="0.92638683320000004" blue="0.93171715740000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
56 <color key="value" red="0.90823972230000005" green="0.92638683320000004" blue="0.93171715740000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
57 </userDefinedRuntimeAttribute>
57 </userDefinedRuntimeAttribute>
58 <userDefinedRuntimeAttribute type="number" keyPath="stepValue">
58 <userDefinedRuntimeAttribute type="number" keyPath="stepValue">
59− <real key="value" value="0.5"/>
59+ <real key="value" value="0.1"/>
60 </userDefinedRuntimeAttribute>
60 </userDefinedRuntimeAttribute>
61 </userDefinedRuntimeAttributes>
61 </userDefinedRuntimeAttributes>
62 </view>
62 </view>