You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

556 lines
18 KiB

//
// KLSwitch.m
// KLSwitch
//
// Created by Kieran Lafferty on 2013-06-15.
// Copyright (c) 2013 Kieran Lafferty. All rights reserved.
//
// https://github.com/KieranLafferty/KLSwitch
#import "KLSwitch.h"
#define kConstrainsFrameToProportions YES
#define kHeightWidthRatio 1.6451612903 //Magic number as a result of dividing the height by the width on the default UISwitch size (51/31)
//NSCoding Keys
#define kCodingOnKey @"on"
#define kCodingLockedKey @"off"
#define kCodingOnTintColorKey @"onColor"
#define kCodingOnColorKey @"onTintColor" //Not implemented
#define kCodingTintColorKey @"tintColor"
#define kCodingThumbTintColorKey @"thumbTintColor"
#define kCodingOnImageKey @"onImage"
#define kCodingOffImageKey @"offImage"
#define kCodingConstrainFrameKey @"constrainFrame"
//Appearance Defaults - Colors
//Track Colors
#define kDefaultTrackOnColor [UIColor colorWithRed:83/255.0 green: 214/255.0 blue: 105/255.0 alpha: 1]
#define kDefaultTrackOffColor [UIColor colorWithWhite: 0.9f alpha:1.0f]
#define kDefaultTrackContrastColor [UIColor whiteColor]
//Thumb Colors
#define kDefaultThumbTintColor [UIColor whiteColor]
#define kDefaultThumbBorderColor [UIColor colorWithWhite: 0.9f alpha:1.0f]
//Appearance - Layout
//Size of knob with respect to the control - Must be a multiple of 2
#define kThumbOffset 1
#define kThumbTrackingGrowthRatio 1.2f //Amount to grow the thumb on press down
#define kDefaultPanActivationThreshold 0.7 //Number between 0.0 - 1.0 describing how far user must drag before initiating the switch
//Appearance - Animations
#define kDefaultAnimationSlideLength 0.25f //Length of time to slide the thumb from left/right to right/left
#define kDefaultAnimationScaleLength 0.15f //Length of time for the thumb to grow on press down
#define kDefaultAnimationContrastResizeLength 0.25f //Length of time for the thumb to grow on press down
#define kSwitchTrackContrastViewShrinkFactor 0.0001f //Must be very low but not 0 or else causes iOS 5 issues
typedef enum {
KLSwitchThumbJustifyLeft,
KLSwitchThumbJustifyRight
} KLSwitchThumbJustify;
@interface KLSwitchThumb : UIView
@property (nonatomic, assign) BOOL isTracking;
-(void) growThumbWithJustification:(KLSwitchThumbJustify) justification;
-(void) shrinkThumbWithJustification:(KLSwitchThumbJustify) justification;
@end
@interface KLSwitchTrack : UIView
@property(nonatomic, getter=isOn) BOOL on;
@property (nonatomic, strong) UIColor* contrastColor;
@property (nonatomic, strong) UIColor* onTintColor;
@property (nonatomic, strong) UIColor* tintColor;
-(id) initWithFrame:(CGRect)frame
onColor:(UIColor*) onColor
offColor:(UIColor*) offColor
contrastColor:(UIColor*) contrastColor;
-(void) growContrastView;
-(void) shrinkContrastView;
-(void) setOn:(BOOL) on
animated:(BOOL) animated;
@end
@interface KLSwitch () <UIGestureRecognizerDelegate>
@property (nonatomic, strong) KLSwitchTrack* track;
@property (nonatomic, strong) KLSwitchThumb* thumb;
//Gesture Recognizers
@property (nonatomic, strong) UIPanGestureRecognizer* panGesture;
@property (nonatomic, strong) UITapGestureRecognizer* tapGesture;
-(void) configureSwitch;
-(void) initializeDefaults;
-(void) toggleState;
-(void) setThumbOn:(BOOL) on
animated:(BOOL) animated;
@end
@implementation KLSwitch
#pragma mark - Initializers
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder: aCoder];
[aCoder encodeBool: _on
forKey: kCodingOnKey];
[aCoder encodeObject: _onTintColor
forKey: kCodingOnTintColorKey];
[aCoder encodeObject: _tintColor
forKey: kCodingTintColorKey];
[aCoder encodeObject: _thumbTintColor
forKey: kCodingThumbTintColorKey];
[aCoder encodeObject: _onImage
forKey: kCodingOnImageKey];
[aCoder encodeObject: _offImage
forKey: kCodingOffImageKey];
[aCoder encodeBool: _shouldConstrainFrame
forKey: kCodingConstrainFrameKey];
}
- (id)initWithCoder:(NSCoder *)aDecoder {
[self initializeDefaults];
if (self = [super initWithCoder: aDecoder]) {
_on = [aDecoder decodeBoolForKey:kCodingOnKey];
_locked = [aDecoder decodeBoolForKey:kCodingLockedKey];
_onTintColor = [aDecoder decodeObjectForKey: kCodingOnTintColorKey];
_tintColor = [aDecoder decodeObjectForKey: kCodingTintColorKey];
_thumbTintColor = [aDecoder decodeObjectForKey: kCodingThumbTintColorKey];
_onImage = [aDecoder decodeObjectForKey: kCodingOnImageKey];
_offImage = [aDecoder decodeObjectForKey: kCodingOffImageKey];
_onTintColor = [aDecoder decodeObjectForKey: kCodingOnTintColorKey];
_shouldConstrainFrame = [aDecoder decodeBoolForKey: kCodingConstrainFrameKey];
[self configureSwitch];
}
return self;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self configureSwitch];
}
return self;
}
- (id)initWithFrame:(CGRect)frame
didChangeHandler:(changeHandler) didChangeHandler {
if (self = [self initWithFrame: frame]) {
_didChangeHandler = didChangeHandler;
}
return self;
}
-(void) setFrame:(CGRect)frame {
if (self.shouldConstrainFrame) {
[super setFrame: CGRectMake(frame.origin.x, frame.origin.y, frame.size.height*kHeightWidthRatio, frame.size.height)];
}
else [super setFrame: frame];
}
#pragma mark - Defaults and layout/appearance
-(void) initializeDefaults {
_onTintColor = kDefaultTrackOnColor;
_tintColor = kDefaultTrackOffColor;
_thumbTintColor = kDefaultThumbTintColor;
_thumbBorderColor = kDefaultThumbBorderColor;
_contrastColor = kDefaultThumbTintColor;
_panActivationThreshold = kDefaultPanActivationThreshold;
_shouldConstrainFrame = kConstrainsFrameToProportions;
}
-(void) configureSwitch {
[self initializeDefaults];
//Configure visual properties of self
[self setBackgroundColor: [UIColor clearColor]];
// tap gesture for toggling the switch
self.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(didTap:)];
[self.tapGesture setDelegate:self];
[self addGestureRecognizer:self.tapGesture];
// pan gesture for moving the switch knob manually
self.panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self
action:@selector(didDrag:)];
[self.panGesture setDelegate:self];
[self addGestureRecognizer:self.panGesture];
/*
Subview layering as follows :
TOP
thumb
track
BOTTOM
*/
// Initialization code
if (!_track) {
_track = [[KLSwitchTrack alloc] initWithFrame: self.bounds
onColor: self.onTintColor
offColor: self.tintColor
contrastColor: self.contrastColor];
[_track setOn: self.isOn
animated: NO];
[self addSubview: self.track];
}
if (!_thumb) {
_thumb = [[KLSwitchThumb alloc] initWithFrame:CGRectMake(kThumbOffset, kThumbOffset, self.bounds.size.height - 2 * kThumbOffset, self.bounds.size.height - 2 * kThumbOffset)];
[self addSubview: _thumb];
}
}
-(void) setOnTintColor:(UIColor *)onTintColor {
_onTintColor = onTintColor;
[self.track setOnTintColor: _onTintColor];
}
-(void) setTintColor:(UIColor *)tintColor {
_tintColor = tintColor;
[self.track setTintColor: _tintColor];
}
-(void) setContrastColor:(UIColor *)contrastColor {
_contrastColor = contrastColor;
[self.track setContrastColor: _contrastColor];
}
-(void) setThumbBorderColor:(UIColor *)thumbBorderColor {
_thumbBorderColor = thumbBorderColor;
[self.thumb.layer setBorderColor: [_thumbBorderColor CGColor]];
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
// Drawing code
//[self.trackingKnob setTintColor: self.thumbTintColor];
[_thumb setBackgroundColor: [UIColor whiteColor]];
//Make the knob a circle and add a shadow
CGFloat roundedCornerRadius = _thumb.frame.size.height/2.0f;
[_thumb.layer setBorderWidth: 0.5];
[_thumb.layer setBorderColor: [self.thumbBorderColor CGColor]];
[_thumb.layer setCornerRadius: roundedCornerRadius];
[_thumb.layer setShadowColor: [[UIColor grayColor] CGColor]];
[_thumb.layer setShadowOffset: CGSizeMake(0, 3)];
[_thumb.layer setShadowOpacity: 0.40f];
[_thumb.layer setShadowRadius: 0.8];
}
#pragma mark - UIGestureRecognizer implementations
-(void) didTap:(UITapGestureRecognizer*) gesture {
if (gesture.state == UIGestureRecognizerStateEnded) {
[self toggleState];
}
}
-(void) didDrag:(UIPanGestureRecognizer*) gesture {
if (gesture.state == UIGestureRecognizerStateBegan) {
//Grow the thumb horizontally towards center by defined ratio
[self setThumbIsTracking: YES
animated: YES];
}
else if (gesture.state == UIGestureRecognizerStateChanged) {
//If touch crosses the threshold then toggle the state
CGPoint locationInThumb = [gesture locationInView: self.thumb];
//Toggle the switch if the user pans left or right past the switch thumb bounds
if ((self.isOn && locationInThumb.x <= 0)
|| (!self.isOn && locationInThumb.x >= self.thumb.bounds.size.width)) {
[self toggleState];
}
CGPoint locationOfTouch = [gesture locationInView:self];
if (CGRectContainsPoint(self.bounds, locationOfTouch))
[self sendActionsForControlEvents:UIControlEventTouchDragInside];
else
[self sendActionsForControlEvents:UIControlEventTouchDragOutside];
}
else if (gesture.state == UIGestureRecognizerStateEnded) {
[self setThumbIsTracking: NO
animated: YES];
}
}
#pragma mark - Event Handlers
-(void) toggleState {
//Alternate between on/off
[self setOn: self.isOn ? NO : YES
animated: YES];
}
- (void)setDefaultOnState:(BOOL)on
{
if (_on == on) {
return;
}
//Move the thumb to the new position
[self setThumbOn: on
animated: NO];
//Animate the contrast view of the track
[self.track setOn: on
animated: NO];
_on = on;
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
- (void)setOn:(BOOL)on
animated:(BOOL)animated {
//Cancel notification to parent if attempting to set to current state
if (_on == on) {
return;
}
//Move the thumb to the new position
[self setThumbOn: on
animated: animated];
//Animate the contrast view of the track
[self.track setOn: on
animated: animated];
_on = on;
//Trigger the completion block if exists
if (self.didChangeHandler) {
self.didChangeHandler(_on);
}
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
- (void) setOn:(BOOL)on {
[self setOn: on animated: NO];
}
- (void) setLocked:(BOOL)locked {
//Cancel notification to parent if attempting to set to current state
if (_locked == locked) {
return;
}
_locked = locked;
UIImageView *lockImageView = (UIImageView *)[_track viewWithTag:LOCK_IMAGE_SUBVIEW];
if (!locked && (lockImageView != nil)) {
[lockImageView removeFromSuperview];
lockImageView = nil;
} else if (locked && (lockImageView == nil)) {
UIImage *lockImage = [UIImage imageNamed:@"lock-icon.png"];
lockImageView = [[UIImageView alloc] initWithImage:lockImage];
lockImageView.frame = CGRectMake(7, 8, lockImage.size.width, lockImage.size.height);
lockImageView.tag = LOCK_IMAGE_SUBVIEW;
[_track addSubview:lockImageView];
[_track bringSubviewToFront:lockImageView];
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
[self sendActionsForControlEvents:UIControlEventTouchDown];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
[self sendActionsForControlEvents:UIControlEventTouchUpInside];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesCancelled:touches withEvent:event];
[self sendActionsForControlEvents:UIControlEventTouchUpOutside];
}
-(void) setThumbIsTracking:(BOOL)isTracking {
if (isTracking) {
//Grow
[self.thumb growThumbWithJustification: self.isOn ? KLSwitchThumbJustifyRight : KLSwitchThumbJustifyLeft];
}
else {
//Shrink
[self.thumb shrinkThumbWithJustification: self.isOn ? KLSwitchThumbJustifyRight : KLSwitchThumbJustifyLeft];
}
[self.thumb setIsTracking: isTracking];
}
-(void) setThumbIsTracking:(BOOL)isTracking
animated:(BOOL) animated {
__weak id weakSelf = self;
[UIView animateWithDuration: kDefaultAnimationScaleLength
delay: fabs(kDefaultAnimationSlideLength - kDefaultAnimationScaleLength)
options: UIViewAnimationOptionCurveEaseOut
animations: ^{
[weakSelf setThumbIsTracking: isTracking];
}
completion:nil];
}
-(void) setThumbOn:(BOOL) on
animated:(BOOL) animated {
if (animated) {
[UIView animateWithDuration:0.3 animations:^{
[self setThumbOn:on animated:NO];
}];
}
CGRect thumbFrame = self.thumb.frame;
if (on) {
thumbFrame.origin.x = self.bounds.size.width - (thumbFrame.size.width + kThumbOffset);
}
else {
thumbFrame.origin.x = kThumbOffset;
}
[self.thumb setFrame: thumbFrame];
}
@end
@implementation KLSwitchThumb
-(void) growThumbWithJustification:(KLSwitchThumbJustify) justification {
if (self.isTracking)
return;
CGRect thumbFrame = self.frame;
CGFloat deltaWidth = self.frame.size.width * (kThumbTrackingGrowthRatio - 1);
thumbFrame.size.width += deltaWidth;
if (justification == KLSwitchThumbJustifyRight) {
thumbFrame.origin.x -= deltaWidth;
}
[self setFrame: thumbFrame];
}
-(void) shrinkThumbWithJustification:(KLSwitchThumbJustify) justification {
if (!self.isTracking)
return;
CGRect thumbFrame = self.frame;
CGFloat deltaWidth = self.frame.size.width * (1 - 1 / (kThumbTrackingGrowthRatio));
thumbFrame.size.width -= deltaWidth;
if (justification == KLSwitchThumbJustifyRight) {
thumbFrame.origin.x += deltaWidth;
}
[self setFrame: thumbFrame];
}
@end
@interface KLSwitchTrack ()
@property (nonatomic, strong) UIView* contrastView;
@property (nonatomic, strong) UIView* onView;
@end
@implementation KLSwitchTrack
-(id) initWithFrame:(CGRect)frame
onColor:(UIColor*) onColor
offColor:(UIColor*) offColor
contrastColor:(UIColor*) contrastColor {
if (self = [super initWithFrame: frame]) {
_onTintColor = onColor;
_tintColor = offColor;
CGFloat cornerRadius = frame.size.height/2.0f;
[self.layer setCornerRadius: cornerRadius];
[self setBackgroundColor: _tintColor];
CGRect contrastRect = frame;
contrastRect.size.width = frame.size.width - 2*kThumbOffset;
contrastRect.size.height = frame.size.height - 2*kThumbOffset;
CGFloat contrastRadius = contrastRect.size.height/2.0f;
_contrastView = [[UIView alloc] initWithFrame:contrastRect];
[_contrastView setBackgroundColor: contrastColor];
[_contrastView setCenter: self.center];
[_contrastView.layer setCornerRadius: contrastRadius];
[self addSubview: _contrastView];
_onView = [[UIView alloc] initWithFrame:frame];
[_onView setBackgroundColor: _onTintColor];
[_onView setCenter: self.center];
[_onView.layer setCornerRadius: cornerRadius];
[self addSubview: _onView];
}
return self;
}
-(void) setOn:(BOOL)on {
if (on) {
[self.onView setAlpha: 1.0];
[self shrinkContrastView];
}
else {
[self.onView setAlpha: 0.0];
[self growContrastView];
}
}
-(void) setOn:(BOOL)on
animated:(BOOL)animated {
if (animated) {
__weak id weakSelf = self;
//First animate the color switch
[UIView animateWithDuration: kDefaultAnimationContrastResizeLength
delay: 0.0
options: UIViewAnimationOptionCurveEaseOut
animations:^{
[weakSelf setOn: on
animated: NO];
}
completion:nil];
}
else {
[self setOn: on];
}
}
-(void) setOnTintColor:(UIColor *)onTintColor {
_onTintColor = onTintColor;
[self.onView setBackgroundColor: _onTintColor];
}
-(void) setTintColor:(UIColor *)tintColor {
_tintColor = tintColor;
[self setBackgroundColor: _tintColor];
}
-(void) setContrastColor:(UIColor *)contrastColor {
_contrastColor = contrastColor;
[self.contrastView setBackgroundColor: _contrastColor];
}
-(void) growContrastView {
//Start out with contrast view small and centered
[self.contrastView setTransform: CGAffineTransformMakeScale(kSwitchTrackContrastViewShrinkFactor, kSwitchTrackContrastViewShrinkFactor)];
[self.contrastView setTransform: CGAffineTransformMakeScale(1, 1)];
}
-(void) shrinkContrastView {
//Start out with contrast view the size of the track
[self.contrastView setTransform: CGAffineTransformMakeScale(1, 1)];
[self.contrastView setTransform: CGAffineTransformMakeScale(kSwitchTrackContrastViewShrinkFactor, kSwitchTrackContrastViewShrinkFactor)];
}
@end