├── .gitignore ├── LICENSE ├── MZSelectableLabel.podspec ├── MZSelectableLabel ├── MZSelectableLabel.h ├── MZSelectableLabel.m ├── UIColor+Equalable.h └── UIColor+Equalable.m ├── MZSelectableLabelDemo ├── MZSelectableLabelDemo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcuserdata │ │ │ └── michal.zaborowski.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── michal.zaborowski.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ ├── MZSelectableLabelDemo.xcscheme │ │ └── xcschememanagement.plist ├── MZSelectableLabelDemo │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── Main.storyboard │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── LaunchImage.launchimage │ │ │ └── Contents.json │ ├── Info.plist │ ├── ViewController.h │ ├── ViewController.m │ └── main.m └── MZSelectableLabelDemoTests │ ├── Info.plist │ └── MZSelectableLabelDemoTests.m ├── README.md └── Screens └── screen1.png /.gitignore: -------------------------------------------------------------------------------- 1 | # CocoaPods 2 | # 3 | # We recommend against adding the Pods directory to your .gitignore. However 4 | # you should judge for yourself, the pros and cons are mentioned at: 5 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control? 6 | # 7 | # Pods/ 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Michał Zaborowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MZSelectableLabel.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'MZSelectableLabel' 3 | s.version = '0.1.2' 4 | s.license = 'MIT' 5 | s.summary = 'A simple to use drop in replacement for UILabel for iOS 7 that provides automatic detection of colors.' 6 | s.homepage = 'https://github.com/m1entus/MZSelectableLabel' 7 | s.authors = 'Michał Zaborowski' 8 | s.source = { :git => 'https://github.com/m1entus/MZSelectableLabel.git', :tag => s.version.to_s } 9 | 10 | s.requires_arc = true 11 | s.source_files = 'MZSelectableLabel/*.{h,m}' 12 | s.platform = :ios, '7.0' 13 | s.frameworks = 'UIKit' 14 | end 15 | -------------------------------------------------------------------------------- /MZSelectableLabel/MZSelectableLabel.h: -------------------------------------------------------------------------------- 1 | /********************************************************************************** 2 | * 3 | * The MIT License (MIT) 4 | * 5 | * Copyright (c) 2014 Michał Zaborowski 6 | * 7 | * This project is an rewritten version of the KILabel 8 | * 9 | * https://github.com/Krelborn/KILabel 10 | *********************************************************************************** 11 | * 12 | * The MIT License (MIT) 13 | * 14 | * Copyright (c) 2013 Matthew Styles 15 | * 16 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 17 | * this software and associated documentation files (the "Software"), to deal in 18 | * the Software without restriction, including without limitation the rights to 19 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 20 | * the Software, and to permit persons to whom the Software is furnished to do so, 21 | * subject to the following conditions: 22 | * 23 | * The above copyright notice and this permission notice shall be included in all 24 | * copies or substantial portions of the Software. 25 | * 26 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 28 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 29 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 30 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 31 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | * 33 | ***********************************************************************************/ 34 | 35 | #import 36 | 37 | #ifndef IBInspectable 38 | #define IBInspectable 39 | #endif 40 | 41 | typedef void(^MZSelectableLabelTapHandler)(NSRange range, NSString *string); 42 | 43 | @interface MZSelectableLabelRange : NSObject 44 | @property (nonatomic, assign) NSRange range; 45 | @property (nonatomic, strong) UIColor *color; 46 | 47 | + (instancetype)selectableRangeWithRange:(NSRange)range color:(UIColor *)color; 48 | @end 49 | 50 | @interface MZSelectableLabel : UILabel 51 | @property (nonatomic, copy) NSMutableArray *selectableRanges; 52 | 53 | //detected ranges in the text 54 | @property (nonatomic, copy) NSArray *detectedSelectableRanges; 55 | 56 | @property (nonatomic, copy) MZSelectableLabelTapHandler selectionHandler; 57 | 58 | @property (nonatomic, assign, getter = isAutomaticForegroundColorDetectionEnabled) IBInspectable BOOL automaticForegroundColorDetectionEnabled; 59 | @property (nonatomic, strong) IBInspectable UIColor *automaticDetectionBackgroundHighlightColor; 60 | @property (nonatomic, strong) IBInspectable UIColor *skipColorForAutomaticDetection; 61 | 62 | - (void)setSelectableRange:(NSRange)range; 63 | - (void)setSelectableRange:(NSRange)range hightlightedBackgroundColor:(UIColor *)color; 64 | 65 | - (MZSelectableLabelRange *)rangeValueAtLocation:(CGPoint)location; 66 | 67 | @end 68 | -------------------------------------------------------------------------------- /MZSelectableLabel/MZSelectableLabel.m: -------------------------------------------------------------------------------- 1 | /********************************************************************************** 2 | * 3 | * The MIT License (MIT) 4 | * 5 | * Copyright (c) 2014 Michał Zaborowski 6 | * 7 | * This project is an rewritten version of the KILabel 8 | * 9 | * https://github.com/Krelborn/KILabel 10 | *********************************************************************************** 11 | * 12 | * The MIT License (MIT) 13 | * 14 | * Copyright (c) 2013 Matthew Styles 15 | * 16 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 17 | * this software and associated documentation files (the "Software"), to deal in 18 | * the Software without restriction, including without limitation the rights to 19 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 20 | * the Software, and to permit persons to whom the Software is furnished to do so, 21 | * subject to the following conditions: 22 | * 23 | * The above copyright notice and this permission notice shall be included in all 24 | * copies or substantial portions of the Software. 25 | * 26 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 28 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 29 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 30 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 31 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | * 33 | ***********************************************************************************/ 34 | 35 | #import "MZSelectableLabel.h" 36 | #import "UIColor+Equalable.h" 37 | 38 | @interface MZSelectableLabel () 39 | 40 | // Used to control layout of glyphs and rendering 41 | @property (nonatomic, retain) NSLayoutManager *layoutManager; 42 | 43 | // Specifies the space in which to render text 44 | @property (nonatomic, retain) NSTextContainer *textContainer; 45 | 46 | // Backing storage for text that is rendered by the layout manager 47 | @property (nonatomic, retain) NSTextStorage *textStorage; 48 | 49 | // State used to trag if the user has dragged during a touch 50 | @property (nonatomic, assign) BOOL isTouchMoved; 51 | 52 | // During a touch, range of text that is displayed as selected 53 | @property (nonatomic, assign) NSRange selectedRange; 54 | 55 | @end 56 | 57 | @implementation MZSelectableLabel 58 | 59 | #pragma mark - Construction 60 | 61 | - (id)initWithFrame:(CGRect)frame 62 | { 63 | self = [super initWithFrame:frame]; 64 | if (self) 65 | { 66 | [self setupTextSystem]; 67 | } 68 | return self; 69 | } 70 | 71 | - (id)initWithCoder:(NSCoder *)aDecoder 72 | { 73 | self = [super initWithCoder:aDecoder]; 74 | if (self) 75 | { 76 | [self setupTextSystem]; 77 | } 78 | return self; 79 | } 80 | 81 | // Common initialisation. Must be done once during construction. 82 | - (void)setupTextSystem 83 | { 84 | _selectableRanges = [NSMutableArray array]; 85 | // Create a text container and set it up to match our label properties 86 | self.textContainer = [[NSTextContainer alloc] init]; 87 | self.textContainer.lineFragmentPadding = 0; 88 | self.textContainer.maximumNumberOfLines = self.numberOfLines; 89 | self.textContainer.lineBreakMode = self.lineBreakMode; 90 | self.textContainer.size = self.frame.size; 91 | 92 | // Create a layout manager for rendering 93 | self.layoutManager = [[NSLayoutManager alloc] init]; 94 | self.layoutManager.delegate = self; 95 | [self.layoutManager addTextContainer:self.textContainer]; 96 | 97 | // Attach the layou manager to the container and storage 98 | [self.textContainer setLayoutManager:self.layoutManager]; 99 | 100 | // Make sure user interaction is enabled so we can accept touches 101 | self.userInteractionEnabled = YES; 102 | 103 | // Establish the text store with our current text 104 | [self updateTextStoreWithText]; 105 | 106 | } 107 | 108 | #pragma mark - Text Storage Management 109 | 110 | - (void)setSkipColorForAutomaticDetection:(UIColor *)skipColorForAutomaticDetection 111 | { 112 | _skipColorForAutomaticDetection = skipColorForAutomaticDetection; 113 | self.automaticForegroundColorDetectionEnabled = _automaticForegroundColorDetectionEnabled; 114 | } 115 | 116 | - (void)setAutomaticDetectionBackgroundHighlightColor:(UIColor *)automaticDetectionBackgroundHighlightColor 117 | { 118 | _automaticDetectionBackgroundHighlightColor = automaticDetectionBackgroundHighlightColor; 119 | self.automaticForegroundColorDetectionEnabled = _automaticForegroundColorDetectionEnabled; 120 | } 121 | 122 | - (void)setAutomaticForegroundColorDetectionEnabled:(BOOL)automaticForegroundColorDetectionEnabled 123 | { 124 | _automaticForegroundColorDetectionEnabled = automaticForegroundColorDetectionEnabled; 125 | 126 | if (automaticForegroundColorDetectionEnabled) { 127 | __weak typeof(self) weakSelf = self; 128 | 129 | NSMutableArray *ranges = [NSMutableArray array]; 130 | [self.attributedText enumerateAttribute:NSForegroundColorAttributeName 131 | inRange:NSMakeRange(0,self.attributedText.length) 132 | options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired 133 | usingBlock:^(id value, NSRange range, BOOL *stop) 134 | { 135 | if (!weakSelf.skipColorForAutomaticDetection || (weakSelf.skipColorForAutomaticDetection && ![weakSelf.skipColorForAutomaticDetection isEqualToColor:value])) { 136 | [ranges addObject:[MZSelectableLabelRange selectableRangeWithRange:range color:self.automaticDetectionBackgroundHighlightColor]]; 137 | } 138 | 139 | }]; 140 | self.detectedSelectableRanges = [ranges copy]; 141 | 142 | } else { 143 | self.detectedSelectableRanges = nil; 144 | } 145 | [self updateTextStoreWithText]; 146 | } 147 | 148 | - (void)setText:(NSString *)text 149 | { 150 | [super setText:text]; 151 | [self updateTextStoreWithText]; 152 | } 153 | 154 | - (void)setAttributedText:(NSAttributedString *)attributedText 155 | { 156 | [super setAttributedText:attributedText]; 157 | [self updateTextStoreWithText]; 158 | } 159 | 160 | - (void)setSelectableRange:(NSRange)range hightlightedBackgroundColor:(UIColor *)color 161 | { 162 | [self.selectableRanges addObject:[MZSelectableLabelRange selectableRangeWithRange:range color:color]]; 163 | } 164 | 165 | - (void)setSelectableRange:(NSRange)range 166 | { 167 | [self setSelectableRange:range hightlightedBackgroundColor:nil]; 168 | } 169 | 170 | // Applies background colour to selected range. Used to hilight touched links 171 | - (void)setSelectedRange:(NSRange)range 172 | { 173 | // Remove the current selection if the selection is changing 174 | if (self.selectedRange.length && !NSEqualRanges(self.selectedRange, range)) 175 | { 176 | [self.textStorage removeAttribute:NSBackgroundColorAttributeName 177 | range:self.selectedRange]; 178 | } 179 | 180 | MZSelectableLabelRange *selectedRange = [self rangeValueAtRange:range]; 181 | 182 | // Apply the new selection to the text 183 | if (range.length && selectedRange && selectedRange.color) 184 | { 185 | [self.textStorage addAttribute:NSBackgroundColorAttributeName 186 | value:selectedRange.color 187 | range:range]; 188 | } 189 | 190 | // Save the new range 191 | _selectedRange = range; 192 | 193 | [self setNeedsDisplay]; 194 | } 195 | 196 | - (void)updateTextStoreWithText 197 | { 198 | // Now update our storage from either the attributedString or the plain text 199 | if (self.attributedText) 200 | { 201 | [self updateTextStoreWithAttributedString:self.attributedText]; 202 | } 203 | else if (self.text) 204 | { 205 | [self updateTextStoreWithAttributedString:[[NSAttributedString alloc] initWithString:self.text attributes:[self attributesFromProperties]]]; 206 | } 207 | else 208 | { 209 | [self updateTextStoreWithAttributedString:[[NSAttributedString alloc] initWithString:@"" attributes:[self attributesFromProperties]]]; 210 | } 211 | 212 | [self setNeedsDisplay]; 213 | } 214 | 215 | - (void)updateTextStoreWithAttributedString:(NSAttributedString *)attributedString 216 | { 217 | if (attributedString.length != 0) 218 | { 219 | attributedString = [MZSelectableLabel sanitizeAttributedString:attributedString]; 220 | } 221 | 222 | if (self.textStorage) 223 | { 224 | // Set the string on the storage 225 | [self.textStorage setAttributedString:attributedString]; 226 | } 227 | else 228 | { 229 | // Create a new text storage and attach it correctly to the layout manager 230 | self.textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString]; 231 | [self.textStorage addLayoutManager:self.layoutManager]; 232 | [self.layoutManager setTextStorage:self.textStorage]; 233 | } 234 | } 235 | 236 | - (MZSelectableLabelRange *)rangeValueAtRange:(NSRange)range 237 | { 238 | for (MZSelectableLabelRange *selectableRange in self.selectableRanges) { 239 | if (NSEqualRanges(selectableRange.range, range)) { 240 | return selectableRange; 241 | } 242 | } 243 | 244 | for (MZSelectableLabelRange *selectableRange in self.detectedSelectableRanges) { 245 | if (NSEqualRanges(selectableRange.range, range)) { 246 | return selectableRange; 247 | } 248 | } 249 | 250 | return nil; 251 | } 252 | 253 | 254 | - (MZSelectableLabelRange *)rangeValueAtLocation:(CGPoint)location 255 | { 256 | // Do nothing if we have no text 257 | if (self.textStorage.string.length == 0) 258 | { 259 | return nil; 260 | } 261 | 262 | // Work out the offset of the text in the view 263 | CGPoint textOffset; 264 | NSRange glyphRange = [self.layoutManager glyphRangeForTextContainer:self.textContainer]; 265 | textOffset = [self calcTextOffsetForGlyphRange:glyphRange]; 266 | 267 | // Get the touch location and use text offset to convert to text cotainer coords 268 | location.x -= textOffset.x; 269 | location.y -= textOffset.y; 270 | 271 | NSUInteger touchedChar = [self.layoutManager glyphIndexForPoint:location inTextContainer:self.textContainer]; 272 | 273 | // If the touch is in white space after the last glyph on the line we don't 274 | // count it as a hit on the text 275 | NSRange lineRange; 276 | CGRect lineRect = [self.layoutManager lineFragmentUsedRectForGlyphAtIndex:touchedChar effectiveRange:&lineRange]; 277 | if (CGRectContainsPoint(lineRect, location) == NO) 278 | { 279 | return nil; 280 | } 281 | 282 | // Find the word that was touched and call the detection block 283 | for (MZSelectableLabelRange *rangeValue in self.selectableRanges) 284 | { 285 | NSRange range = rangeValue.range; 286 | 287 | if ((touchedChar >= range.location) && touchedChar < (range.location + range.length)) 288 | { 289 | return rangeValue; 290 | } 291 | } 292 | 293 | for (MZSelectableLabelRange *rangeValue in self.detectedSelectableRanges) 294 | { 295 | NSRange range = rangeValue.range; 296 | 297 | if ((touchedChar >= range.location) && touchedChar < (range.location + range.length)) 298 | { 299 | return rangeValue; 300 | } 301 | } 302 | 303 | return nil; 304 | } 305 | 306 | // Returns the XY offset of the range of glyphs from the view's origin 307 | - (CGPoint)calcTextOffsetForGlyphRange:(NSRange)glyphRange 308 | { 309 | CGPoint textOffset = CGPointZero; 310 | 311 | CGRect textBounds = [self.layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:self.textContainer]; 312 | CGFloat paddingHeight = (self.bounds.size.height - textBounds.size.height) / 2.0f; 313 | if (paddingHeight > 0) 314 | { 315 | textOffset.y = paddingHeight; 316 | } 317 | 318 | return textOffset; 319 | } 320 | 321 | #pragma mark - Layout and Rendering 322 | 323 | // Returns attributed string attributes based on the text properties set on the label. 324 | // These are styles that are only applied when NOT using the attributedText directly. 325 | - (NSDictionary *)attributesFromProperties 326 | { 327 | // Setup shadow attributes 328 | NSShadow *shadow = shadow = [[NSShadow alloc] init]; 329 | if (self.shadowColor) 330 | { 331 | shadow.shadowColor = self.shadowColor; 332 | shadow.shadowOffset = self.shadowOffset; 333 | } 334 | else 335 | { 336 | shadow.shadowOffset = CGSizeMake(0, -1); 337 | shadow.shadowColor = nil; 338 | } 339 | 340 | // Setup colour attributes 341 | UIColor *colour = self.textColor; 342 | if (!self.isEnabled) 343 | { 344 | colour = [UIColor lightGrayColor]; 345 | } 346 | else if (self.isHighlighted) 347 | { 348 | colour = self.highlightedTextColor; 349 | } 350 | 351 | // Setup paragraph attributes 352 | NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init]; 353 | paragraph.alignment = self.textAlignment; 354 | 355 | // Create the dictionary 356 | NSDictionary *attributes = @{ 357 | NSFontAttributeName : self.font, 358 | NSForegroundColorAttributeName : colour, 359 | NSShadowAttributeName : shadow, 360 | NSParagraphStyleAttributeName : paragraph }; 361 | return attributes; 362 | } 363 | 364 | - (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines 365 | { 366 | // Use our text container to calculate the bounds required. First save our 367 | // current text container setup 368 | CGSize savedTextContainerSize = self.textContainer.size; 369 | NSInteger savedTextContainerNumberOfLines = self.textContainer.maximumNumberOfLines; 370 | 371 | // Apply the new potential bounds and number of lines 372 | self.textContainer.size = bounds.size; 373 | self.textContainer.maximumNumberOfLines = numberOfLines; 374 | 375 | // Measure the text with the new state 376 | CGRect textBounds; 377 | @try 378 | { 379 | NSRange glyphRange = [self.layoutManager glyphRangeForTextContainer:self.textContainer]; 380 | textBounds = [self.layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:self.textContainer]; 381 | 382 | // Position the bounds and round up the size for good measure 383 | textBounds.origin = bounds.origin; 384 | textBounds.size.width = ceilf(textBounds.size.width); 385 | textBounds.size.height = ceilf(textBounds.size.height); 386 | } 387 | @finally 388 | { 389 | // Restore the old container state before we exit under any circumstances 390 | self.textContainer.size = savedTextContainerSize; 391 | self.textContainer.maximumNumberOfLines = savedTextContainerNumberOfLines; 392 | } 393 | 394 | return textBounds; 395 | } 396 | 397 | - (void)drawTextInRect:(CGRect)rect 398 | { 399 | // Don't call super implementation. Might want to uncomment this out when 400 | // debugging layout and rendering problems. 401 | // [super drawTextInRect:rect]; 402 | 403 | // Calculate the offset of the text in the view 404 | CGPoint textOffset; 405 | NSRange glyphRange = [self.layoutManager glyphRangeForTextContainer:self.textContainer]; 406 | textOffset = [self calcTextOffsetForGlyphRange:glyphRange]; 407 | 408 | // Drawing code 409 | [self.layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:textOffset]; 410 | [self.layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:textOffset]; 411 | } 412 | 413 | - (void)setFrame:(CGRect)frame 414 | { 415 | [super setFrame:frame]; 416 | self.textContainer.size = self.bounds.size; 417 | } 418 | 419 | - (void)setBounds:(CGRect)bounds 420 | { 421 | [super setBounds:bounds]; 422 | self.textContainer.size = self.bounds.size; 423 | } 424 | 425 | - (void)layoutSubviews 426 | { 427 | [super layoutSubviews]; 428 | 429 | // Update our container size when the view frame changes 430 | self.textContainer.size = self.bounds.size; 431 | } 432 | 433 | #pragma mark - Interactions 434 | 435 | - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { 436 | MZSelectableLabelRange *touchedRange = [self rangeValueAtLocation:point]; 437 | return touchedRange != nil; 438 | } 439 | 440 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 441 | { 442 | self.isTouchMoved = NO; 443 | 444 | // Get the info for the touched link if there is one 445 | MZSelectableLabelRange *touchedRange; 446 | CGPoint touchLocation = [[touches anyObject] locationInView:self]; 447 | touchedRange = [self rangeValueAtLocation:touchLocation]; 448 | 449 | if (touchedRange) 450 | { 451 | self.selectedRange = touchedRange.range; 452 | } 453 | else 454 | { 455 | [super touchesBegan:touches withEvent:event]; 456 | } 457 | } 458 | 459 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 460 | { 461 | [super touchesMoved:touches withEvent:event]; 462 | 463 | UITouch *currentTouch = touches.anyObject; 464 | if (!CGPointEqualToPoint([currentTouch locationInView:currentTouch.view], 465 | [currentTouch previousLocationInView:currentTouch.view])) 466 | { 467 | self.isTouchMoved = YES; 468 | } 469 | } 470 | 471 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 472 | { 473 | [super touchesEnded:touches withEvent:event]; 474 | 475 | // If the user dragged their finger we ignore the touch 476 | if (self.isTouchMoved) 477 | { 478 | self.selectedRange = NSMakeRange(0, 0); 479 | 480 | return; 481 | } 482 | 483 | // Get the info for the touched link if there is one 484 | MZSelectableLabelRange *touchedRange; 485 | CGPoint touchLocation = [[touches anyObject] locationInView:self]; 486 | touchedRange = [self rangeValueAtLocation:touchLocation]; 487 | 488 | if (touchedRange) 489 | { 490 | if (self.selectionHandler) { 491 | self.selectionHandler(touchedRange.range, [[self.attributedText string] substringWithRange:touchedRange.range]); 492 | } 493 | } 494 | else 495 | { 496 | [super touchesBegan:touches withEvent:event]; 497 | } 498 | 499 | self.selectedRange = NSMakeRange(0, 0); 500 | } 501 | 502 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 503 | { 504 | [super touchesCancelled:touches withEvent:event]; 505 | 506 | // Make sure we don't leave a selection when the touch is cancelled 507 | self.selectedRange = NSMakeRange(0, 0); 508 | } 509 | 510 | 511 | #pragma mark - Layout manager delegate 512 | 513 | -(BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex 514 | { 515 | // Don't allow line breaks inside URLs 516 | NSRange range; 517 | NSURL *linkURL = [layoutManager.textStorage attribute:NSLinkAttributeName 518 | atIndex:charIndex 519 | effectiveRange:&range]; 520 | 521 | return !(linkURL && (charIndex > range.location) && (charIndex <= NSMaxRange(range))); 522 | } 523 | 524 | 525 | + (NSAttributedString *)sanitizeAttributedString:(NSAttributedString *)attributedString 526 | { 527 | // Setup paragraph alignement properly. IB applies the line break style 528 | // to the attributed string. The problem is that the text container then 529 | // breaks at the first line of text. If we set the line break to wrapping 530 | // then the text container defines the break mode and it works. 531 | // NOTE: This is either an Apple bug or something I've misunderstood. 532 | 533 | // Get the current paragraph style. IB only allows a single paragraph so 534 | // getting the style of the first char is fine. 535 | NSRange range; 536 | NSParagraphStyle *paragraphStyle = [attributedString attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:&range]; 537 | 538 | if (paragraphStyle == nil) 539 | { 540 | return attributedString; 541 | } 542 | 543 | // Remove the line breaks 544 | NSMutableParagraphStyle *mutableParagraphStyle = [paragraphStyle mutableCopy]; 545 | mutableParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping; 546 | 547 | // Apply new style 548 | NSMutableAttributedString *restyled = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString]; 549 | [restyled addAttribute:NSParagraphStyleAttributeName value:mutableParagraphStyle range:NSMakeRange(0, restyled.length)]; 550 | 551 | return restyled; 552 | } 553 | 554 | @end 555 | 556 | @implementation MZSelectableLabelRange 557 | 558 | + (instancetype)selectableRangeWithRange:(NSRange)range color:(UIColor *)color 559 | { 560 | MZSelectableLabelRange *selectableRange = [[[self class] alloc] init]; 561 | selectableRange.range = range; 562 | selectableRange.color = color; 563 | return selectableRange; 564 | } 565 | @end 566 | -------------------------------------------------------------------------------- /MZSelectableLabel/UIColor+Equalable.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Equalable.h 3 | // MZSelectableLabelDemo 4 | // 5 | // Created by Michał Zaborowski on 05.08.2014. 6 | // Copyright (c) 2014 Michal Zaborowski. All rights reserved. 7 | // 8 | // http://stackoverflow.com/questions/970475/how-to-compare-uicolors 9 | 10 | #import 11 | 12 | @interface UIColor (Equalable) 13 | - (BOOL)isEqualToColor:(UIColor *)otherColor; 14 | @end 15 | -------------------------------------------------------------------------------- /MZSelectableLabel/UIColor+Equalable.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Equalable.m 3 | // MZSelectableLabelDemo 4 | // 5 | // Created by Michał Zaborowski on 05.08.2014. 6 | // Copyright (c) 2014 Michal Zaborowski. All rights reserved. 7 | // 8 | // http://stackoverflow.com/questions/970475/how-to-compare-uicolors 9 | 10 | #import "UIColor+Equalable.h" 11 | 12 | @implementation UIColor (Equalable) 13 | 14 | - (BOOL)isEqualToColor:(UIColor *)otherColor { 15 | CGColorSpaceRef colorSpaceRGB = CGColorSpaceCreateDeviceRGB(); 16 | 17 | UIColor *(^convertColorToRGBSpace)(UIColor*) = ^(UIColor *color) { 18 | if(CGColorSpaceGetModel(CGColorGetColorSpace(color.CGColor)) == kCGColorSpaceModelMonochrome) { 19 | const CGFloat *oldComponents = CGColorGetComponents(color.CGColor); 20 | CGFloat components[4] = {oldComponents[0], oldComponents[0], oldComponents[0], oldComponents[1]}; 21 | CGColorRef colorRef = CGColorCreate( colorSpaceRGB, components ); 22 | 23 | UIColor *color = [UIColor colorWithCGColor:colorRef]; 24 | CGColorRelease(colorRef); 25 | return color; 26 | } else 27 | return color; 28 | }; 29 | 30 | UIColor *selfColor = convertColorToRGBSpace(self); 31 | otherColor = convertColorToRGBSpace(otherColor); 32 | CGColorSpaceRelease(colorSpaceRGB); 33 | 34 | return [selfColor isEqual:otherColor]; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1FA74B4A1990C30700FCD5FA /* UIColor+Equalable.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FA74B491990C30700FCD5FA /* UIColor+Equalable.m */; }; 11 | 1FAA3633198FD106005FAD10 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FAA3632198FD106005FAD10 /* main.m */; }; 12 | 1FAA3636198FD106005FAD10 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FAA3635198FD106005FAD10 /* AppDelegate.m */; }; 13 | 1FAA3639198FD106005FAD10 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FAA3638198FD106005FAD10 /* ViewController.m */; }; 14 | 1FAA363C198FD106005FAD10 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1FAA363A198FD106005FAD10 /* Main.storyboard */; }; 15 | 1FAA363E198FD106005FAD10 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1FAA363D198FD106005FAD10 /* Images.xcassets */; }; 16 | 1FAA364A198FD106005FAD10 /* MZSelectableLabelDemoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FAA3649198FD106005FAD10 /* MZSelectableLabelDemoTests.m */; }; 17 | 1FAA3656198FD145005FAD10 /* MZSelectableLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FAA3655198FD145005FAD10 /* MZSelectableLabel.m */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | 1FAA3644198FD106005FAD10 /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = 1FAA3625198FD105005FAD10 /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = 1FAA362C198FD105005FAD10; 26 | remoteInfo = MZSelectableLabelDemo; 27 | }; 28 | /* End PBXContainerItemProxy section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 1FA74B481990C30700FCD5FA /* UIColor+Equalable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+Equalable.h"; sourceTree = ""; }; 32 | 1FA74B491990C30700FCD5FA /* UIColor+Equalable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+Equalable.m"; sourceTree = ""; }; 33 | 1FAA362D198FD106005FAD10 /* MZSelectableLabelDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MZSelectableLabelDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 1FAA3631198FD106005FAD10 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | 1FAA3632198FD106005FAD10 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 36 | 1FAA3634198FD106005FAD10 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 37 | 1FAA3635198FD106005FAD10 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 38 | 1FAA3637198FD106005FAD10 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 39 | 1FAA3638198FD106005FAD10 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 40 | 1FAA363B198FD106005FAD10 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 41 | 1FAA363D198FD106005FAD10 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 42 | 1FAA3643198FD106005FAD10 /* MZSelectableLabelDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MZSelectableLabelDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | 1FAA3648198FD106005FAD10 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 44 | 1FAA3649198FD106005FAD10 /* MZSelectableLabelDemoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MZSelectableLabelDemoTests.m; sourceTree = ""; }; 45 | 1FAA3654198FD145005FAD10 /* MZSelectableLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MZSelectableLabel.h; sourceTree = ""; }; 46 | 1FAA3655198FD145005FAD10 /* MZSelectableLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MZSelectableLabel.m; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | 1FAA362A198FD105005FAD10 /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | 1FAA3640198FD106005FAD10 /* Frameworks */ = { 58 | isa = PBXFrameworksBuildPhase; 59 | buildActionMask = 2147483647; 60 | files = ( 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | 1FAA3624198FD105005FAD10 = { 68 | isa = PBXGroup; 69 | children = ( 70 | 1FAA3653198FD145005FAD10 /* MZSelectableLabel */, 71 | 1FAA362F198FD106005FAD10 /* MZSelectableLabelDemo */, 72 | 1FAA3646198FD106005FAD10 /* MZSelectableLabelDemoTests */, 73 | 1FAA362E198FD106005FAD10 /* Products */, 74 | ); 75 | sourceTree = ""; 76 | }; 77 | 1FAA362E198FD106005FAD10 /* Products */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | 1FAA362D198FD106005FAD10 /* MZSelectableLabelDemo.app */, 81 | 1FAA3643198FD106005FAD10 /* MZSelectableLabelDemoTests.xctest */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 1FAA362F198FD106005FAD10 /* MZSelectableLabelDemo */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 1FAA3634198FD106005FAD10 /* AppDelegate.h */, 90 | 1FAA3635198FD106005FAD10 /* AppDelegate.m */, 91 | 1FAA3637198FD106005FAD10 /* ViewController.h */, 92 | 1FAA3638198FD106005FAD10 /* ViewController.m */, 93 | 1FAA363A198FD106005FAD10 /* Main.storyboard */, 94 | 1FAA363D198FD106005FAD10 /* Images.xcassets */, 95 | 1FAA3630198FD106005FAD10 /* Supporting Files */, 96 | ); 97 | path = MZSelectableLabelDemo; 98 | sourceTree = ""; 99 | }; 100 | 1FAA3630198FD106005FAD10 /* Supporting Files */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 1FAA3631198FD106005FAD10 /* Info.plist */, 104 | 1FAA3632198FD106005FAD10 /* main.m */, 105 | ); 106 | name = "Supporting Files"; 107 | sourceTree = ""; 108 | }; 109 | 1FAA3646198FD106005FAD10 /* MZSelectableLabelDemoTests */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 1FAA3649198FD106005FAD10 /* MZSelectableLabelDemoTests.m */, 113 | 1FAA3647198FD106005FAD10 /* Supporting Files */, 114 | ); 115 | path = MZSelectableLabelDemoTests; 116 | sourceTree = ""; 117 | }; 118 | 1FAA3647198FD106005FAD10 /* Supporting Files */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 1FAA3648198FD106005FAD10 /* Info.plist */, 122 | ); 123 | name = "Supporting Files"; 124 | sourceTree = ""; 125 | }; 126 | 1FAA3653198FD145005FAD10 /* MZSelectableLabel */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | 1FAA3654198FD145005FAD10 /* MZSelectableLabel.h */, 130 | 1FAA3655198FD145005FAD10 /* MZSelectableLabel.m */, 131 | 1FA74B481990C30700FCD5FA /* UIColor+Equalable.h */, 132 | 1FA74B491990C30700FCD5FA /* UIColor+Equalable.m */, 133 | ); 134 | name = MZSelectableLabel; 135 | path = ../MZSelectableLabel; 136 | sourceTree = ""; 137 | }; 138 | /* End PBXGroup section */ 139 | 140 | /* Begin PBXNativeTarget section */ 141 | 1FAA362C198FD105005FAD10 /* MZSelectableLabelDemo */ = { 142 | isa = PBXNativeTarget; 143 | buildConfigurationList = 1FAA364D198FD106005FAD10 /* Build configuration list for PBXNativeTarget "MZSelectableLabelDemo" */; 144 | buildPhases = ( 145 | 1FAA3629198FD105005FAD10 /* Sources */, 146 | 1FAA362A198FD105005FAD10 /* Frameworks */, 147 | 1FAA362B198FD105005FAD10 /* Resources */, 148 | ); 149 | buildRules = ( 150 | ); 151 | dependencies = ( 152 | ); 153 | name = MZSelectableLabelDemo; 154 | productName = MZSelectableLabelDemo; 155 | productReference = 1FAA362D198FD106005FAD10 /* MZSelectableLabelDemo.app */; 156 | productType = "com.apple.product-type.application"; 157 | }; 158 | 1FAA3642198FD106005FAD10 /* MZSelectableLabelDemoTests */ = { 159 | isa = PBXNativeTarget; 160 | buildConfigurationList = 1FAA3650198FD106005FAD10 /* Build configuration list for PBXNativeTarget "MZSelectableLabelDemoTests" */; 161 | buildPhases = ( 162 | 1FAA363F198FD106005FAD10 /* Sources */, 163 | 1FAA3640198FD106005FAD10 /* Frameworks */, 164 | 1FAA3641198FD106005FAD10 /* Resources */, 165 | ); 166 | buildRules = ( 167 | ); 168 | dependencies = ( 169 | 1FAA3645198FD106005FAD10 /* PBXTargetDependency */, 170 | ); 171 | name = MZSelectableLabelDemoTests; 172 | productName = MZSelectableLabelDemoTests; 173 | productReference = 1FAA3643198FD106005FAD10 /* MZSelectableLabelDemoTests.xctest */; 174 | productType = "com.apple.product-type.bundle.unit-test"; 175 | }; 176 | /* End PBXNativeTarget section */ 177 | 178 | /* Begin PBXProject section */ 179 | 1FAA3625198FD105005FAD10 /* Project object */ = { 180 | isa = PBXProject; 181 | attributes = { 182 | LastUpgradeCheck = 0600; 183 | ORGANIZATIONNAME = "Michal Zaborowski"; 184 | TargetAttributes = { 185 | 1FAA362C198FD105005FAD10 = { 186 | CreatedOnToolsVersion = 6.0; 187 | }; 188 | 1FAA3642198FD106005FAD10 = { 189 | CreatedOnToolsVersion = 6.0; 190 | TestTargetID = 1FAA362C198FD105005FAD10; 191 | }; 192 | }; 193 | }; 194 | buildConfigurationList = 1FAA3628198FD105005FAD10 /* Build configuration list for PBXProject "MZSelectableLabelDemo" */; 195 | compatibilityVersion = "Xcode 3.2"; 196 | developmentRegion = English; 197 | hasScannedForEncodings = 0; 198 | knownRegions = ( 199 | en, 200 | Base, 201 | ); 202 | mainGroup = 1FAA3624198FD105005FAD10; 203 | productRefGroup = 1FAA362E198FD106005FAD10 /* Products */; 204 | projectDirPath = ""; 205 | projectRoot = ""; 206 | targets = ( 207 | 1FAA362C198FD105005FAD10 /* MZSelectableLabelDemo */, 208 | 1FAA3642198FD106005FAD10 /* MZSelectableLabelDemoTests */, 209 | ); 210 | }; 211 | /* End PBXProject section */ 212 | 213 | /* Begin PBXResourcesBuildPhase section */ 214 | 1FAA362B198FD105005FAD10 /* Resources */ = { 215 | isa = PBXResourcesBuildPhase; 216 | buildActionMask = 2147483647; 217 | files = ( 218 | 1FAA363C198FD106005FAD10 /* Main.storyboard in Resources */, 219 | 1FAA363E198FD106005FAD10 /* Images.xcassets in Resources */, 220 | ); 221 | runOnlyForDeploymentPostprocessing = 0; 222 | }; 223 | 1FAA3641198FD106005FAD10 /* Resources */ = { 224 | isa = PBXResourcesBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | ); 228 | runOnlyForDeploymentPostprocessing = 0; 229 | }; 230 | /* End PBXResourcesBuildPhase section */ 231 | 232 | /* Begin PBXSourcesBuildPhase section */ 233 | 1FAA3629198FD105005FAD10 /* Sources */ = { 234 | isa = PBXSourcesBuildPhase; 235 | buildActionMask = 2147483647; 236 | files = ( 237 | 1FA74B4A1990C30700FCD5FA /* UIColor+Equalable.m in Sources */, 238 | 1FAA3639198FD106005FAD10 /* ViewController.m in Sources */, 239 | 1FAA3656198FD145005FAD10 /* MZSelectableLabel.m in Sources */, 240 | 1FAA3636198FD106005FAD10 /* AppDelegate.m in Sources */, 241 | 1FAA3633198FD106005FAD10 /* main.m in Sources */, 242 | ); 243 | runOnlyForDeploymentPostprocessing = 0; 244 | }; 245 | 1FAA363F198FD106005FAD10 /* Sources */ = { 246 | isa = PBXSourcesBuildPhase; 247 | buildActionMask = 2147483647; 248 | files = ( 249 | 1FAA364A198FD106005FAD10 /* MZSelectableLabelDemoTests.m in Sources */, 250 | ); 251 | runOnlyForDeploymentPostprocessing = 0; 252 | }; 253 | /* End PBXSourcesBuildPhase section */ 254 | 255 | /* Begin PBXTargetDependency section */ 256 | 1FAA3645198FD106005FAD10 /* PBXTargetDependency */ = { 257 | isa = PBXTargetDependency; 258 | target = 1FAA362C198FD105005FAD10 /* MZSelectableLabelDemo */; 259 | targetProxy = 1FAA3644198FD106005FAD10 /* PBXContainerItemProxy */; 260 | }; 261 | /* End PBXTargetDependency section */ 262 | 263 | /* Begin PBXVariantGroup section */ 264 | 1FAA363A198FD106005FAD10 /* Main.storyboard */ = { 265 | isa = PBXVariantGroup; 266 | children = ( 267 | 1FAA363B198FD106005FAD10 /* Base */, 268 | ); 269 | name = Main.storyboard; 270 | sourceTree = ""; 271 | }; 272 | /* End PBXVariantGroup section */ 273 | 274 | /* Begin XCBuildConfiguration section */ 275 | 1FAA364B198FD106005FAD10 /* Debug */ = { 276 | isa = XCBuildConfiguration; 277 | buildSettings = { 278 | ALWAYS_SEARCH_USER_PATHS = NO; 279 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 280 | CLANG_CXX_LIBRARY = "libc++"; 281 | CLANG_ENABLE_MODULES = YES; 282 | CLANG_ENABLE_OBJC_ARC = YES; 283 | CLANG_WARN_BOOL_CONVERSION = YES; 284 | CLANG_WARN_CONSTANT_CONVERSION = YES; 285 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 286 | CLANG_WARN_EMPTY_BODY = YES; 287 | CLANG_WARN_ENUM_CONVERSION = YES; 288 | CLANG_WARN_INT_CONVERSION = YES; 289 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 290 | CLANG_WARN_UNREACHABLE_CODE = YES; 291 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 292 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 293 | COPY_PHASE_STRIP = NO; 294 | ENABLE_STRICT_OBJC_MSGSEND = YES; 295 | GCC_C_LANGUAGE_STANDARD = gnu99; 296 | GCC_DYNAMIC_NO_PIC = NO; 297 | GCC_OPTIMIZATION_LEVEL = 0; 298 | GCC_PREPROCESSOR_DEFINITIONS = ( 299 | "DEBUG=1", 300 | "$(inherited)", 301 | ); 302 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 303 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 304 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 305 | GCC_WARN_UNDECLARED_SELECTOR = YES; 306 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 307 | GCC_WARN_UNUSED_FUNCTION = YES; 308 | GCC_WARN_UNUSED_VARIABLE = YES; 309 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 310 | MTL_ENABLE_DEBUG_INFO = YES; 311 | ONLY_ACTIVE_ARCH = YES; 312 | SDKROOT = iphoneos; 313 | }; 314 | name = Debug; 315 | }; 316 | 1FAA364C198FD106005FAD10 /* Release */ = { 317 | isa = XCBuildConfiguration; 318 | buildSettings = { 319 | ALWAYS_SEARCH_USER_PATHS = NO; 320 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 321 | CLANG_CXX_LIBRARY = "libc++"; 322 | CLANG_ENABLE_MODULES = YES; 323 | CLANG_ENABLE_OBJC_ARC = YES; 324 | CLANG_WARN_BOOL_CONVERSION = YES; 325 | CLANG_WARN_CONSTANT_CONVERSION = YES; 326 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 327 | CLANG_WARN_EMPTY_BODY = YES; 328 | CLANG_WARN_ENUM_CONVERSION = YES; 329 | CLANG_WARN_INT_CONVERSION = YES; 330 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 331 | CLANG_WARN_UNREACHABLE_CODE = YES; 332 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 333 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 334 | COPY_PHASE_STRIP = YES; 335 | ENABLE_NS_ASSERTIONS = NO; 336 | ENABLE_STRICT_OBJC_MSGSEND = YES; 337 | GCC_C_LANGUAGE_STANDARD = gnu99; 338 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 339 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 340 | GCC_WARN_UNDECLARED_SELECTOR = YES; 341 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 342 | GCC_WARN_UNUSED_FUNCTION = YES; 343 | GCC_WARN_UNUSED_VARIABLE = YES; 344 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 345 | MTL_ENABLE_DEBUG_INFO = NO; 346 | SDKROOT = iphoneos; 347 | VALIDATE_PRODUCT = YES; 348 | }; 349 | name = Release; 350 | }; 351 | 1FAA364E198FD106005FAD10 /* Debug */ = { 352 | isa = XCBuildConfiguration; 353 | buildSettings = { 354 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 355 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 356 | INFOPLIST_FILE = MZSelectableLabelDemo/Info.plist; 357 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 358 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 359 | PRODUCT_NAME = "$(TARGET_NAME)"; 360 | }; 361 | name = Debug; 362 | }; 363 | 1FAA364F198FD106005FAD10 /* Release */ = { 364 | isa = XCBuildConfiguration; 365 | buildSettings = { 366 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 367 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 368 | INFOPLIST_FILE = MZSelectableLabelDemo/Info.plist; 369 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 370 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 371 | PRODUCT_NAME = "$(TARGET_NAME)"; 372 | }; 373 | name = Release; 374 | }; 375 | 1FAA3651198FD106005FAD10 /* Debug */ = { 376 | isa = XCBuildConfiguration; 377 | buildSettings = { 378 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/MZSelectableLabelDemo.app/MZSelectableLabelDemo"; 379 | FRAMEWORK_SEARCH_PATHS = ( 380 | "$(SDKROOT)/Developer/Library/Frameworks", 381 | "$(inherited)", 382 | ); 383 | GCC_PREPROCESSOR_DEFINITIONS = ( 384 | "DEBUG=1", 385 | "$(inherited)", 386 | ); 387 | INFOPLIST_FILE = MZSelectableLabelDemoTests/Info.plist; 388 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 389 | PRODUCT_NAME = "$(TARGET_NAME)"; 390 | TEST_HOST = "$(BUNDLE_LOADER)"; 391 | }; 392 | name = Debug; 393 | }; 394 | 1FAA3652198FD106005FAD10 /* Release */ = { 395 | isa = XCBuildConfiguration; 396 | buildSettings = { 397 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/MZSelectableLabelDemo.app/MZSelectableLabelDemo"; 398 | FRAMEWORK_SEARCH_PATHS = ( 399 | "$(SDKROOT)/Developer/Library/Frameworks", 400 | "$(inherited)", 401 | ); 402 | INFOPLIST_FILE = MZSelectableLabelDemoTests/Info.plist; 403 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 404 | PRODUCT_NAME = "$(TARGET_NAME)"; 405 | TEST_HOST = "$(BUNDLE_LOADER)"; 406 | }; 407 | name = Release; 408 | }; 409 | /* End XCBuildConfiguration section */ 410 | 411 | /* Begin XCConfigurationList section */ 412 | 1FAA3628198FD105005FAD10 /* Build configuration list for PBXProject "MZSelectableLabelDemo" */ = { 413 | isa = XCConfigurationList; 414 | buildConfigurations = ( 415 | 1FAA364B198FD106005FAD10 /* Debug */, 416 | 1FAA364C198FD106005FAD10 /* Release */, 417 | ); 418 | defaultConfigurationIsVisible = 0; 419 | defaultConfigurationName = Release; 420 | }; 421 | 1FAA364D198FD106005FAD10 /* Build configuration list for PBXNativeTarget "MZSelectableLabelDemo" */ = { 422 | isa = XCConfigurationList; 423 | buildConfigurations = ( 424 | 1FAA364E198FD106005FAD10 /* Debug */, 425 | 1FAA364F198FD106005FAD10 /* Release */, 426 | ); 427 | defaultConfigurationIsVisible = 0; 428 | defaultConfigurationName = Release; 429 | }; 430 | 1FAA3650198FD106005FAD10 /* Build configuration list for PBXNativeTarget "MZSelectableLabelDemoTests" */ = { 431 | isa = XCConfigurationList; 432 | buildConfigurations = ( 433 | 1FAA3651198FD106005FAD10 /* Debug */, 434 | 1FAA3652198FD106005FAD10 /* Release */, 435 | ); 436 | defaultConfigurationIsVisible = 0; 437 | defaultConfigurationName = Release; 438 | }; 439 | /* End XCConfigurationList section */ 440 | }; 441 | rootObject = 1FAA3625198FD105005FAD10 /* Project object */; 442 | } 443 | -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo.xcodeproj/project.xcworkspace/xcuserdata/michal.zaborowski.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1entus/MZSelectableLabel/ad8bb31e8850fd144d795f58114bb627d9906b63/MZSelectableLabelDemo/MZSelectableLabelDemo.xcodeproj/project.xcworkspace/xcuserdata/michal.zaborowski.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo.xcodeproj/xcuserdata/michal.zaborowski.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo.xcodeproj/xcuserdata/michal.zaborowski.xcuserdatad/xcschemes/MZSelectableLabelDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 61 | 62 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo.xcodeproj/xcuserdata/michal.zaborowski.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MZSelectableLabelDemo.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 1FAA362C198FD105005FAD10 16 | 17 | primary 18 | 19 | 20 | 1FAA3642198FD106005FAD10 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // MZSelectableLabelDemo 4 | // 5 | // Created by Michał Zaborowski on 04.08.2014. 6 | // Copyright (c) 2014 Michal Zaborowski. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // MZSelectableLabelDemo 4 | // 5 | // Created by Michał Zaborowski on 04.08.2014. 6 | // Copyright (c) 2014 Michal Zaborowski. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | - (void)applicationWillResignActive:(UIApplication *)application { 24 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 25 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 26 | } 27 | 28 | - (void)applicationDidEnterBackground:(UIApplication *)application { 29 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 30 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 31 | } 32 | 33 | - (void)applicationWillEnterForeground:(UIApplication *)application { 34 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 35 | } 36 | 37 | - (void)applicationDidBecomeActive:(UIApplication *)application { 38 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 39 | } 40 | 41 | - (void)applicationWillTerminate:(UIApplication *)application { 42 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "40x40", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "60x60", 16 | "scale" : "2x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "portrait", 12 | "idiom" : "iphone", 13 | "subtype" : "retina4", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "7.0", 16 | "scale" : "2x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | io.inspace.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // MZSelectableLabelDemo 4 | // 5 | // Created by Michał Zaborowski on 04.08.2014. 6 | // Copyright (c) 2014 Michal Zaborowski. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // MZSelectableLabelDemo 4 | // 5 | // Created by Michał Zaborowski on 04.08.2014. 6 | // Copyright (c) 2014 Michal Zaborowski. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "MZSelectableLabel.h" 11 | 12 | @interface ViewController () 13 | @property (weak, nonatomic) IBOutlet MZSelectableLabel *label; 14 | 15 | @end 16 | 17 | @implementation ViewController 18 | 19 | - (void)viewDidLoad { 20 | [super viewDidLoad]; 21 | 22 | // to respond to links being touched by the user. 23 | self.label.selectionHandler = ^(NSRange range, NSString *string) { 24 | // Put up an alert with a message if it's not an URL 25 | 26 | NSString *message = [NSString stringWithFormat:@"You tapped %@", string]; 27 | UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Hello" 28 | message:message 29 | delegate:nil 30 | cancelButtonTitle:@"Dismiss" 31 | otherButtonTitles:nil]; 32 | [alert show]; 33 | }; 34 | 35 | // Uncomment to automatic detection! 36 | // self.label.skipColorForAutomaticDetection = [UIColor blackColor]; 37 | // self.label.automaticDetectionBackgroundHighlightColor = [UIColor lightGrayColor]; 38 | // self.label.automaticForegroundColorDetectionEnabled = YES; 39 | 40 | 41 | if (!self.label.automaticForegroundColorDetectionEnabled) { 42 | [self.label setSelectableRange:[[self.label.attributedText string] rangeOfString:@"Tap me"] hightlightedBackgroundColor:[UIColor colorWithWhite:0.3 alpha:0.3]]; 43 | } 44 | 45 | UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressLabel:)]; 46 | longPress.minimumPressDuration = 0.5; 47 | [self.label addGestureRecognizer:longPress]; 48 | 49 | } 50 | 51 | - (void)longPressLabel:(UILongPressGestureRecognizer *)recognizer 52 | { 53 | // Only accept gestures on our label and only in the begin state 54 | if ((recognizer.view != self.label) || (recognizer.state != UIGestureRecognizerStateBegan)) 55 | { 56 | return; 57 | } 58 | 59 | // Get the position of the touch in the label 60 | CGPoint location = [recognizer locationInView:self.label]; 61 | 62 | // Get the link under the location from the label 63 | MZSelectableLabelRange *selectedRange = [self.label rangeValueAtLocation:location]; 64 | 65 | if (!selectedRange) 66 | { 67 | // No link was touched 68 | return; 69 | } 70 | 71 | NSString *message = [NSString stringWithFormat:@"You long pressed %@", [[self.label.attributedText string] substringWithRange:selectedRange.range]]; 72 | UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Hello" 73 | message:message 74 | delegate:nil 75 | cancelButtonTitle:@"Dismiss" 76 | otherButtonTitles:nil]; 77 | [alert show]; 78 | 79 | } 80 | 81 | 82 | - (void)didReceiveMemoryWarning { 83 | [super didReceiveMemoryWarning]; 84 | // Dispose of any resources that can be recreated. 85 | } 86 | 87 | @end 88 | -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // MZSelectableLabelDemo 4 | // 5 | // Created by Michał Zaborowski on 04.08.2014. 6 | // Copyright (c) 2014 Michal Zaborowski. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | io.inspace.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /MZSelectableLabelDemo/MZSelectableLabelDemoTests/MZSelectableLabelDemoTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // MZSelectableLabelDemoTests.m 3 | // MZSelectableLabelDemoTests 4 | // 5 | // Created by Michał Zaborowski on 04.08.2014. 6 | // Copyright (c) 2014 Michal Zaborowski. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface MZSelectableLabelDemoTests : XCTestCase 13 | 14 | @end 15 | 16 | @implementation MZSelectableLabelDemoTests 17 | 18 | - (void)setUp { 19 | [super setUp]; 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | - (void)tearDown { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | [super tearDown]; 26 | } 27 | 28 | - (void)testExample { 29 | // This is an example of a functional test case. 30 | XCTAssert(YES, @"Pass"); 31 | } 32 | 33 | - (void)testPerformanceExample { 34 | // This is an example of a performance test case. 35 | [self measureBlock:^{ 36 | // Put the code you want to measure the time of here. 37 | }]; 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MZSelectableLabel 2 | =========== 3 | 4 | A simple to use drop in replacement for UILabel for iOS 7 that provides automatic detection of colors. 5 | 6 | [![](https://raw.github.com/m1entus/MZSelectableLabel/master/Screens/screen1.png)](https://raw.github.com/m1entus/MZSelectableLabel/master/Screens/screen1.png) 7 | 8 | ## How To Use 9 | 10 | 1. Add the files MZSelectableLabel.m and MZSelectableLabel.h to your project. 11 | 2. Design your user interface as you would normally. In Interface Builder set the custom class for any UILabel you want to replace to MZSelectableLabel. 12 | The label should honour all IB settings or create MZSelectableLabel objects in code. 13 | 14 | Additional: 15 | 16 | You can set automatic foreground color detection: 17 | ```objective-c 18 | @property (nonatomic, assign, getter = isAutomaticForegroundColorDetectionEnabled) IBInspectable BOOL automaticForegroundColorDetectionEnabled; 19 | @property (nonatomic, strong) IBInspectable UIColor *skipColorForAutomaticDetection; 20 | ``` 21 | 22 | This means that if you have attributed string, MZSelectableLabel will detect all ranges for you. 23 | Even you can skip detection for default color for example `[UIColor blackColor]`. 24 | 25 | ## Example 26 | 27 | ```objective-c 28 | [self.label setSelectableRange:[[self.label.attributedText string] rangeOfString:@"Tap me"] hightlightedBackgroundColor:[UIColor colorWithWhite:0.3 alpha:0.3]]; 29 | 30 | self.label.selectionHandler = ^(NSRange range, NSString *string) { 31 | 32 | NSString *message = [NSString stringWithFormat:@"You tapped %@", string]; 33 | UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Hello" 34 | message:message 35 | delegate:nil 36 | cancelButtonTitle:@"Dismiss" 37 | otherButtonTitles:nil]; 38 | [alert show]; 39 | }; 40 | ``` 41 | 42 | ## Demo 43 | 44 | ```objective-c 45 | Repository includes MZSelectableLabel that shows a simple use of the label in a storyboard with examples for implementing tappable links. 46 | 47 | The demo also demonstrates how to use a gesture recognizer with the label to implement a long press on a link, which uses the rangeValueAtLocation method. 48 | ``` 49 | 50 | ## Requirements 51 | 52 | MZSelectableLabel requires either iOS 7.x and above. 53 | 54 | 55 | ## Contact 56 | 57 | [Michal Zaborowski](http://github.com/m1entus) 58 | 59 | [Twitter](https://twitter.com/iMientus) 60 | 61 | 62 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/m1entus/mzselectablelabel/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 63 | 64 | -------------------------------------------------------------------------------- /Screens/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m1entus/MZSelectableLabel/ad8bb31e8850fd144d795f58114bb627d9906b63/Screens/screen1.png --------------------------------------------------------------------------------