KILabel.m 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  1. /***********************************************************************************
  2. *
  3. * The MIT License (MIT)
  4. *
  5. * Copyright (c) 2013 Matthew Styles
  6. *
  7. * Permission is hereby granted, free of charge, to any person obtaining a copy of
  8. * this software and associated documentation files (the "Software"), to deal in
  9. * the Software without restriction, including without limitation the rights to
  10. * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  11. * the Software, and to permit persons to whom the Software is furnished to do so,
  12. * subject to the following conditions:
  13. *
  14. * The above copyright notice and this permission notice shall be included in all
  15. * copies or substantial portions of the Software.
  16. *
  17. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  19. * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  20. * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  21. * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  22. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  23. *
  24. ***********************************************************************************/
  25. #import "KILabel.h"
  26. NSString * const KILabelLinkTypeKey = @"linkType";
  27. NSString * const KILabelRangeKey = @"range";
  28. NSString * const KILabelLinkKey = @"link";
  29. #pragma mark - Private Interface
  30. @interface KILabel()
  31. // Used to control layout of glyphs and rendering
  32. @property (nonatomic, retain) NSLayoutManager *layoutManager;
  33. // Specifies the space in which to render text
  34. @property (nonatomic, retain) NSTextContainer *textContainer;
  35. // Backing storage for text that is rendered by the layout manager
  36. @property (nonatomic, retain) NSTextStorage *textStorage;
  37. // Dictionary of detected links and their ranges in the text
  38. @property (nonatomic, copy) NSArray *linkRanges;
  39. // State used to trag if the user has dragged during a touch
  40. @property (nonatomic, assign) BOOL isTouchMoved;
  41. // During a touch, range of text that is displayed as selected
  42. @property (nonatomic, assign) NSRange selectedRange;
  43. @property (nonatomic, strong) NSRegularExpression *urlRegex;
  44. @end
  45. #pragma mark - Implementation
  46. @implementation KILabel
  47. {
  48. NSMutableDictionary *_linkTypeAttributes;
  49. }
  50. #pragma mark - Construction
  51. - (id)initWithFrame:(CGRect)frame
  52. {
  53. self = [super initWithFrame:frame];
  54. if (self)
  55. {
  56. [self setupTextSystem];
  57. }
  58. return self;
  59. }
  60. - (id)initWithCoder:(NSCoder *)aDecoder
  61. {
  62. self = [super initWithCoder:aDecoder];
  63. if (self)
  64. {
  65. [self setupTextSystem];
  66. }
  67. return self;
  68. }
  69. // Common initialisation. Must be done once during construction.
  70. - (void)setupTextSystem
  71. {
  72. // Create a text container and set it up to match our label properties
  73. _textContainer = [[NSTextContainer alloc] init];
  74. _textContainer.lineFragmentPadding = 0;
  75. _textContainer.maximumNumberOfLines = self.numberOfLines;
  76. _textContainer.lineBreakMode = self.lineBreakMode;
  77. _textContainer.size = self.frame.size;
  78. // Create a layout manager for rendering
  79. _layoutManager = [[NSLayoutManager alloc] init];
  80. _layoutManager.delegate = self;
  81. [_layoutManager addTextContainer:_textContainer];
  82. // Attach the layou manager to the container and storage
  83. [_textContainer setLayoutManager:_layoutManager];
  84. // Make sure user interaction is enabled so we can accept touches
  85. self.userInteractionEnabled = YES;
  86. // Don't go via public setter as this will have undesired side effect
  87. _automaticLinkDetectionEnabled = YES;
  88. // All links are detectable by default
  89. _linkDetectionTypes = KILinkTypeOptionAll;
  90. // Link Type Attributes. Default is empty (no attributes).
  91. _linkTypeAttributes = [NSMutableDictionary dictionary];
  92. // Don't underline URL links by default.
  93. _systemURLStyle = NO;
  94. // By default we hilight the selected link during a touch to give feedback that we are
  95. // responding to touch.
  96. _selectedLinkBackgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
  97. // Establish the text store with our current text
  98. [self updateTextStoreWithText];
  99. }
  100. #pragma mark - Text and Style management
  101. - (void)setAutomaticLinkDetectionEnabled:(BOOL)decorating
  102. {
  103. _automaticLinkDetectionEnabled = decorating;
  104. // Make sure the text is updated properly
  105. [self updateTextStoreWithText];
  106. }
  107. - (void)setLinkDetectionTypes:(KILinkTypeOption)linkDetectionTypes
  108. {
  109. _linkDetectionTypes = linkDetectionTypes;
  110. // Make sure the text is updated properly
  111. [self updateTextStoreWithText];
  112. }
  113. - (NSDictionary *)linkAtPoint:(CGPoint)location
  114. {
  115. // Do nothing if we have no text
  116. if (_textStorage.string.length == 0)
  117. {
  118. return nil;
  119. }
  120. // Work out the offset of the text in the view
  121. CGPoint textOffset = [self calcGlyphsPositionInView];
  122. // Get the touch location and use text offset to convert to text cotainer coords
  123. location.x -= textOffset.x;
  124. location.y -= textOffset.y;
  125. NSUInteger touchedChar = [_layoutManager glyphIndexForPoint:location inTextContainer:_textContainer];
  126. // If the touch is in white space after the last glyph on the line we don't
  127. // count it as a hit on the text
  128. NSRange lineRange;
  129. CGRect lineRect = [_layoutManager lineFragmentUsedRectForGlyphAtIndex:touchedChar effectiveRange:&lineRange];
  130. if (CGRectContainsPoint(lineRect, location) == NO)
  131. return nil;
  132. // Find the word that was touched and call the detection block
  133. for (NSDictionary *dictionary in self.linkRanges)
  134. {
  135. NSRange range = [[dictionary objectForKey:KILabelRangeKey] rangeValue];
  136. if ((touchedChar >= range.location) && touchedChar < (range.location + range.length))
  137. {
  138. return dictionary;
  139. }
  140. }
  141. return nil;
  142. }
  143. // Applies background color to selected range. Used to hilight touched links
  144. - (void)setSelectedRange:(NSRange)range
  145. {
  146. // Remove the current selection if the selection is changing
  147. if (self.selectedRange.length && !NSEqualRanges(self.selectedRange, range))
  148. {
  149. [_textStorage removeAttribute:NSBackgroundColorAttributeName range:self.selectedRange];
  150. }
  151. // Apply the new selection to the text
  152. if (range.length && _selectedLinkBackgroundColor != nil)
  153. {
  154. [_textStorage addAttribute:NSBackgroundColorAttributeName value:_selectedLinkBackgroundColor range:range];
  155. }
  156. // Save the new range
  157. _selectedRange = range;
  158. [self setNeedsDisplay];
  159. }
  160. - (void)setNumberOfLines:(NSInteger)numberOfLines
  161. {
  162. [super setNumberOfLines:numberOfLines];
  163. _textContainer.maximumNumberOfLines = numberOfLines;
  164. }
  165. - (void)setText:(NSString *)text
  166. {
  167. // Pass the text to the super class first
  168. [super setText:text];
  169. // Update our text store with an attributed string based on the original
  170. // label text properties.
  171. if (!text)
  172. {
  173. text = @"";
  174. }
  175. NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:[self attributesFromProperties]];
  176. [self updateTextStoreWithAttributedString:attributedText];
  177. }
  178. - (void)setAttributedText:(NSAttributedString *)attributedText
  179. {
  180. // Pass the text to the super class first
  181. [super setAttributedText:attributedText];
  182. [self updateTextStoreWithAttributedString:attributedText];
  183. }
  184. - (void)setSystemURLStyle:(BOOL)systemURLStyle
  185. {
  186. _systemURLStyle = systemURLStyle;
  187. // Force refresh
  188. self.text = self.text;
  189. }
  190. - (NSDictionary*)attributesForLinkType:(KILinkType)linkType
  191. {
  192. NSDictionary *attributes = _linkTypeAttributes[@(linkType)];
  193. if (!attributes)
  194. {
  195. attributes = @{NSForegroundColorAttributeName : self.tintColor};
  196. }
  197. return attributes;
  198. }
  199. - (void)setAttributes:(NSDictionary*)attributes forLinkType:(KILinkType)linkType
  200. {
  201. if (attributes)
  202. {
  203. _linkTypeAttributes[@(linkType)] = attributes;
  204. }
  205. else
  206. {
  207. [_linkTypeAttributes removeObjectForKey:@(linkType)];
  208. }
  209. // Force refresh text
  210. self.text = self.text;
  211. }
  212. #pragma mark - Text Storage Management
  213. - (void)updateTextStoreWithText
  214. {
  215. // Now update our storage from either the attributedString or the plain text
  216. if (self.attributedText)
  217. {
  218. [self updateTextStoreWithAttributedString:self.attributedText];
  219. }
  220. else if (self.text)
  221. {
  222. [self updateTextStoreWithAttributedString:[[NSAttributedString alloc] initWithString:self.text attributes:[self attributesFromProperties]]];
  223. }
  224. else
  225. {
  226. [self updateTextStoreWithAttributedString:[[NSAttributedString alloc] initWithString:@"" attributes:[self attributesFromProperties]]];
  227. }
  228. [self setNeedsDisplay];
  229. }
  230. - (void)updateTextStoreWithAttributedString:(NSAttributedString *)attributedString
  231. {
  232. if (attributedString.length != 0)
  233. {
  234. attributedString = [KILabel sanitizeAttributedString:attributedString];
  235. }
  236. if (self.isAutomaticLinkDetectionEnabled && (attributedString.length != 0))
  237. {
  238. self.linkRanges = [self getRangesForLinks:attributedString];
  239. attributedString = [self addLinkAttributesToAttributedString:attributedString linkRanges:self.linkRanges];
  240. }
  241. else
  242. {
  243. self.linkRanges = nil;
  244. }
  245. if (_textStorage)
  246. {
  247. // Set the string on the storage
  248. [_textStorage setAttributedString:attributedString];
  249. }
  250. else
  251. {
  252. // Create a new text storage and attach it correctly to the layout manager
  253. _textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
  254. [_textStorage addLayoutManager:_layoutManager];
  255. [_layoutManager setTextStorage:_textStorage];
  256. }
  257. }
  258. // Returns attributed string attributes based on the text properties set on the label.
  259. // These are styles that are only applied when NOT using the attributedText directly.
  260. - (NSDictionary *)attributesFromProperties
  261. {
  262. // Setup shadow attributes
  263. NSShadow *shadow = shadow = [[NSShadow alloc] init];
  264. if (self.shadowColor)
  265. {
  266. shadow.shadowColor = self.shadowColor;
  267. shadow.shadowOffset = self.shadowOffset;
  268. }
  269. else
  270. {
  271. shadow.shadowOffset = CGSizeMake(0, -1);
  272. shadow.shadowColor = nil;
  273. }
  274. // Setup color attributes
  275. UIColor *color = self.textColor;
  276. if (!self.isEnabled)
  277. {
  278. color = [UIColor lightGrayColor];
  279. }
  280. else if (self.isHighlighted)
  281. {
  282. color = self.highlightedTextColor;
  283. }
  284. // Setup paragraph attributes
  285. NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
  286. paragraph.alignment = self.textAlignment;
  287. // Create the dictionary
  288. NSDictionary *attributes = @{NSFontAttributeName : self.font,
  289. NSForegroundColorAttributeName : color,
  290. NSShadowAttributeName : shadow,
  291. NSParagraphStyleAttributeName : paragraph,
  292. };
  293. return attributes;
  294. }
  295. /**
  296. * Returns array of ranges for all special words, user handles, hashtags and urls in the specfied
  297. * text.
  298. *
  299. * @param text Text to parse for links
  300. *
  301. * @return Array of dictionaries describing the links.
  302. */
  303. - (NSArray *)getRangesForLinks:(NSAttributedString *)text
  304. {
  305. NSMutableArray *rangesForLinks = [[NSMutableArray alloc] init];
  306. if (self.linkDetectionTypes & KILinkTypeOptionUserHandle)
  307. {
  308. [rangesForLinks addObjectsFromArray:[self getRangesForUserHandles:text.string]];
  309. }
  310. if (self.linkDetectionTypes & KILinkTypeOptionHashtag)
  311. {
  312. [rangesForLinks addObjectsFromArray:[self getRangesForHashtags:text.string]];
  313. }
  314. if (self.linkDetectionTypes & KILinkTypeOptionURL)
  315. {
  316. [rangesForLinks addObjectsFromArray:[self getRangesForURLs:self.attributedText]];
  317. }
  318. return rangesForLinks;
  319. }
  320. - (NSArray *)getRangesForUserHandles:(NSString *)text
  321. {
  322. NSMutableArray *rangesForUserHandles = [[NSMutableArray alloc] init];
  323. // Setup a regular expression for user handles and hashtags
  324. static NSRegularExpression *regex = nil;
  325. static dispatch_once_t onceToken;
  326. dispatch_once(&onceToken, ^{
  327. NSError *error = nil;
  328. regex = [[NSRegularExpression alloc] initWithPattern:@"(?<!\\w)@([\\w\\_]+)?" options:0 error:&error];
  329. });
  330. // Run the expression and get matches
  331. NSArray *matches = [regex matchesInString:text options:0 range:NSMakeRange(0, text.length)];
  332. // Add all our ranges to the result
  333. for (NSTextCheckingResult *match in matches)
  334. {
  335. NSRange matchRange = [match range];
  336. NSString *matchString = [text substringWithRange:matchRange];
  337. if (![self ignoreMatch:matchString])
  338. {
  339. [rangesForUserHandles addObject:@{KILabelLinkTypeKey : @(KILinkTypeUserHandle),
  340. KILabelRangeKey : [NSValue valueWithRange:matchRange],
  341. KILabelLinkKey : matchString
  342. }];
  343. }
  344. }
  345. return rangesForUserHandles;
  346. }
  347. - (NSArray *)getRangesForHashtags:(NSString *)text
  348. {
  349. NSMutableArray *rangesForHashtags = [[NSMutableArray alloc] init];
  350. // Setup a regular expression for user handles and hashtags
  351. static NSRegularExpression *regex = nil;
  352. static dispatch_once_t onceToken;
  353. dispatch_once(&onceToken, ^{
  354. NSError *error = nil;
  355. regex = [[NSRegularExpression alloc] initWithPattern:@"(?<!\\w)#([\\w\\_]+)?" options:0 error:&error];
  356. });
  357. // Run the expression and get matches
  358. NSArray *matches = [regex matchesInString:text options:0 range:NSMakeRange(0, text.length)];
  359. // Add all our ranges to the result
  360. for (NSTextCheckingResult *match in matches)
  361. {
  362. NSRange matchRange = [match range];
  363. NSString *matchString = [text substringWithRange:matchRange];
  364. if (![self ignoreMatch:matchString])
  365. {
  366. [rangesForHashtags addObject:@{KILabelLinkTypeKey : @(KILinkTypeHashtag),
  367. KILabelRangeKey : [NSValue valueWithRange:matchRange],
  368. KILabelLinkKey : matchString,
  369. }];
  370. }
  371. }
  372. return rangesForHashtags;
  373. }
  374. - (NSArray *)getRangesForURLs:(NSAttributedString *)text
  375. {
  376. NSArray *matches = [self.urlRegex matchesInString:text.string options:0 range:NSMakeRange(0, [text.string length])];
  377. NSMutableArray *rangesForURLs = [[NSMutableArray alloc] init];;
  378. // Use a data detector to find urls in the text
  379. NSString *plainText = text.string;
  380. // Add a range entry for every url we found
  381. for (NSTextCheckingResult *match in matches)
  382. {
  383. NSRange matchRange = [match range];
  384. // If there's a link embedded in the attributes, use that instead of the raw text
  385. NSString *realURL = [plainText substringWithRange:matchRange];
  386. if (![self ignoreMatch:realURL])
  387. {
  388. [rangesForURLs addObject:@{KILabelLinkTypeKey : @(KILinkTypeURL),
  389. KILabelRangeKey : [NSValue valueWithRange:matchRange],
  390. KILabelLinkKey : realURL,
  391. }];
  392. }
  393. }
  394. return rangesForURLs;
  395. }
  396. - (BOOL)ignoreMatch:(NSString*)string
  397. {
  398. return [_ignoredKeywords containsObject:[string lowercaseString]];
  399. }
  400. - (NSAttributedString *)addLinkAttributesToAttributedString:(NSAttributedString *)string linkRanges:(NSArray *)linkRanges
  401. {
  402. NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:string];
  403. for (NSDictionary *dictionary in linkRanges)
  404. {
  405. NSRange range = [[dictionary objectForKey:KILabelRangeKey] rangeValue];
  406. KILinkType linkType = [dictionary[KILabelLinkTypeKey] unsignedIntegerValue];
  407. NSDictionary *attributes = [self attributesForLinkType:linkType];
  408. // Use our tint color to hilight the link
  409. [attributedString addAttributes:attributes range:range];
  410. // Add an URL attribute if this is a URL
  411. if (_systemURLStyle && ((KILinkType)[dictionary[KILabelLinkTypeKey] unsignedIntegerValue] == KILinkTypeURL))
  412. {
  413. // Add a link attribute using the stored link
  414. [attributedString addAttribute:NSLinkAttributeName value:dictionary[KILabelLinkKey] range:range];
  415. }
  416. }
  417. return attributedString;
  418. }
  419. #pragma mark - Layout and Rendering
  420. - (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines
  421. {
  422. // Use our text container to calculate the bounds required. First save our
  423. // current text container setup
  424. CGSize savedTextContainerSize = _textContainer.size;
  425. NSInteger savedTextContainerNumberOfLines = _textContainer.maximumNumberOfLines;
  426. // Apply the new potential bounds and number of lines
  427. _textContainer.size = bounds.size;
  428. _textContainer.maximumNumberOfLines = numberOfLines;
  429. // Measure the text with the new state
  430. CGRect textBounds = [_layoutManager usedRectForTextContainer:_textContainer];
  431. // Position the bounds and round up the size for good measure
  432. textBounds.origin = bounds.origin;
  433. textBounds.size.width = ceil(textBounds.size.width);
  434. textBounds.size.height = ceil(textBounds.size.height);
  435. if (textBounds.size.height < bounds.size.height)
  436. {
  437. // Take verical alignment into account
  438. CGFloat offsetY = (bounds.size.height - textBounds.size.height) / 2.0;
  439. textBounds.origin.y += offsetY;
  440. }
  441. // Restore the old container state before we exit under any circumstances
  442. _textContainer.size = savedTextContainerSize;
  443. _textContainer.maximumNumberOfLines = savedTextContainerNumberOfLines;
  444. return textBounds;
  445. }
  446. - (void)drawTextInRect:(CGRect)rect
  447. {
  448. // Don't call super implementation. Might want to uncomment this out when
  449. // debugging layout and rendering problems.
  450. // [super drawTextInRect:rect];
  451. // Calculate the offset of the text in the view
  452. NSRange glyphRange = [_layoutManager glyphRangeForTextContainer:_textContainer];
  453. CGPoint glyphsPosition = [self calcGlyphsPositionInView];
  454. // Drawing code
  455. [_layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:glyphsPosition];
  456. [_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:glyphsPosition];
  457. }
  458. // Returns the XY offset of the range of glyphs from the view's origin
  459. - (CGPoint)calcGlyphsPositionInView
  460. {
  461. CGPoint textOffset = CGPointZero;
  462. CGRect textBounds = [_layoutManager usedRectForTextContainer:_textContainer];
  463. textBounds.size.width = ceil(textBounds.size.width);
  464. textBounds.size.height = ceil(textBounds.size.height);
  465. if (textBounds.size.height < self.bounds.size.height)
  466. {
  467. CGFloat paddingHeight = (self.bounds.size.height - textBounds.size.height) / 2.0;
  468. textOffset.y = paddingHeight;
  469. }
  470. return textOffset;
  471. }
  472. - (void)setFrame:(CGRect)frame
  473. {
  474. [super setFrame:frame];
  475. _textContainer.size = self.bounds.size;
  476. }
  477. - (void)setBounds:(CGRect)bounds
  478. {
  479. [super setBounds:bounds];
  480. _textContainer.size = self.bounds.size;
  481. }
  482. - (void)layoutSubviews
  483. {
  484. [super layoutSubviews];
  485. // Update our container size when the view frame changes
  486. _textContainer.size = self.bounds.size;
  487. }
  488. - (void)setIgnoredKeywords:(NSSet *)ignoredKeywords
  489. {
  490. NSMutableSet *set = [NSMutableSet setWithCapacity:ignoredKeywords.count];
  491. [ignoredKeywords enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
  492. [set addObject:[obj lowercaseString]];
  493. }];
  494. _ignoredKeywords = [set copy];
  495. }
  496. #pragma mark - Interactions
  497. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  498. {
  499. _isTouchMoved = NO;
  500. // Get the info for the touched link if there is one
  501. NSDictionary *touchedLink;
  502. CGPoint touchLocation = [[touches anyObject] locationInView:self];
  503. touchedLink = [self linkAtPoint:touchLocation];
  504. if (touchedLink)
  505. {
  506. self.selectedRange = [[touchedLink objectForKey:KILabelRangeKey] rangeValue];
  507. }
  508. else
  509. {
  510. [super touchesBegan:touches withEvent:event];
  511. }
  512. }
  513. - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
  514. {
  515. [super touchesMoved:touches withEvent:event];
  516. _isTouchMoved = YES;
  517. }
  518. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
  519. {
  520. [super touchesEnded:touches withEvent:event];
  521. // If the user dragged their finger we ignore the touch
  522. if (_isTouchMoved)
  523. {
  524. self.selectedRange = NSMakeRange(0, 0);
  525. return;
  526. }
  527. // Get the info for the touched link if there is one
  528. NSDictionary *touchedLink;
  529. CGPoint touchLocation = [[touches anyObject] locationInView:self];
  530. touchedLink = [self linkAtPoint:touchLocation];
  531. if (touchedLink)
  532. {
  533. NSRange range = [[touchedLink objectForKey:KILabelRangeKey] rangeValue];
  534. NSString *touchedSubstring = [touchedLink objectForKey:KILabelLinkKey];
  535. KILinkType linkType = (KILinkType)[[touchedLink objectForKey:KILabelLinkTypeKey] intValue];
  536. [self receivedActionForLinkType:linkType string:touchedSubstring range:range];
  537. }
  538. else
  539. {
  540. [super touchesBegan:touches withEvent:event];
  541. }
  542. self.selectedRange = NSMakeRange(0, 0);
  543. }
  544. - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
  545. {
  546. [super touchesCancelled:touches withEvent:event];
  547. // Make sure we don't leave a selection when the touch is cancelled
  548. self.selectedRange = NSMakeRange(0, 0);
  549. }
  550. - (void)receivedActionForLinkType:(KILinkType)linkType string:(NSString*)string range:(NSRange)range
  551. {
  552. switch (linkType)
  553. {
  554. case KILinkTypeUserHandle:
  555. if (_userHandleLinkTapHandler)
  556. {
  557. _userHandleLinkTapHandler(self, string, range);
  558. }
  559. break;
  560. case KILinkTypeHashtag:
  561. if (_hashtagLinkTapHandler)
  562. {
  563. _hashtagLinkTapHandler(self, string, range);
  564. }
  565. break;
  566. case KILinkTypeURL:
  567. if (_urlLinkTapHandler)
  568. {
  569. _urlLinkTapHandler(self, string, range);
  570. }
  571. break;
  572. }
  573. }
  574. #pragma mark - setter & getter
  575. - (NSRegularExpression *)urlRegex {
  576. if (_urlRegex == nil) {
  577. NSString *regulaStr =@"((http[s]{0,1}|ftp)://[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)|(www.[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)";
  578. _urlRegex = [NSRegularExpression regularExpressionWithPattern:regulaStr options:NSRegularExpressionCaseInsensitive error:nil];
  579. }
  580. return _urlRegex;
  581. }
  582. #pragma mark - Layout manager delegate
  583. -(BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex
  584. {
  585. // Don't allow line breaks inside URLs
  586. NSRange range;
  587. NSURL *linkURL = [layoutManager.textStorage attribute:NSLinkAttributeName atIndex:charIndex effectiveRange:&range];
  588. return !(linkURL && (charIndex > range.location) && (charIndex <= NSMaxRange(range)));
  589. }
  590. + (NSAttributedString *)sanitizeAttributedString:(NSAttributedString *)attributedString
  591. {
  592. // Setup paragraph alignement properly. IB applies the line break style
  593. // to the attributed string. The problem is that the text container then
  594. // breaks at the first line of text. If we set the line break to wrapping
  595. // then the text container defines the break mode and it works.
  596. // NOTE: This is either an Apple bug or something I've misunderstood.
  597. // Get the current paragraph style. IB only allows a single paragraph so
  598. // getting the style of the first char is fine.
  599. NSRange range;
  600. NSParagraphStyle *paragraphStyle = [attributedString attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:&range];
  601. if (paragraphStyle == nil)
  602. {
  603. return attributedString;
  604. }
  605. // Remove the line breaks
  606. NSMutableParagraphStyle *mutableParagraphStyle = [paragraphStyle mutableCopy];
  607. mutableParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
  608. // Apply new style
  609. NSMutableAttributedString *restyled = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];
  610. [restyled addAttribute:NSParagraphStyleAttributeName value:mutableParagraphStyle range:NSMakeRange(0, restyled.length)];
  611. return restyled;
  612. }
  613. @end