CustomSwitch.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. //
  2. // CustomSwitch.m
  3. // JasonDevelop
  4. //
  5. // Created by Jason Lee on 3/11/14.
  6. // Copyright (c) jasondevelop. All rights reserved.
  7. //
  8. @import QuartzCore;
  9. #import "CustomSwitch.h"
  10. #define kfIOS7Width 51.0f
  11. #define kfIOS6Width 79.0f
  12. #define kConstrainsFrameToProportions YES
  13. #define kHeightWidthRatio 1.6451612903 //Magic number as a result of dividing the height by the width on the default UISwitch size (51/31)
  14. //NSCoding Keys
  15. #define kCodingOnKey @"on"
  16. #define kCodingLockedKey @"off"
  17. #define kCodingOnTintColorKey @"onColor"
  18. #define kCodingOnColorKey @"onTintColor" //Not implemented
  19. #define kCodingTintColorKey @"tintColor"
  20. #define kCodingThumbTintColorKey @"thumbTintColor"
  21. #define kCodingOnImageKey @"onImage"
  22. #define kCodingOffImageKey @"offImage"
  23. #define kCodingConstrainFrameKey @"constrainFrame"
  24. //Appearance Defaults - Colors
  25. //Track Colors
  26. #define kDefaultTrackOnColor [UIColor colorWithRed:83/255.0 green: 214/255.0 blue: 105/255.0 alpha: 1]
  27. #define kDefaultTrackOffColor [UIColor colorWithWhite: 0.9f alpha:1.0f]
  28. #define kDefaultTrackContrastColor [UIColor orangeColor]
  29. //Thumb Colors
  30. #define kDefaultThumbTintColor [UIColor whiteColor]
  31. //#define kDefaultThumbTintColor [UIColor orangeColor]
  32. #define kDefaultThumbBorderColor [UIColor colorWithWhite: 0.9f alpha:1.0f]
  33. //Appearance - Layout
  34. //Size of knob with respect to the control - Must be a multiple of 2
  35. #define kThumbOffset 1
  36. #define kThumbTrackingGrowthRatio 1.2f //Amount to grow the thumb on press down
  37. #define kDefaultPanActivationThreshold 0.7 //Number between 0.0 - 1.0 describing how far user must drag before initiating the switch
  38. //Appearance - Animations
  39. #define kDefaultAnimationSlideLength 0.25f //Length of time to slide the thumb from left/right to right/left
  40. #define kDefaultAnimationScaleLength 0.15f //Length of time for the thumb to grow on press down
  41. #define kDefaultAnimationContrastResizeLength 0.25f //Length of time for the thumb to grow on press down
  42. #define kSwitchTrackContrastViewShrinkFactor 0.0001f //Must be very low but not 0 or else causes iOS 5 issues
  43. typedef enum {
  44. KLSwitchThumbJustifyLeft,
  45. KLSwitchThumbJustifyRight
  46. } KLSwitchThumbJustify;
  47. @interface KLSwitchThumb : UIView
  48. @property (nonatomic, assign) BOOL isTracking;
  49. -(void) growThumbWithJustification:(KLSwitchThumbJustify) justification;
  50. -(void) shrinkThumbWithJustification:(KLSwitchThumbJustify) justification;
  51. @end
  52. @interface KLSwitchTrack : UIView
  53. @property(nonatomic, getter=isOn) BOOL on;
  54. @property (nonatomic, strong) UIColor* contrastColor;
  55. @property (nonatomic, strong) UIColor* onTintColor;
  56. @property (nonatomic, strong) UIColor* tintColor;
  57. -(id) initWithFrame:(CGRect)frame
  58. onColor:(UIColor*) onColor
  59. offColor:(UIColor*) offColor
  60. contrastColor:(UIColor*) contrastColor;
  61. -(void) growContrastView;
  62. -(void) shrinkContrastView;
  63. -(void) setOn:(BOOL) on
  64. animated:(BOOL) animated;
  65. @end
  66. @interface CustomSwitch () <UIGestureRecognizerDelegate>
  67. @property (nonatomic, strong) KLSwitchTrack* track;
  68. @property (nonatomic, strong) KLSwitchThumb* thumb;
  69. //Gesture Recognizers
  70. @property (nonatomic, strong) UIPanGestureRecognizer* panGesture;
  71. @property (nonatomic, strong) UITapGestureRecognizer* tapGesture;
  72. -(void) configureSwitch;
  73. -(void) initializeDefaults;
  74. -(void) toggleState;
  75. -(void) setThumbOn:(BOOL) on
  76. animated:(BOOL) animated;
  77. @end
  78. @implementation CustomSwitch
  79. #pragma mark - Initializers
  80. - (void)encodeWithCoder:(NSCoder *)aCoder {
  81. [super encodeWithCoder: aCoder];
  82. [aCoder encodeBool: _on
  83. forKey: kCodingOnKey];
  84. [aCoder encodeObject: _onTintColor
  85. forKey: kCodingOnTintColorKey];
  86. [aCoder encodeObject: _tintColor
  87. forKey: kCodingTintColorKey];
  88. [aCoder encodeObject: _thumbTintColor
  89. forKey: kCodingThumbTintColorKey];
  90. [aCoder encodeObject: _onImage
  91. forKey: kCodingOnImageKey];
  92. [aCoder encodeObject: _offImage
  93. forKey: kCodingOffImageKey];
  94. [aCoder encodeBool: _shouldConstrainFrame
  95. forKey: kCodingConstrainFrameKey];
  96. }
  97. - (id)initWithCoder:(NSCoder *)aDecoder {
  98. [self initializeDefaults];
  99. if (self = [super initWithCoder: aDecoder]) {
  100. _on = [aDecoder decodeBoolForKey:kCodingOnKey];
  101. _locked = [aDecoder decodeBoolForKey:kCodingLockedKey];
  102. _onTintColor = [aDecoder decodeObjectForKey: kCodingOnTintColorKey];
  103. _tintColor = [aDecoder decodeObjectForKey: kCodingTintColorKey];
  104. _thumbTintColor = [aDecoder decodeObjectForKey: kCodingThumbTintColorKey];
  105. _onImage = [aDecoder decodeObjectForKey: kCodingOnImageKey];
  106. _offImage = [aDecoder decodeObjectForKey: kCodingOffImageKey];
  107. _onTintColor = [aDecoder decodeObjectForKey: kCodingOnTintColorKey];
  108. _shouldConstrainFrame = [aDecoder decodeBoolForKey: kCodingConstrainFrameKey];
  109. [self configureSwitch];
  110. }
  111. return self;
  112. }
  113. - (id)initWithFrame:(CGRect)frame
  114. {
  115. self = [super initWithFrame:frame];
  116. if (self) {
  117. [self configureSwitch];
  118. }
  119. return self;
  120. }
  121. - (id)initWithFrame:(CGRect)frame
  122. didChangeHandler:(changeHandler) didChangeHandler {
  123. if (self = [self initWithFrame: frame]) {
  124. _didChangeHandler = didChangeHandler;
  125. }
  126. return self;
  127. }
  128. -(void) setFrame:(CGRect)frame {
  129. if (self.shouldConstrainFrame) {
  130. [super setFrame: CGRectMake(frame.origin.x, frame.origin.y, frame.size.height*kHeightWidthRatio, frame.size.height)];
  131. }
  132. else [super setFrame: frame];
  133. }
  134. - (void)awakeFromNib {
  135. [self setThumbTintColor:[UIColor blueColor]];
  136. [self setOnTintColor:[UIColor whiteColor]];
  137. [self setThumbBorderColor:[UIColor whiteColor]];
  138. [self setBackgroundColor:[UIColor clearColor]];
  139. self.layer.cornerRadius = 16.0f;
  140. self.layer.borderColor = [UIColor lightGrayColor].CGColor;
  141. self.layer.borderWidth = 1.0f;
  142. }
  143. #pragma mark - Defaults and layout/appearance
  144. -(void) initializeDefaults {
  145. _onTintColor = kDefaultTrackOnColor;
  146. _tintColor = kDefaultTrackOffColor;
  147. _thumbTintColor = kDefaultThumbTintColor;
  148. _thumbBorderColor = kDefaultThumbBorderColor;
  149. _contrastColor = kDefaultThumbTintColor;
  150. _panActivationThreshold = kDefaultPanActivationThreshold;
  151. _shouldConstrainFrame = kConstrainsFrameToProportions;
  152. }
  153. -(void) configureSwitch {
  154. [self initializeDefaults];
  155. //Configure visual properties of self
  156. [self setBackgroundColor: [UIColor clearColor]];
  157. // tap gesture for toggling the switch
  158. self.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self
  159. action:@selector(didTap:)];
  160. [self.tapGesture setDelegate:self];
  161. [self addGestureRecognizer:self.tapGesture];
  162. // pan gesture for moving the switch knob manually
  163. self.panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self
  164. action:@selector(didDrag:)];
  165. [self.panGesture setDelegate:self];
  166. [self addGestureRecognizer:self.panGesture];
  167. /*
  168. Subview layering as follows :
  169. TOP
  170. thumb
  171. track
  172. BOTTOM
  173. */
  174. // Initialization code
  175. if (!_track) {
  176. _track = [[KLSwitchTrack alloc] initWithFrame: self.bounds
  177. onColor: self.onTintColor
  178. offColor: self.tintColor
  179. contrastColor: self.contrastColor];
  180. [_track setOn: self.isOn
  181. animated: NO];
  182. [self addSubview: self.track];
  183. }
  184. if (!_thumb) {
  185. _thumb = [[KLSwitchThumb alloc] initWithFrame:CGRectMake(kThumbOffset, kThumbOffset, self.bounds.size.height - 2 * kThumbOffset, self.bounds.size.height - 2 * kThumbOffset)];
  186. [self addSubview: _thumb];
  187. }
  188. }
  189. -(void) setOnTintColor:(UIColor *)onTintColor {
  190. _onTintColor = onTintColor;
  191. [self.track setOnTintColor: _onTintColor];
  192. }
  193. -(void) setTintColor:(UIColor *)tintColor {
  194. _tintColor = tintColor;
  195. [self.track setTintColor: _tintColor];
  196. }
  197. -(void) setContrastColor:(UIColor *)contrastColor {
  198. _contrastColor = contrastColor;
  199. [self.track setContrastColor: _contrastColor];
  200. }
  201. -(void) setThumbBorderColor:(UIColor *)thumbBorderColor {
  202. _thumbBorderColor = thumbBorderColor;
  203. [self.thumb.layer setBorderColor: [_thumbBorderColor CGColor]];
  204. }
  205. - (void)setThumbTintColor:(UIColor *)thumbTintColor {
  206. _thumbTintColor = thumbTintColor;
  207. // [self.thumb.layer setBackgroundColor:[_thumbTintColor CGColor]];
  208. [self.thumb setBackgroundColor:_thumbTintColor];
  209. }
  210. - (void)drawRect:(CGRect)rect
  211. {
  212. [super drawRect:rect];
  213. // Drawing code
  214. //[self.trackingKnob setTintColor: self.thumbTintColor];
  215. [_thumb setBackgroundColor: [UIColor whiteColor]];
  216. //Make the knob a circle and add a shadow
  217. CGFloat roundedCornerRadius = _thumb.frame.size.height/2.0f;
  218. [_thumb.layer setBorderWidth: 0.5];
  219. [_thumb.layer setBorderColor: [self.thumbBorderColor CGColor]];
  220. [_thumb.layer setBackgroundColor:[self.thumbTintColor CGColor]];
  221. [_thumb.layer setCornerRadius: roundedCornerRadius];
  222. [_thumb.layer setShadowColor: [[UIColor grayColor] CGColor]];
  223. [_thumb.layer setShadowOffset: CGSizeMake(0, 3)];
  224. [_thumb.layer setShadowOpacity: 0.40f];
  225. [_thumb.layer setShadowRadius: 0.8];
  226. }
  227. #pragma mark - UIGestureRecognizer implementations
  228. -(void) didTap:(UITapGestureRecognizer*) gesture {
  229. if (gesture.state == UIGestureRecognizerStateEnded) {
  230. [self toggleState];
  231. }
  232. }
  233. -(void) didDrag:(UIPanGestureRecognizer*) gesture {
  234. if (gesture.state == UIGestureRecognizerStateBegan) {
  235. //Grow the thumb horizontally towards center by defined ratio
  236. [self setThumbIsTracking: YES
  237. animated: YES];
  238. }
  239. else if (gesture.state == UIGestureRecognizerStateChanged) {
  240. //If touch crosses the threshold then toggle the state
  241. CGPoint locationInThumb = [gesture locationInView: self.thumb];
  242. //Toggle the switch if the user pans left or right past the switch thumb bounds
  243. if ((self.isOn && locationInThumb.x <= 0)
  244. || (!self.isOn && locationInThumb.x >= self.thumb.bounds.size.width)) {
  245. [self toggleState];
  246. }
  247. CGPoint locationOfTouch = [gesture locationInView:self];
  248. if (CGRectContainsPoint(self.bounds, locationOfTouch))
  249. [self sendActionsForControlEvents:UIControlEventTouchDragInside];
  250. else
  251. [self sendActionsForControlEvents:UIControlEventTouchDragOutside];
  252. }
  253. else if (gesture.state == UIGestureRecognizerStateEnded) {
  254. [self setThumbIsTracking: NO
  255. animated: YES];
  256. }
  257. }
  258. #pragma mark - Event Handlers
  259. -(void) toggleState {
  260. //Alternate between on/off
  261. [self setOn: self.isOn ? NO : YES
  262. animated: YES];
  263. }
  264. - (void)setOn:(BOOL)on
  265. animated:(BOOL)animated {
  266. //Cancel notification to parent if attempting to set to current state
  267. if (_on == on) {
  268. return;
  269. }
  270. //Move the thumb to the new position
  271. [self setThumbOn: on
  272. animated: animated];
  273. //Animate the contrast view of the track
  274. [self.track setOn: on
  275. animated: animated];
  276. _on = on;
  277. //Trigger the completion block if exists
  278. if (self.didChangeHandler) {
  279. self.didChangeHandler(_on);
  280. }
  281. [self sendActionsForControlEvents:UIControlEventValueChanged];
  282. }
  283. - (void) setOn:(BOOL)on {
  284. [self setOn: on animated: NO];
  285. }
  286. - (void) setLocked:(BOOL)locked {
  287. //Cancel notification to parent if attempting to set to current state
  288. if (_locked == locked) {
  289. return;
  290. }
  291. _locked = locked;
  292. UIImageView *lockImageView = (UIImageView *)[_track viewWithTag:LOCK_IMAGE_SUBVIEW];
  293. if (!locked && (lockImageView != nil)) {
  294. [lockImageView removeFromSuperview];
  295. lockImageView = nil;
  296. } else if (locked && (lockImageView == nil)) {
  297. UIImage *lockImage = [UIImage imageNamed:@"lock-icon.png"];
  298. lockImageView = [[UIImageView alloc] initWithImage:lockImage];
  299. lockImageView.frame = CGRectMake(7, 8, lockImage.size.width, lockImage.size.height);
  300. lockImageView.tag = LOCK_IMAGE_SUBVIEW;
  301. [_track addSubview:lockImageView];
  302. [_track bringSubviewToFront:lockImageView];
  303. }
  304. }
  305. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  306. {
  307. [super touchesBegan:touches withEvent:event];
  308. [self sendActionsForControlEvents:UIControlEventTouchDown];
  309. }
  310. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
  311. {
  312. [super touchesEnded:touches withEvent:event];
  313. [self sendActionsForControlEvents:UIControlEventTouchUpInside];
  314. }
  315. - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
  316. {
  317. [super touchesCancelled:touches withEvent:event];
  318. [self sendActionsForControlEvents:UIControlEventTouchUpOutside];
  319. }
  320. -(void) setThumbIsTracking:(BOOL)isTracking {
  321. if (isTracking) {
  322. //Grow
  323. [self.thumb growThumbWithJustification: self.isOn ? KLSwitchThumbJustifyRight : KLSwitchThumbJustifyLeft];
  324. }
  325. else {
  326. //Shrink
  327. [self.thumb shrinkThumbWithJustification: self.isOn ? KLSwitchThumbJustifyRight : KLSwitchThumbJustifyLeft];
  328. }
  329. [self.thumb setIsTracking: isTracking];
  330. }
  331. -(void) setThumbIsTracking:(BOOL)isTracking
  332. animated:(BOOL) animated {
  333. __weak id weakSelf = self;
  334. [UIView animateWithDuration: kDefaultAnimationScaleLength
  335. delay: fabs(kDefaultAnimationSlideLength - kDefaultAnimationScaleLength)
  336. options: UIViewAnimationOptionCurveEaseOut
  337. animations: ^{
  338. [weakSelf setThumbIsTracking: isTracking];
  339. }
  340. completion:nil];
  341. }
  342. -(void) setThumbOn:(BOOL) on
  343. animated:(BOOL) animated {
  344. if (animated) {
  345. [UIView animateWithDuration:0.3 animations:^{
  346. [self setThumbOn:on animated:NO];
  347. }];
  348. }
  349. CGRect thumbFrame = self.thumb.frame;
  350. if (on) {
  351. thumbFrame.origin.x = self.bounds.size.width - (thumbFrame.size.width + kThumbOffset);
  352. }
  353. else {
  354. thumbFrame.origin.x = kThumbOffset;
  355. }
  356. [self.thumb setFrame: thumbFrame];
  357. }
  358. @end
  359. @implementation KLSwitchThumb
  360. -(void) growThumbWithJustification:(KLSwitchThumbJustify) justification {
  361. if (self.isTracking)
  362. return;
  363. CGRect thumbFrame = self.frame;
  364. CGFloat deltaWidth = self.frame.size.width * (kThumbTrackingGrowthRatio - 1);
  365. thumbFrame.size.width += deltaWidth;
  366. if (justification == KLSwitchThumbJustifyRight) {
  367. thumbFrame.origin.x -= deltaWidth;
  368. }
  369. [self setFrame: thumbFrame];
  370. }
  371. -(void) shrinkThumbWithJustification:(KLSwitchThumbJustify) justification {
  372. if (!self.isTracking)
  373. return;
  374. CGRect thumbFrame = self.frame;
  375. CGFloat deltaWidth = self.frame.size.width * (1 - 1 / (kThumbTrackingGrowthRatio));
  376. thumbFrame.size.width -= deltaWidth;
  377. if (justification == KLSwitchThumbJustifyRight) {
  378. thumbFrame.origin.x += deltaWidth;
  379. }
  380. [self setFrame: thumbFrame];
  381. }
  382. @end
  383. @interface KLSwitchTrack ()
  384. @property (nonatomic, strong) UIView* contrastView;
  385. @property (nonatomic, strong) UIView* onView;
  386. @end
  387. @implementation KLSwitchTrack
  388. -(id) initWithFrame:(CGRect)frame
  389. onColor:(UIColor*) onColor
  390. offColor:(UIColor*) offColor
  391. contrastColor:(UIColor*) contrastColor {
  392. if (self = [super initWithFrame: frame]) {
  393. _onTintColor = onColor;
  394. _tintColor = offColor;
  395. CGFloat cornerRadius = frame.size.height/2.0f;
  396. [self.layer setCornerRadius: cornerRadius];
  397. [self setBackgroundColor: _tintColor];
  398. CGRect contrastRect = frame;
  399. contrastRect.size.width = frame.size.width - 2*kThumbOffset;
  400. contrastRect.size.height = frame.size.height - 2*kThumbOffset;
  401. CGFloat contrastRadius = contrastRect.size.height/2.0f;
  402. _contrastView = [[UIView alloc] initWithFrame:contrastRect];
  403. [_contrastView setBackgroundColor: contrastColor];
  404. [_contrastView setCenter: self.center];
  405. [_contrastView.layer setCornerRadius: contrastRadius];
  406. [self addSubview: _contrastView];
  407. _onView = [[UIView alloc] initWithFrame:frame];
  408. [_onView setBackgroundColor: _onTintColor];
  409. [_onView setCenter: self.center];
  410. [_onView.layer setCornerRadius: cornerRadius];
  411. [self addSubview: _onView];
  412. }
  413. return self;
  414. }
  415. -(void) setOn:(BOOL)on {
  416. if (on) {
  417. [self.onView setAlpha: 1.0];
  418. [self shrinkContrastView];
  419. }
  420. else {
  421. [self.onView setAlpha: 0.0];
  422. [self growContrastView];
  423. }
  424. }
  425. -(void) setOn:(BOOL)on
  426. animated:(BOOL)animated {
  427. if (animated) {
  428. __weak id weakSelf = self;
  429. //First animate the color switch
  430. [UIView animateWithDuration: kDefaultAnimationContrastResizeLength
  431. delay: 0.0
  432. options: UIViewAnimationOptionCurveEaseOut
  433. animations:^{
  434. [weakSelf setOn: on
  435. animated: NO];
  436. }
  437. completion:nil];
  438. }
  439. else {
  440. [self setOn: on];
  441. }
  442. }
  443. -(void) setOnTintColor:(UIColor *)onTintColor {
  444. _onTintColor = onTintColor;
  445. [self.onView setBackgroundColor: _onTintColor];
  446. }
  447. -(void) setTintColor:(UIColor *)tintColor {
  448. _tintColor = tintColor;
  449. [self setBackgroundColor: _tintColor];
  450. }
  451. -(void) setContrastColor:(UIColor *)contrastColor {
  452. _contrastColor = contrastColor;
  453. [self.contrastView setBackgroundColor: _contrastColor];
  454. }
  455. -(void) growContrastView {
  456. //Start out with contrast view small and centered
  457. [self.contrastView setTransform: CGAffineTransformMakeScale(kSwitchTrackContrastViewShrinkFactor, kSwitchTrackContrastViewShrinkFactor)];
  458. [self.contrastView setTransform: CGAffineTransformMakeScale(1, 1)];
  459. }
  460. -(void) shrinkContrastView {
  461. //Start out with contrast view the size of the track
  462. [self.contrastView setTransform: CGAffineTransformMakeScale(1, 1)];
  463. [self.contrastView setTransform: CGAffineTransformMakeScale(kSwitchTrackContrastViewShrinkFactor, kSwitchTrackContrastViewShrinkFactor)];
  464. }
  465. @end