Skip to content

Commit 8e446b1

Browse files
author
Xavier Schott
committed
Fixed #47. Fixed #48
1 parent 2d830d4 commit 8e446b1

13 files changed

Lines changed: 210 additions & 51 deletions

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ Ideal to select individual steps, star rating, integers, rainbow colors, slices
2222
Ideal to represent steps. *The discrete slider and the camel labels can work in unison.*
2323

2424
## Compatibility
25-
1. Written in **Swift 3**, can be integrated with **Swift** or **Obj-C**
25+
1. Written in **Swift 4**, can be integrated with **Swift** or **Obj-C**
2626
2. `TGPControls` are **AutoLayout**, `IB Designable` and `IB Inspectable` ready
27-
3. Version **5.0.1** comes with a **Swift 4** demo application for **iOS 8** and above.
27+
3. Version **5.0.1** and better comes with a **Swift 4** demo application for **iOS 8** and above.
2828
_**iOS 7** supported in versions 2.1.0 and earlier_
29+
4. Automatic support for both left-to-right and right-to-left languages
2930

3031
![imagessliderdemo](https://cloud.githubusercontent.com/assets/4073988/6628373/183c7452-c8c2-11e4-9a63-107805bc0cc4.gif)
3132

@@ -82,7 +83,9 @@ All graphic aspects, such as physical spacing of the ticks or physical width of
8283
```
8384
discreteSlider.ticksListener = camelLabels
8485
```
86+
8587
![complete](https://cloud.githubusercontent.com/assets/4073988/5912616/26cf1b0a-a58b-11e4-92f7-f9dbcd53c413.gif)
88+
8689
That's all!
8790
Through the `TGPControlsTicksProtocol`, the camelLabels listen to _value_ and _size_ changes. You may want to adopt this protocol to handle changes, or use the traditional `UIControl` notifications:
8891

@@ -118,23 +121,27 @@ self.stepper.value = Double(sender.value)
118121

119122
| Tick | |
120123
|:-----| ----- |
124+
| `tickStyle` | See style property |
121125
| `tickSize` | absolute dimension |
122-
| `tickImage` | a `UIImage`, fits in `tickSize` (†) |
123126
| `tickCount` | discrete steps, must be 2 or greater |
127+
| `tickTintColor` | takes precedence over the control `tintColor` |
128+
| `tickImage` | a `UIImage`, fits in `tickSize` (†) |
124129
| `ticksDistance` | horizontal spacing _(calculated)_ |
125130

126131
| Track | |
127132
|:------| ----- |
133+
| `trackStyle` | See style property |
128134
| `trackThickness` | height |
129135
| `trackImage` | a `UIImage`, ignores `trackThickness` (†) |
130136
| `minimumTrackTintColor` | track lower side |
131137
| `maximumTrackTintColor` | track higher side |
132138

133139
| Thumb | |
134140
|:------| ----- |
141+
| `thumbStyle` | See style property |
135142
| `thumbSize` | absolute size |
136-
| `thumbImage` | a`UIImage`, dictates `thumbSize` (†) |
137143
| `thumbTintColor` | background |
144+
| `thumbImage` | a`UIImage`, dictates `thumbSize` (†) |
138145
| `thumbShadowRadius` | breaking the _flat design concept_ |
139146
| `thumbShadowOffset` | applied to `thumbShadowRadius`, may affect control bounds |
140147

@@ -159,8 +166,9 @@ Most of the customization can be done inside **Interface Builder**.
159166

160167
| Property | |
161168
|:---------| ----- |
162-
| `ticksListener` | ties a discrete slider to its camel labels. This is your most robust method to not only ensure that the layout of both controls match exactly, but also adjust this spacing when orientation changes. A typical use may be `discreteSlider.ticksListener = camelLabels` |
169+
| `tickCount` | **design only** (_preferrably not to be used_): the number of ticks is driven by the number of elements in the `names` array |
163170
| `names` | supplies a new set of labels ; supersedes the `tickCount` property, which will return the number of labels. A typical use may be `camelLabels.names = ["OFF", "ON"]` |
171+
| `ticksListener` | ties a discrete slider to its camel labels. This is your most robust method to not only ensure that the layout of both controls match exactly, but also adjust this spacing when orientation changes. A typical use may be `discreteSlider.ticksListener = camelLabels` |
164172
| `ticksDistance` | _override_ the labels spacing entirely ; **prefer** the `ticksListener` mechanism if it is available to you. A typical use may be `camelLabels.ticksDistance = 15` |
165173
| `value` | which label is emphasized (_selected_) |
166174
| `backgroundColor` | labels become *tap-through* (**click-through**) when set to `UIColor.clear` ; use TGPCamelLabels *on top of* other UI elements, **even native iOS objects**!(*) |
@@ -170,6 +178,7 @@ Most of the customization can be done inside **Interface Builder**.
170178

171179
| Edges & Animation | |
172180
|:------------------| ----- |
181+
| `numberOfLiness` | Support for multiple lines labels |
173182
| `offCenter` | **leftmost and righmost labels only**: relative inset expressed as a proportion of individual label width: 0: none, +0.5: nudge in by a half width (fully fit) or -0.5: draw completely outside |
174183
| `insets` | **leftmost and righmost labels only**: absolute inset expressed in pixels |
175184
| `emphasisLayout` | emphasized (_selected_) labels vertical alignment ; `.top`, `.centerY` or `.bottom`. Default is `.top` (‡) |

TGPControls.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Pod::Spec.new do |spec|
22

33
# ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
44
spec.name = "TGPControls"
5-
spec.version = "5.0.2"
5+
spec.version = "5.0.3"
66
spec.summary = "Custom animated iOS controls: Animated discrete slider, animated labels"
77

88
spec.description = <<-DESC
@@ -26,7 +26,7 @@ Pod::Spec.new do |spec|
2626
spec.swift_version = '4.0'
2727

2828
# ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
29-
spec.source = { :git => "https://github.com/SwiftArchitect/TGPControls.git", :tag => "v5.0.2" }
29+
spec.source = { :git => "https://github.com/SwiftArchitect/TGPControls.git", :tag => "v5.0.3" }
3030
spec.source_files = "TGPControls/**/*.{swift}"
3131
spec.exclude_files = "TGPControlsDemo/*"
3232

TGPControls/TGPCamelLabels.swift

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,11 @@ public class TGPCamelLabels: TGPCamelLabels_INTERFACE_BUILDER {
7878
}
7979
}
8080

81-
@IBInspectable public var numberOfLinesInLabel:Int = 1 {
82-
didSet {
83-
layoutTrack()
84-
}
85-
}
81+
@IBInspectable public var numberOfLinesInLabel:Int = 1 {
82+
didSet {
83+
layoutTrack()
84+
}
85+
}
8686

8787
// Label off-center to the left and right of the slider
8888
// expressed in label width. 0: none, -1/2: half outside, 1/2; half inside
@@ -166,6 +166,7 @@ public class TGPCamelLabels: TGPCamelLabels_INTERFACE_BUILDER {
166166
var lastValue = NSNotFound
167167
var emphasizedLabels:[UILabel] = []
168168
var regularLabels:[UILabel] = []
169+
var localeCharacterDirection = CFLocaleLanguageDirection.leftToRight
169170

170171
// MARK: UIView
171172

@@ -195,6 +196,11 @@ public class TGPCamelLabels: TGPCamelLabels_INTERFACE_BUILDER {
195196
// MARK: TGPCamelLabels
196197

197198
func initProperties() {
199+
if let systemLocale = CFLocaleCopyCurrent(),
200+
let localeIdentifier = CFLocaleGetIdentifier(systemLocale) {
201+
localeCharacterDirection = CFLocaleGetLanguageCharacterDirection(localeIdentifier.rawValue)
202+
}
203+
198204
debugNames(count: 10)
199205
layoutTrack()
200206
}
@@ -234,10 +240,16 @@ public class TGPCamelLabels: TGPCamelLabels_INTERFACE_BUILDER {
234240
let count = names.count
235241
if count > 0 {
236242
var centerX = (bounds.width - (CGFloat(count - 1) * ticksDistance))/2.0
243+
if .rightToLeft == localeCharacterDirection {
244+
centerX = bounds.width - centerX
245+
}
246+
let tickSpacing = (.rightToLeft == localeCharacterDirection)
247+
? -ticksDistance
248+
: ticksDistance
237249
let centerY = bounds.height / 2.0
238250
for name in names {
239251
let upLabel = UILabel.init()
240-
upLabel.numberOfLines = self.numberOfLinesInLabel
252+
upLabel.numberOfLines = numberOfLinesInLabel
241253
emphasizedLabels.append(upLabel)
242254
upLabel.text = name
243255
if let upFontName = upFontName {
@@ -261,7 +273,7 @@ public class TGPCamelLabels: TGPCamelLabels_INTERFACE_BUILDER {
261273
addSubview(upLabel)
262274

263275
let dnLabel = UILabel.init()
264-
dnLabel.numberOfLines = self.numberOfLinesInLabel
276+
dnLabel.numberOfLines = numberOfLinesInLabel
265277
regularLabels.append(dnLabel)
266278
dnLabel.text = name
267279
if let downFontName = downFontName {
@@ -279,15 +291,21 @@ public class TGPCamelLabels: TGPCamelLabels_INTERFACE_BUILDER {
279291
}()
280292
addSubview(dnLabel)
281293

282-
centerX += ticksDistance
294+
centerX += tickSpacing
283295
}
284296

285297
// Fix left and right label, if there are at least 2 labels
286298
if names.count > 1 {
287-
insetLabel(emphasizedLabels.first, withInset: insets, andMultiplier: offCenter)
288-
insetLabel(emphasizedLabels.last, withInset: -insets, andMultiplier: -offCenter)
289-
insetLabel(regularLabels.first, withInset: insets, andMultiplier: offCenter)
290-
insetLabel(regularLabels.last, withInset: -insets, andMultiplier: -offCenter)
299+
let localeInsets = (.rightToLeft == localeCharacterDirection)
300+
? -insets
301+
: insets
302+
let localeOffCenter = (.rightToLeft == localeCharacterDirection)
303+
? -offCenter
304+
: offCenter
305+
insetLabel(emphasizedLabels.first, withInset: localeInsets, andMultiplier: localeOffCenter)
306+
insetLabel(emphasizedLabels.last, withInset: -localeInsets, andMultiplier: -localeOffCenter)
307+
insetLabel(regularLabels.first, withInset: localeInsets, andMultiplier: localeOffCenter)
308+
insetLabel(regularLabels.last, withInset: -localeInsets, andMultiplier: -localeOffCenter)
291309
}
292310

293311
dockEffect(duration:0.0)

TGPControls/TGPDiscreteSlider.swift

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,6 @@ public class TGPDiscreteSlider:TGPSlider_INTERFACE_BUILDER {
208208
let segments = CGFloat(max(1, tickCount - 1))
209209
return trackRectangle.width / segments
210210
}
211-
set {}
212211
}
213212

214213
@objc public var ticksListener:TGPControlsTicksProtocol? = nil {
@@ -221,15 +220,18 @@ public class TGPDiscreteSlider:TGPSlider_INTERFACE_BUILDER {
221220
var intValue:Int = 0
222221
var intMinimumValue = -5
223222

224-
var ticksAbscisses:[CGPoint] = []
225-
var thumbAbscisse:CGFloat = 0
223+
var ticksAbscissae:[CGPoint] = []
224+
var thumbAbscissa:CGFloat = 0
226225
var thumbLayer = CALayer()
227226
var leftTrackLayer = CALayer()
228227
var rightTrackLayer = CALayer()
229228
var trackLayer = CALayer()
229+
var leadingTrackLayer: CALayer!
230+
var trailingTrackLayer: CALayer!
230231
var ticksLayer = CALayer()
231232
var trackRectangle = CGRect.zero
232233
var touchedInside = false
234+
var localeCharacterDirection = CFLocaleLanguageDirection.leftToRight
233235

234236
let iOSThumbShadowRadius:CGFloat = 4
235237
let iOSThumbShadowOffset = CGSize(width:0, height:3)
@@ -262,16 +264,29 @@ public class TGPDiscreteSlider:TGPSlider_INTERFACE_BUILDER {
262264
// MARK: TGPDiscreteSlider
263265

264266
func initProperties() {
267+
if let systemLocale = CFLocaleCopyCurrent(),
268+
let localeIdentifier = CFLocaleGetIdentifier(systemLocale) {
269+
localeCharacterDirection = CFLocaleGetLanguageCharacterDirection(localeIdentifier.rawValue)
270+
}
271+
272+
leadingTrackLayer = (.rightToLeft == localeCharacterDirection)
273+
? rightTrackLayer
274+
: leftTrackLayer
275+
trailingTrackLayer = (.rightToLeft == localeCharacterDirection)
276+
? leftTrackLayer
277+
: rightTrackLayer
278+
265279
// Track is a clear clipping layer, and left + right sublayers, which brings in free animation
266280
trackLayer.masksToBounds = true
267281
trackLayer.backgroundColor = UIColor.clear.cgColor
268282
layer.addSublayer(trackLayer)
283+
trackLayer.addSublayer(leftTrackLayer)
284+
trackLayer.addSublayer(rightTrackLayer)
285+
269286
if let backgroundColor = tintColor {
270-
leftTrackLayer.backgroundColor = backgroundColor.cgColor
287+
leadingTrackLayer.backgroundColor = backgroundColor.cgColor
271288
}
272-
trackLayer.addSublayer(leftTrackLayer)
273289
rightTrackLayer.backgroundColor = maximumTrackTintColor.cgColor
274-
trackLayer.addSublayer(rightTrackLayer)
275290

276291
// Ticks in between track and thumb
277292
layer.addSublayer(ticksLayer)
@@ -300,7 +315,7 @@ public class TGPDiscreteSlider:TGPSlider_INTERFACE_BUILDER {
300315
fallthrough
301316

302317
case .image:
303-
for originPoint in ticksAbscisses {
318+
for originPoint in ticksAbscissae {
304319
let rectangle = CGRect(x: originPoint.x-(tickSize.width/2),
305320
y: originPoint.y-(tickSize.height/2),
306321
width: tickSize.width,
@@ -391,21 +406,21 @@ public class TGPDiscreteSlider:TGPSlider_INTERFACE_BUILDER {
391406

392407
leftTrackLayer.frame = {
393408
var frame = trackLayer.bounds
394-
frame.size.width = thumbAbscisse - trackRectangle.minX
409+
frame.size.width = thumbAbscissa - trackRectangle.minX
395410
return frame
396411
}()
397412

398-
if let backgroundColor = minimumTrackTintColor ?? tintColor {
399-
leftTrackLayer.backgroundColor = backgroundColor.cgColor
400-
}
401-
402413
rightTrackLayer.frame = {
403414
var frame = trackLayer.bounds
404415
frame.size.width = trackRectangle.width - leftTrackLayer.frame.width
405416
frame.origin.x = leftTrackLayer.frame.maxX
406417
return frame
407418
}()
408-
rightTrackLayer.backgroundColor = maximumTrackTintColor.cgColor
419+
420+
if let backgroundColor = minimumTrackTintColor ?? tintColor {
421+
leadingTrackLayer.backgroundColor = backgroundColor.cgColor
422+
}
423+
trailingTrackLayer.backgroundColor = maximumTrackTintColor.cgColor
409424
}
410425

411426
func drawThumb() {
@@ -414,7 +429,7 @@ public class TGPDiscreteSlider:TGPSlider_INTERFACE_BUILDER {
414429
let thumbSizeForStyle = thumbSizeIncludingShadow()
415430
let thumbWidth = thumbSizeForStyle.width
416431
let thumbHeight = thumbSizeForStyle.height
417-
let rectangle = CGRect(x:thumbAbscisse - (thumbWidth / 2),
432+
let rectangle = CGRect(x:thumbAbscissa - (thumbWidth / 2),
418433
y: (frame.height - thumbHeight)/2,
419434
width: thumbWidth,
420435
height: thumbHeight)
@@ -515,11 +530,11 @@ public class TGPDiscreteSlider:TGPSlider_INTERFACE_BUILDER {
515530
width: trackSize.width,
516531
height: trackSize.height)
517532
let trackY = frame.height / 2
518-
ticksAbscisses = []
533+
ticksAbscissae = []
519534
for iterate in 0 ... segments {
520535
let ratio = Double(iterate) / Double(segments)
521536
let originX = trackRectangle.origin.x + (CGFloat)(trackSize.width * CGFloat(ratio))
522-
ticksAbscisses.append(CGPoint(x: originX, y: trackY))
537+
ticksAbscissae.append(CGPoint(x: originX, y: trackY))
523538
}
524539
layoutThumb()
525540

@@ -536,7 +551,10 @@ public class TGPDiscreteSlider:TGPSlider_INTERFACE_BUILDER {
536551
let nonZeroIncrement = ((0 == incrementValue) ? 1 : incrementValue)
537552
var thumbRatio = Double(value - minimumValue) / Double(segments * nonZeroIncrement)
538553
thumbRatio = max(0.0, min(thumbRatio, 1.0)) // Normalized
539-
thumbAbscisse = trackRectangle.origin.x + (CGFloat)(trackRectangle.width * CGFloat(thumbRatio))
554+
thumbRatio = (.rightToLeft == localeCharacterDirection)
555+
? 1.0 - thumbRatio
556+
: thumbRatio
557+
thumbAbscissa = trackRectangle.origin.x + (CGFloat)(trackRectangle.width * CGFloat(thumbRatio))
540558
}
541559

542560
func thumbSizeIncludingShadow() -> CGSize {
@@ -628,14 +646,14 @@ public class TGPDiscreteSlider:TGPSlider_INTERFACE_BUILDER {
628646
func touchDown(_ touches: Set<UITouch>, animationDuration duration:TimeInterval) {
629647
if let touch = touches.first {
630648
let location = touch.location(in: touch.view)
631-
moveThumbTo(abscisse: location.x, animationDuration: duration)
649+
moveThumbTo(abscissa: location.x, animationDuration: duration)
632650
}
633651
}
634652

635653
func touchUp(_ touches: Set<UITouch>) {
636654
if let touch = touches.first {
637655
let location = touch.location(in: touch.view)
638-
let tick = pickTickFromSliderPosition(abscisse: location.x)
656+
let tick = pickTickFromSliderPosition(abscissa: location.x)
639657
moveThumbToTick(tick: tick)
640658
}
641659
}
@@ -665,14 +683,14 @@ public class TGPDiscreteSlider:TGPSlider_INTERFACE_BUILDER {
665683
setNeedsDisplay()
666684
}
667685

668-
func moveThumbTo(abscisse:CGFloat, animationDuration duration:TimeInterval) {
686+
func moveThumbTo(abscissa:CGFloat, animationDuration duration:TimeInterval) {
669687
let leftMost = trackRectangle.minX
670688
let rightMost = trackRectangle.maxX
671689

672-
thumbAbscisse = max(leftMost, min(abscisse, rightMost))
690+
thumbAbscissa = max(leftMost, min(abscissa, rightMost))
673691
CATransaction.setAnimationDuration(duration)
674692

675-
let tick = pickTickFromSliderPosition(abscisse: thumbAbscisse)
693+
let tick = pickTickFromSliderPosition(abscissa: thumbAbscissa)
676694
let nonZeroIncrement = ((0 == incrementValue) ? 1 : incrementValue)
677695
let intValue = Int(minimumValue) + (Int(tick) * nonZeroIncrement)
678696
if intValue != self.intValue {
@@ -683,11 +701,14 @@ public class TGPDiscreteSlider:TGPSlider_INTERFACE_BUILDER {
683701
setNeedsDisplay()
684702
}
685703

686-
func pickTickFromSliderPosition(abscisse: CGFloat) -> UInt {
704+
func pickTickFromSliderPosition(abscissa: CGFloat) -> UInt {
687705
let leftMost = trackRectangle.minX
688706
let rightMost = trackRectangle.maxX
689-
let clampedAbscisse = max(leftMost, min(abscisse, rightMost))
690-
let ratio = Double(clampedAbscisse - leftMost) / Double(rightMost - leftMost)
707+
let clampedAbscissa = max(leftMost, min(abscissa, rightMost))
708+
var ratio = Double(clampedAbscissa - leftMost) / Double(rightMost - leftMost)
709+
ratio = (.rightToLeft == localeCharacterDirection)
710+
? 1.0 - ratio
711+
: ratio
691712
let segments = max(1, tickCount - 1)
692713
return UInt(round( Double(segments) * ratio))
693714
}

0 commit comments

Comments
 (0)