├── .gitignore ├── .swift-version ├── Classes ├── ExpandableLabel.swift └── Info.plist ├── ExpandableLabel.podspec ├── ExpandableLabel.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── ExpandableLabel.xcscheme ├── ExpandableLabelDemo ├── ExpandableLabelDemo.xcodeproj │ └── project.pbxproj └── ExpandableLabelDemo │ ├── AppDelegate.swift │ ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard │ ├── ExpandableCell.swift │ ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Info.plist │ └── ViewController.swift ├── LICENSE ├── README.md ├── Resources ├── ExpandableLabel.gif └── MoreLessExpand.gif └── fastlane └── Fastfile /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | build/* 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | *.xcworkspace 12 | !default.xcworkspace 13 | xcuserdata 14 | profile 15 | *.moved-aside 16 | Pods/ 17 | Podfile.lock 18 | fastlane/report.xml 19 | 20 | .DS_Store 21 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.2 2 | -------------------------------------------------------------------------------- /Classes/ExpandableLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpandableLabel.swift 3 | // 4 | // Copyright (c) 2015 apploft. GmbH 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | typealias LineIndexTuple = (line: CTLine, index: Int) 25 | 26 | import UIKit 27 | 28 | /** 29 | * The delegate of ExpandableLabel. 30 | */ 31 | @objc public protocol ExpandableLabelDelegate: NSObjectProtocol { 32 | @objc func willExpandLabel(_ label: ExpandableLabel) 33 | @objc func didExpandLabel(_ label: ExpandableLabel) 34 | @objc func willCollapseLabel(_ label: ExpandableLabel) 35 | @objc func didCollapseLabel(_ label: ExpandableLabel) 36 | } 37 | 38 | /** 39 | * ExpandableLabel 40 | */ 41 | @objc open class ExpandableLabel: UILabel { 42 | public enum TextReplacementType { 43 | case character 44 | case word 45 | } 46 | 47 | /// The delegate of ExpandableLabel 48 | @objc weak open var delegate: ExpandableLabelDelegate? 49 | 50 | /// Set 'true' if the label should be collapsed or 'false' for expanded. 51 | @IBInspectable open var collapsed: Bool = true { 52 | didSet { 53 | super.attributedText = (collapsed) ? self.collapsedText : self.expandedText 54 | super.numberOfLines = (collapsed) ? self.collapsedNumberOfLines : 0 55 | if let animationView = animationView { 56 | UIView.animate(withDuration: 0.5) { 57 | animationView.layoutIfNeeded() 58 | } 59 | } 60 | } 61 | } 62 | 63 | /// Set 'true' if the label can be expanded or 'false' if not. 64 | /// The default value is 'true'. 65 | @IBInspectable open var shouldExpand: Bool = true 66 | 67 | /// Set 'true' if the label can be collapsed or 'false' if not. 68 | /// The default value is 'false'. 69 | @IBInspectable open var shouldCollapse: Bool = false 70 | 71 | /// Set the link name (and attributes) that is shown when collapsed. 72 | /// The default value is "More". Cannot be nil. 73 | @objc open var collapsedAttributedLink: NSAttributedString! { 74 | didSet { 75 | self.collapsedAttributedLink = collapsedAttributedLink.copyWithAddedFontAttribute(font) 76 | } 77 | } 78 | 79 | /// Set the link name (and attributes) that is shown when expanded. 80 | /// The default value is "Less". Can be nil. 81 | @objc open var expandedAttributedLink: NSAttributedString? 82 | 83 | /// Set the ellipsis that appears just after the text and before the link. 84 | /// The default value is "...". Can be nil. 85 | @objc open var ellipsis: NSAttributedString? { 86 | didSet { 87 | self.ellipsis = ellipsis?.copyWithAddedFontAttribute(font) 88 | } 89 | } 90 | 91 | /// Set a view to animate changes of the label collapsed state with. If this value is nil, no animation occurs. 92 | /// Usually you assign the superview of this label or a UIScrollView in which this label sits. 93 | /// Also don't forget to set the contentMode of this label to top to smoothly reveal the hidden lines. 94 | /// The default value is 'nil'. 95 | @objc open var animationView: UIView? 96 | 97 | open var textReplacementType: TextReplacementType = .word 98 | 99 | private var collapsedText: NSAttributedString? 100 | private var linkHighlighted: Bool = false 101 | private let touchSize = CGSize(width: 44, height: 44) 102 | private var linkRect: CGRect? 103 | private var collapsedNumberOfLines: NSInteger = 0 104 | private var expandedLinkPosition: NSTextAlignment? 105 | private var collapsedLinkTextRange: NSRange? 106 | private var expandedLinkTextRange: NSRange? 107 | 108 | open override var numberOfLines: NSInteger { 109 | didSet { 110 | collapsedNumberOfLines = numberOfLines 111 | } 112 | } 113 | 114 | @objc public required init?(coder aDecoder: NSCoder) { 115 | super.init(coder: aDecoder) 116 | commonInit() 117 | } 118 | 119 | @objc public override init(frame: CGRect) { 120 | super.init(frame: frame) 121 | self.commonInit() 122 | } 123 | 124 | @objc public init() { 125 | super.init(frame: .zero) 126 | } 127 | 128 | open override var text: String? { 129 | set(text) { 130 | if let text = text { 131 | self.attributedText = NSAttributedString(string: text) 132 | } else { 133 | self.attributedText = nil 134 | } 135 | } 136 | get { 137 | return self.attributedText?.string 138 | } 139 | } 140 | 141 | open private(set) var expandedText: NSAttributedString? 142 | open override var attributedText: NSAttributedString? { 143 | set(attributedText) { 144 | if let attributedText = attributedText?.copyWithAddedFontAttribute(font).copyWithParagraphAttribute(font), 145 | attributedText.length > 0 { 146 | self.collapsedText = getCollapsedText(for: attributedText, link: (linkHighlighted) ? collapsedAttributedLink.copyWithHighlightedColor() : self.collapsedAttributedLink) 147 | self.expandedText = getExpandedText(for: attributedText, link: (linkHighlighted) ? expandedAttributedLink?.copyWithHighlightedColor() : self.expandedAttributedLink) 148 | super.attributedText = (self.collapsed) ? self.collapsedText : self.expandedText 149 | } else { 150 | self.expandedText = nil 151 | self.collapsedText = nil 152 | super.attributedText = nil 153 | } 154 | } 155 | get { 156 | return super.attributedText 157 | } 158 | } 159 | 160 | open func setLessLinkWith(lessLink: String, attributes: [NSAttributedString.Key: AnyObject], position: NSTextAlignment?) { 161 | var alignedattributes = attributes 162 | if let pos = position { 163 | expandedLinkPosition = pos 164 | let titleParagraphStyle = NSMutableParagraphStyle() 165 | titleParagraphStyle.alignment = pos 166 | alignedattributes[.paragraphStyle] = titleParagraphStyle 167 | } 168 | expandedAttributedLink = NSMutableAttributedString(string: lessLink, 169 | attributes: alignedattributes) 170 | } 171 | } 172 | 173 | // MARK: - Touch Handling 174 | 175 | extension ExpandableLabel { 176 | 177 | open override func touchesBegan(_ touches: Set, with event: UIEvent?) { 178 | setLinkHighlighted(touches, event: event, highlighted: true) 179 | } 180 | 181 | open override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 182 | setLinkHighlighted(touches, event: event, highlighted: false) 183 | } 184 | 185 | open override func touchesEnded(_ touches: Set, with event: UIEvent?) { 186 | guard let touch = touches.first else { 187 | return 188 | } 189 | 190 | if !collapsed { 191 | guard let range = self.expandedLinkTextRange else { 192 | return 193 | } 194 | 195 | if shouldCollapse && check(touch: touch, isInRange: range) { 196 | delegate?.willCollapseLabel(self) 197 | collapsed = true 198 | delegate?.didCollapseLabel(self) 199 | linkHighlighted = isHighlighted 200 | setNeedsDisplay() 201 | } 202 | } else { 203 | if shouldExpand && setLinkHighlighted(touches, event: event, highlighted: false) { 204 | delegate?.willExpandLabel(self) 205 | collapsed = false 206 | delegate?.didExpandLabel(self) 207 | } 208 | } 209 | } 210 | 211 | open override func touchesMoved(_ touches: Set, with event: UIEvent?) { 212 | setLinkHighlighted(touches, event: event, highlighted: false) 213 | } 214 | } 215 | 216 | // MARK: Privates 217 | 218 | extension ExpandableLabel { 219 | private func commonInit() { 220 | isUserInteractionEnabled = true 221 | lineBreakMode = .byClipping 222 | collapsedNumberOfLines = numberOfLines 223 | expandedAttributedLink = nil 224 | collapsedAttributedLink = NSAttributedString(string: "More", attributes: [.font: UIFont.boldSystemFont(ofSize: font.pointSize)]) 225 | ellipsis = NSAttributedString(string: "...") 226 | } 227 | 228 | private func textReplaceWordWithLink(_ lineIndex: LineIndexTuple, text: NSAttributedString, linkName: NSAttributedString) -> NSAttributedString { 229 | let lineText = text.text(for: lineIndex.line) 230 | var lineTextWithLink = lineText 231 | (lineText.string as NSString).enumerateSubstrings(in: NSRange(location: 0, length: lineText.length), options: [.byWords, .reverse]) { (word, subRange, enclosingRange, stop) -> Void in 232 | let lineTextWithLastWordRemoved = lineText.attributedSubstring(from: NSRange(location: 0, length: subRange.location)) 233 | let lineTextWithAddedLink = NSMutableAttributedString(attributedString: lineTextWithLastWordRemoved) 234 | if let ellipsis = self.ellipsis { 235 | lineTextWithAddedLink.append(ellipsis) 236 | lineTextWithAddedLink.append(NSAttributedString(string: " ", attributes: [.font: self.font])) 237 | } 238 | lineTextWithAddedLink.append(linkName) 239 | let fits = self.textFitsWidth(lineTextWithAddedLink) 240 | if fits { 241 | lineTextWithLink = lineTextWithAddedLink 242 | let lineTextWithLastWordRemovedRect = lineTextWithLastWordRemoved.boundingRect(for: self.frame.size.width) 243 | let wordRect = linkName.boundingRect(for: self.frame.size.width) 244 | let width = lineTextWithLastWordRemoved.string == "" ? self.frame.width : wordRect.size.width 245 | self.linkRect = CGRect(x: lineTextWithLastWordRemovedRect.size.width, y: self.font.lineHeight * CGFloat(lineIndex.index), width: width, height: wordRect.size.height) 246 | stop.pointee = true 247 | } 248 | } 249 | return lineTextWithLink 250 | } 251 | 252 | private func textReplaceWithLink(_ lineIndex: LineIndexTuple, text: NSAttributedString, linkName: NSAttributedString) -> NSAttributedString { 253 | let lineText = text.text(for: lineIndex.line) 254 | let lineTextTrimmedNewLines = NSMutableAttributedString() 255 | lineTextTrimmedNewLines.append(lineText) 256 | let nsString = lineTextTrimmedNewLines.string as NSString 257 | let range = nsString.rangeOfCharacter(from: CharacterSet.newlines) 258 | if range.length > 0 { 259 | lineTextTrimmedNewLines.replaceCharacters(in: range, with: "") 260 | } 261 | let linkText = NSMutableAttributedString() 262 | if let ellipsis = self.ellipsis { 263 | linkText.append(ellipsis) 264 | linkText.append(NSAttributedString(string: " ", attributes: [.font: self.font])) 265 | } 266 | linkText.append(linkName) 267 | 268 | let lengthDifference = lineTextTrimmedNewLines.string.composedCount - linkText.string.composedCount 269 | let truncatedString = lineTextTrimmedNewLines.attributedSubstring( 270 | from: NSMakeRange(0, lengthDifference >= 0 ? lengthDifference : lineTextTrimmedNewLines.string.composedCount)) 271 | let lineTextWithLink = NSMutableAttributedString(attributedString: truncatedString) 272 | lineTextWithLink.append(linkText) 273 | return lineTextWithLink 274 | } 275 | 276 | private func getExpandedText(for text: NSAttributedString?, link: NSAttributedString?) -> NSAttributedString? { 277 | guard let text = text else { return nil } 278 | let expandedText = NSMutableAttributedString() 279 | expandedText.append(text) 280 | if let link = link, textWillBeTruncated(expandedText) { 281 | let spaceOrNewLine = expandedLinkPosition == nil ? " " : "\n" 282 | expandedText.append(NSAttributedString(string: "\(spaceOrNewLine)")) 283 | expandedText.append(NSMutableAttributedString(string: "\(link.string)", attributes: link.attributes(at: 0, effectiveRange: nil)).copyWithAddedFontAttribute(font)) 284 | expandedLinkTextRange = NSMakeRange(expandedText.length - link.length, link.length) 285 | } 286 | 287 | return expandedText 288 | } 289 | 290 | private func getCollapsedText(for text: NSAttributedString?, link: NSAttributedString) -> NSAttributedString? { 291 | guard let text = text else { return nil } 292 | let lines = text.lines(for: frame.size.width) 293 | if collapsedNumberOfLines > 0 && collapsedNumberOfLines < lines.count { 294 | let lastLineRef = lines[collapsedNumberOfLines-1] as CTLine 295 | var lineIndex: LineIndexTuple? 296 | var modifiedLastLineText: NSAttributedString? 297 | 298 | if self.textReplacementType == .word { 299 | lineIndex = findLineWithWords(lastLine: lastLineRef, text: text, lines: lines) 300 | if let lineIndex = lineIndex { 301 | modifiedLastLineText = textReplaceWordWithLink(lineIndex, text: text, linkName: link) 302 | } 303 | } else { 304 | lineIndex = (lastLineRef, collapsedNumberOfLines - 1) 305 | if let lineIndex = lineIndex { 306 | modifiedLastLineText = textReplaceWithLink(lineIndex, text: text, linkName: link) 307 | } 308 | } 309 | 310 | if let lineIndex = lineIndex, let modifiedLastLineText = modifiedLastLineText { 311 | let collapsedLines = NSMutableAttributedString() 312 | for index in 0.. LineIndexTuple { 327 | var lastLineRef = lastLine 328 | var lastLineIndex = collapsedNumberOfLines - 1 329 | var lineWords = spiltIntoWords(str: text.text(for: lastLineRef).string as NSString) 330 | while lineWords.count < 2 && lastLineIndex > 0 { 331 | lastLineIndex -= 1 332 | lastLineRef = lines[lastLineIndex] as CTLine 333 | lineWords = spiltIntoWords(str: text.text(for: lastLineRef).string as NSString) 334 | } 335 | return (lastLineRef, lastLineIndex) 336 | } 337 | 338 | private func spiltIntoWords(str: NSString) -> [String] { 339 | var strings: [String] = [] 340 | str.enumerateSubstrings(in: NSRange(location: 0, length: str.length), options: [.byWords, .reverse]) { (word, subRange, enclosingRange, stop) -> Void in 341 | if let unwrappedWord = word { 342 | strings.append(unwrappedWord) 343 | } 344 | if strings.count > 1 { stop.pointee = true } 345 | } 346 | return strings 347 | } 348 | 349 | private func textFitsWidth(_ text: NSAttributedString) -> Bool { 350 | return (text.boundingRect(for: frame.size.width).size.height <= font.lineHeight) as Bool 351 | } 352 | 353 | private func textWillBeTruncated(_ text: NSAttributedString) -> Bool { 354 | let lines = text.lines(for: frame.size.width) 355 | return collapsedNumberOfLines > 0 && collapsedNumberOfLines < lines.count 356 | } 357 | 358 | private func textClicked(touches: Set?, event: UIEvent?) -> Bool { 359 | let touch = event?.allTouches?.first 360 | let location = touch?.location(in: self) 361 | let textRect = self.attributedText?.boundingRect(for: self.frame.width) 362 | if let location = location, let textRect = textRect { 363 | let finger = CGRect(x: location.x-touchSize.width/2, y: location.y-touchSize.height/2, width: touchSize.width, height: touchSize.height) 364 | if finger.intersects(textRect) { 365 | return true 366 | } 367 | } 368 | return false 369 | } 370 | 371 | @discardableResult private func setLinkHighlighted(_ touches: Set?, event: UIEvent?, highlighted: Bool) -> Bool { 372 | guard let touch = touches?.first else { 373 | return false 374 | } 375 | 376 | guard let range = self.collapsedLinkTextRange else { 377 | return false 378 | } 379 | 380 | if collapsed && check(touch: touch, isInRange: range) { 381 | linkHighlighted = highlighted 382 | setNeedsDisplay() 383 | return true 384 | } 385 | return false 386 | } 387 | } 388 | 389 | // MARK: Convenience Methods 390 | 391 | private extension NSAttributedString { 392 | func hasFontAttribute() -> Bool { 393 | guard !self.string.isEmpty else { return false } 394 | let font = self.attribute(.font, at: 0, effectiveRange: nil) as? UIFont 395 | return font != nil 396 | } 397 | 398 | func copyWithParagraphAttribute(_ font: UIFont) -> NSAttributedString { 399 | let paragraphStyle = NSMutableParagraphStyle() 400 | paragraphStyle.lineHeightMultiple = 1.05 401 | paragraphStyle.alignment = .left 402 | paragraphStyle.lineSpacing = 0.0 403 | paragraphStyle.minimumLineHeight = font.lineHeight 404 | paragraphStyle.maximumLineHeight = font.lineHeight 405 | 406 | let copy = NSMutableAttributedString(attributedString: self) 407 | let range = NSRange(location: 0, length: copy.length) 408 | copy.addAttribute(.paragraphStyle, value: paragraphStyle, range: range) 409 | copy.addAttribute(.baselineOffset, value: font.pointSize * 0.08, range: range) 410 | return copy 411 | } 412 | 413 | func copyWithAddedFontAttribute(_ font: UIFont) -> NSAttributedString { 414 | if !hasFontAttribute() { 415 | let copy = NSMutableAttributedString(attributedString: self) 416 | copy.addAttribute(.font, value: font, range: NSRange(location: 0, length: copy.length)) 417 | return copy 418 | } 419 | return self.copy() as! NSAttributedString 420 | } 421 | 422 | func copyWithHighlightedColor() -> NSAttributedString { 423 | let alphaComponent = CGFloat(0.5) 424 | let baseColor: UIColor = (self.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? UIColor)?.withAlphaComponent(alphaComponent) ?? 425 | UIColor.black.withAlphaComponent(alphaComponent) 426 | let highlightedCopy = NSMutableAttributedString(attributedString: self) 427 | let range = NSRange(location: 0, length: highlightedCopy.length) 428 | highlightedCopy.removeAttribute(.foregroundColor, range: range) 429 | highlightedCopy.addAttribute(.foregroundColor, value: baseColor, range: range) 430 | return highlightedCopy 431 | } 432 | 433 | func lines(for width: CGFloat) -> [CTLine] { 434 | let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: width, height: .greatestFiniteMagnitude)) 435 | let frameSetterRef: CTFramesetter = CTFramesetterCreateWithAttributedString(self as CFAttributedString) 436 | let frameRef: CTFrame = CTFramesetterCreateFrame(frameSetterRef, CFRange(location: 0, length: 0), path.cgPath, nil) 437 | 438 | let linesNS: NSArray = CTFrameGetLines(frameRef) 439 | let linesAO: [AnyObject] = linesNS as [AnyObject] 440 | let lines: [CTLine] = linesAO as! [CTLine] 441 | 442 | return lines 443 | } 444 | 445 | func text(for lineRef: CTLine) -> NSAttributedString { 446 | let lineRangeRef: CFRange = CTLineGetStringRange(lineRef) 447 | let range: NSRange = NSRange(location: lineRangeRef.location, length: lineRangeRef.length) 448 | return self.attributedSubstring(from: range) 449 | } 450 | 451 | func boundingRect(for width: CGFloat) -> CGRect { 452 | return self.boundingRect(with: CGSize(width: width, height: .greatestFiniteMagnitude), 453 | options: .usesLineFragmentOrigin, context: nil) 454 | } 455 | } 456 | 457 | extension String { 458 | var composedCount : Int { 459 | var count = 0 460 | enumerateSubstrings(in: startIndex.. Bool { 467 | let touchPoint = touch.location(in: self) 468 | let index = characterIndex(at: touchPoint) 469 | return NSLocationInRange(index, targetRange) 470 | } 471 | 472 | private func characterIndex(at touchPoint: CGPoint) -> Int { 473 | guard let attributedString = attributedText else { return NSNotFound } 474 | if !bounds.contains(touchPoint) { 475 | return NSNotFound 476 | } 477 | 478 | let textRect = self.textRect(forBounds: bounds, limitedToNumberOfLines: numberOfLines) 479 | if !textRect.contains(touchPoint) { 480 | return NSNotFound 481 | } 482 | 483 | var point = touchPoint 484 | // Offset tap coordinates by textRect origin to make them relative to the origin of frame 485 | point = CGPoint(x: point.x - textRect.origin.x, y: point.y - textRect.origin.y) 486 | // Convert tap coordinates (start at top left) to CT coordinates (start at bottom left) 487 | point = CGPoint(x: point.x, y: textRect.size.height - point.y) 488 | 489 | let framesetter = CTFramesetterCreateWithAttributedString(attributedString) 490 | let suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, attributedString.length), nil, CGSize(width: textRect.width, height: CGFloat.greatestFiniteMagnitude), nil) 491 | 492 | let path = CGMutablePath() 493 | path.addRect(CGRect(x: 0, y: 0, width: suggestedSize.width, height: CGFloat(ceilf(Float(suggestedSize.height))))) 494 | 495 | let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedString.length), path, nil) 496 | let lines = CTFrameGetLines(frame) 497 | let linesCount = numberOfLines > 0 ? min(numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines) 498 | if linesCount == 0 { 499 | return NSNotFound 500 | } 501 | 502 | var lineOrigins = [CGPoint](repeating: .zero, count: linesCount) 503 | CTFrameGetLineOrigins(frame, CFRangeMake(0, linesCount), &lineOrigins) 504 | 505 | for (idx, lineOrigin) in lineOrigins.enumerated() { 506 | var lineOrigin = lineOrigin 507 | let lineIndex = CFIndex(idx) 508 | let line = unsafeBitCast(CFArrayGetValueAtIndex(lines, lineIndex), to: CTLine.self) 509 | 510 | // Get bounding information of line 511 | var ascent: CGFloat = 0.0 512 | var descent: CGFloat = 0.0 513 | var leading: CGFloat = 0.0 514 | let width = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, &leading)) 515 | let yMin = CGFloat(floor(lineOrigin.y - descent)) 516 | let yMax = CGFloat(ceil(lineOrigin.y + ascent)) 517 | 518 | // Apply penOffset using flushFactor for horizontal alignment to set lineOrigin since this is the horizontal offset from drawFramesetter 519 | let flushFactor = flushFactorForTextAlignment(textAlignment: textAlignment) 520 | let penOffset = CGFloat(CTLineGetPenOffsetForFlush(line, flushFactor, Double(textRect.size.width))) 521 | lineOrigin.x = penOffset 522 | 523 | // Check if we've already passed the line 524 | if point.y > yMax { 525 | return NSNotFound 526 | } 527 | // Check if the point is within this line vertically 528 | if point.y >= yMin { 529 | // Check if the point is within this line horizontally 530 | if point.x >= lineOrigin.x && point.x <= lineOrigin.x + width { 531 | // Convert CT coordinates to line-relative coordinates 532 | let relativePoint = CGPoint(x: point.x - lineOrigin.x, y: point.y - lineOrigin.y) 533 | return Int(CTLineGetStringIndexForPosition(line, relativePoint)) 534 | } 535 | } 536 | } 537 | 538 | return NSNotFound 539 | } 540 | 541 | private func flushFactorForTextAlignment(textAlignment: NSTextAlignment) -> CGFloat { 542 | switch textAlignment { 543 | case .center: 544 | return 0.5 545 | case .right: 546 | return 1.0 547 | case .left, .natural, .justified: 548 | return 0.0 549 | } 550 | } 551 | } 552 | 553 | 554 | -------------------------------------------------------------------------------- /Classes/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ExpandableLabel.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "ExpandableLabel" 4 | s.version = "0.5.2" 5 | s.summary = "A simple UILabel subclass that shows a tappable link if the content doesn't fit the specified number of lines" 6 | 7 | s.description = <<-DESC 8 | ExpandableLabel is a simple UILabel subclass that shows 9 | a tappable link if the content doesn't fit the specified 10 | number of lines. If touched, the label will expand to show 11 | the entire content. 12 | DESC 13 | 14 | s.homepage = "https://github.com/apploft/ExpandableLabel" 15 | 16 | s.license = { :type => 'MIT', :file => 'LICENSE' } 17 | 18 | s.author = "Mathias Köhnke" 19 | 20 | s.platform = :ios, "8.0" 21 | 22 | s.source = { :git => "https://github.com/apploft/ExpandableLabel.git", :tag => s.version.to_s } 23 | 24 | s.source_files = "Classes", "Classes/**/*.{swift}" 25 | s.exclude_files = "Classes/Exclude" 26 | 27 | s.requires_arc = true 28 | 29 | end 30 | -------------------------------------------------------------------------------- /ExpandableLabel.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1B0D8A521F3AAA6C004142A4 /* ExpandableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B0D8A501F3AAA6C004142A4 /* ExpandableLabel.swift */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXFileReference section */ 14 | 1B0D8A441F3AAA50004142A4 /* ExpandableLabel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ExpandableLabel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 15 | 1B0D8A501F3AAA6C004142A4 /* ExpandableLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpandableLabel.swift; sourceTree = ""; }; 16 | 1B0D8A511F3AAA6C004142A4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 17 | /* End PBXFileReference section */ 18 | 19 | /* Begin PBXFrameworksBuildPhase section */ 20 | 1B0D8A401F3AAA50004142A4 /* Frameworks */ = { 21 | isa = PBXFrameworksBuildPhase; 22 | buildActionMask = 2147483647; 23 | files = ( 24 | ); 25 | runOnlyForDeploymentPostprocessing = 0; 26 | }; 27 | /* End PBXFrameworksBuildPhase section */ 28 | 29 | /* Begin PBXGroup section */ 30 | 1B0D8A3A1F3AAA50004142A4 = { 31 | isa = PBXGroup; 32 | children = ( 33 | 1B0D8A4F1F3AAA6C004142A4 /* Classes */, 34 | 1B0D8A451F3AAA50004142A4 /* Products */, 35 | ); 36 | sourceTree = ""; 37 | }; 38 | 1B0D8A451F3AAA50004142A4 /* Products */ = { 39 | isa = PBXGroup; 40 | children = ( 41 | 1B0D8A441F3AAA50004142A4 /* ExpandableLabel.framework */, 42 | ); 43 | name = Products; 44 | sourceTree = ""; 45 | }; 46 | 1B0D8A4F1F3AAA6C004142A4 /* Classes */ = { 47 | isa = PBXGroup; 48 | children = ( 49 | 1B0D8A501F3AAA6C004142A4 /* ExpandableLabel.swift */, 50 | 1B0D8A511F3AAA6C004142A4 /* Info.plist */, 51 | ); 52 | path = Classes; 53 | sourceTree = ""; 54 | }; 55 | /* End PBXGroup section */ 56 | 57 | /* Begin PBXHeadersBuildPhase section */ 58 | 1B0D8A411F3AAA50004142A4 /* Headers */ = { 59 | isa = PBXHeadersBuildPhase; 60 | buildActionMask = 2147483647; 61 | files = ( 62 | ); 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | /* End PBXHeadersBuildPhase section */ 66 | 67 | /* Begin PBXNativeTarget section */ 68 | 1B0D8A431F3AAA50004142A4 /* ExpandableLabel */ = { 69 | isa = PBXNativeTarget; 70 | buildConfigurationList = 1B0D8A4C1F3AAA50004142A4 /* Build configuration list for PBXNativeTarget "ExpandableLabel" */; 71 | buildPhases = ( 72 | 1B0D8A3F1F3AAA50004142A4 /* Sources */, 73 | 1B0D8A401F3AAA50004142A4 /* Frameworks */, 74 | 1B0D8A411F3AAA50004142A4 /* Headers */, 75 | 1B0D8A421F3AAA50004142A4 /* Resources */, 76 | ); 77 | buildRules = ( 78 | ); 79 | dependencies = ( 80 | ); 81 | name = ExpandableLabel; 82 | productName = ExpandableLabel; 83 | productReference = 1B0D8A441F3AAA50004142A4 /* ExpandableLabel.framework */; 84 | productType = "com.apple.product-type.framework"; 85 | }; 86 | /* End PBXNativeTarget section */ 87 | 88 | /* Begin PBXProject section */ 89 | 1B0D8A3B1F3AAA50004142A4 /* Project object */ = { 90 | isa = PBXProject; 91 | attributes = { 92 | LastUpgradeCheck = 0900; 93 | TargetAttributes = { 94 | 1B0D8A431F3AAA50004142A4 = { 95 | CreatedOnToolsVersion = 8.3.3; 96 | LastSwiftMigration = 0900; 97 | ProvisioningStyle = Automatic; 98 | }; 99 | }; 100 | }; 101 | buildConfigurationList = 1B0D8A3E1F3AAA50004142A4 /* Build configuration list for PBXProject "ExpandableLabel" */; 102 | compatibilityVersion = "Xcode 3.2"; 103 | developmentRegion = English; 104 | hasScannedForEncodings = 0; 105 | knownRegions = ( 106 | en, 107 | ); 108 | mainGroup = 1B0D8A3A1F3AAA50004142A4; 109 | productRefGroup = 1B0D8A451F3AAA50004142A4 /* Products */; 110 | projectDirPath = ""; 111 | projectRoot = ""; 112 | targets = ( 113 | 1B0D8A431F3AAA50004142A4 /* ExpandableLabel */, 114 | ); 115 | }; 116 | /* End PBXProject section */ 117 | 118 | /* Begin PBXResourcesBuildPhase section */ 119 | 1B0D8A421F3AAA50004142A4 /* Resources */ = { 120 | isa = PBXResourcesBuildPhase; 121 | buildActionMask = 2147483647; 122 | files = ( 123 | ); 124 | runOnlyForDeploymentPostprocessing = 0; 125 | }; 126 | /* End PBXResourcesBuildPhase section */ 127 | 128 | /* Begin PBXSourcesBuildPhase section */ 129 | 1B0D8A3F1F3AAA50004142A4 /* Sources */ = { 130 | isa = PBXSourcesBuildPhase; 131 | buildActionMask = 2147483647; 132 | files = ( 133 | 1B0D8A521F3AAA6C004142A4 /* ExpandableLabel.swift in Sources */, 134 | ); 135 | runOnlyForDeploymentPostprocessing = 0; 136 | }; 137 | /* End PBXSourcesBuildPhase section */ 138 | 139 | /* Begin XCBuildConfiguration section */ 140 | 1B0D8A4A1F3AAA50004142A4 /* Debug */ = { 141 | isa = XCBuildConfiguration; 142 | buildSettings = { 143 | ALWAYS_SEARCH_USER_PATHS = NO; 144 | CLANG_ANALYZER_NONNULL = YES; 145 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 146 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 147 | CLANG_CXX_LIBRARY = "libc++"; 148 | CLANG_ENABLE_MODULES = YES; 149 | CLANG_ENABLE_OBJC_ARC = YES; 150 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 151 | CLANG_WARN_BOOL_CONVERSION = YES; 152 | CLANG_WARN_COMMA = YES; 153 | CLANG_WARN_CONSTANT_CONVERSION = YES; 154 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 155 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 156 | CLANG_WARN_EMPTY_BODY = YES; 157 | CLANG_WARN_ENUM_CONVERSION = YES; 158 | CLANG_WARN_INFINITE_RECURSION = YES; 159 | CLANG_WARN_INT_CONVERSION = YES; 160 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 161 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 162 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 163 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 164 | CLANG_WARN_STRICT_PROTOTYPES = YES; 165 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 166 | CLANG_WARN_UNREACHABLE_CODE = YES; 167 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 168 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 169 | COPY_PHASE_STRIP = NO; 170 | CURRENT_PROJECT_VERSION = 1; 171 | DEBUG_INFORMATION_FORMAT = dwarf; 172 | ENABLE_STRICT_OBJC_MSGSEND = YES; 173 | ENABLE_TESTABILITY = YES; 174 | GCC_C_LANGUAGE_STANDARD = gnu99; 175 | GCC_DYNAMIC_NO_PIC = NO; 176 | GCC_NO_COMMON_BLOCKS = YES; 177 | GCC_OPTIMIZATION_LEVEL = 0; 178 | GCC_PREPROCESSOR_DEFINITIONS = ( 179 | "DEBUG=1", 180 | "$(inherited)", 181 | ); 182 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 183 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 184 | GCC_WARN_UNDECLARED_SELECTOR = YES; 185 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 186 | GCC_WARN_UNUSED_FUNCTION = YES; 187 | GCC_WARN_UNUSED_VARIABLE = YES; 188 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 189 | MTL_ENABLE_DEBUG_INFO = YES; 190 | ONLY_ACTIVE_ARCH = YES; 191 | SDKROOT = iphoneos; 192 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 193 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 194 | TARGETED_DEVICE_FAMILY = "1,2"; 195 | VERSIONING_SYSTEM = "apple-generic"; 196 | VERSION_INFO_PREFIX = ""; 197 | }; 198 | name = Debug; 199 | }; 200 | 1B0D8A4B1F3AAA50004142A4 /* Release */ = { 201 | isa = XCBuildConfiguration; 202 | buildSettings = { 203 | ALWAYS_SEARCH_USER_PATHS = NO; 204 | CLANG_ANALYZER_NONNULL = YES; 205 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 206 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 207 | CLANG_CXX_LIBRARY = "libc++"; 208 | CLANG_ENABLE_MODULES = YES; 209 | CLANG_ENABLE_OBJC_ARC = YES; 210 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 211 | CLANG_WARN_BOOL_CONVERSION = YES; 212 | CLANG_WARN_COMMA = YES; 213 | CLANG_WARN_CONSTANT_CONVERSION = YES; 214 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 215 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 216 | CLANG_WARN_EMPTY_BODY = YES; 217 | CLANG_WARN_ENUM_CONVERSION = YES; 218 | CLANG_WARN_INFINITE_RECURSION = YES; 219 | CLANG_WARN_INT_CONVERSION = YES; 220 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 221 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 222 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 223 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 224 | CLANG_WARN_STRICT_PROTOTYPES = YES; 225 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 226 | CLANG_WARN_UNREACHABLE_CODE = YES; 227 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 228 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 229 | COPY_PHASE_STRIP = NO; 230 | CURRENT_PROJECT_VERSION = 1; 231 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 232 | ENABLE_NS_ASSERTIONS = NO; 233 | ENABLE_STRICT_OBJC_MSGSEND = YES; 234 | GCC_C_LANGUAGE_STANDARD = gnu99; 235 | GCC_NO_COMMON_BLOCKS = YES; 236 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 237 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 238 | GCC_WARN_UNDECLARED_SELECTOR = YES; 239 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 240 | GCC_WARN_UNUSED_FUNCTION = YES; 241 | GCC_WARN_UNUSED_VARIABLE = YES; 242 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 243 | MTL_ENABLE_DEBUG_INFO = NO; 244 | SDKROOT = iphoneos; 245 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 246 | TARGETED_DEVICE_FAMILY = "1,2"; 247 | VALIDATE_PRODUCT = YES; 248 | VERSIONING_SYSTEM = "apple-generic"; 249 | VERSION_INFO_PREFIX = ""; 250 | }; 251 | name = Release; 252 | }; 253 | 1B0D8A4D1F3AAA50004142A4 /* Debug */ = { 254 | isa = XCBuildConfiguration; 255 | buildSettings = { 256 | CODE_SIGN_IDENTITY = ""; 257 | DEFINES_MODULE = YES; 258 | DYLIB_COMPATIBILITY_VERSION = 1; 259 | DYLIB_CURRENT_VERSION = 1; 260 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 261 | INFOPLIST_FILE = "$(SRCROOT)/Classes/Info.plist"; 262 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 263 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 264 | PRODUCT_BUNDLE_IDENTIFIER = de.apploft.ExpandableLabel; 265 | PRODUCT_NAME = "$(TARGET_NAME)"; 266 | SKIP_INSTALL = YES; 267 | SWIFT_VERSION = 4.2; 268 | }; 269 | name = Debug; 270 | }; 271 | 1B0D8A4E1F3AAA50004142A4 /* Release */ = { 272 | isa = XCBuildConfiguration; 273 | buildSettings = { 274 | CODE_SIGN_IDENTITY = ""; 275 | DEFINES_MODULE = YES; 276 | DYLIB_COMPATIBILITY_VERSION = 1; 277 | DYLIB_CURRENT_VERSION = 1; 278 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 279 | INFOPLIST_FILE = "$(SRCROOT)/Classes/Info.plist"; 280 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 281 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 282 | PRODUCT_BUNDLE_IDENTIFIER = de.apploft.ExpandableLabel; 283 | PRODUCT_NAME = "$(TARGET_NAME)"; 284 | SKIP_INSTALL = YES; 285 | SWIFT_VERSION = 4.2; 286 | }; 287 | name = Release; 288 | }; 289 | /* End XCBuildConfiguration section */ 290 | 291 | /* Begin XCConfigurationList section */ 292 | 1B0D8A3E1F3AAA50004142A4 /* Build configuration list for PBXProject "ExpandableLabel" */ = { 293 | isa = XCConfigurationList; 294 | buildConfigurations = ( 295 | 1B0D8A4A1F3AAA50004142A4 /* Debug */, 296 | 1B0D8A4B1F3AAA50004142A4 /* Release */, 297 | ); 298 | defaultConfigurationIsVisible = 0; 299 | defaultConfigurationName = Release; 300 | }; 301 | 1B0D8A4C1F3AAA50004142A4 /* Build configuration list for PBXNativeTarget "ExpandableLabel" */ = { 302 | isa = XCConfigurationList; 303 | buildConfigurations = ( 304 | 1B0D8A4D1F3AAA50004142A4 /* Debug */, 305 | 1B0D8A4E1F3AAA50004142A4 /* Release */, 306 | ); 307 | defaultConfigurationIsVisible = 0; 308 | defaultConfigurationName = Release; 309 | }; 310 | /* End XCConfigurationList section */ 311 | }; 312 | rootObject = 1B0D8A3B1F3AAA50004142A4 /* Project object */; 313 | } 314 | -------------------------------------------------------------------------------- /ExpandableLabel.xcodeproj/xcshareddata/xcschemes/ExpandableLabel.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /ExpandableLabelDemo/ExpandableLabelDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | BF0468121B44121F009EC2F9 /* ExpandableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0468111B44121F009EC2F9 /* ExpandableLabel.swift */; }; 11 | BF38D1C81B42EE3E000091FD /* ExpandableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF38D1C71B42EE3E000091FD /* ExpandableCell.swift */; }; 12 | BF4342A01B416FDD002FDE1F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF43429F1B416FDD002FDE1F /* AppDelegate.swift */; }; 13 | BF4342A21B416FDD002FDE1F /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF4342A11B416FDD002FDE1F /* ViewController.swift */; }; 14 | BF4342A51B416FDD002FDE1F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF4342A31B416FDD002FDE1F /* Main.storyboard */; }; 15 | BF4342A71B416FDD002FDE1F /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF4342A61B416FDD002FDE1F /* Images.xcassets */; }; 16 | BF4342AA1B416FDD002FDE1F /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF4342A81B416FDD002FDE1F /* LaunchScreen.xib */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | BF0468111B44121F009EC2F9 /* ExpandableLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpandableLabel.swift; sourceTree = ""; }; 21 | BF38D1C71B42EE3E000091FD /* ExpandableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpandableCell.swift; sourceTree = ""; }; 22 | BF43429A1B416FDD002FDE1F /* ExpandableLabelDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExpandableLabelDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | BF43429E1B416FDD002FDE1F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 24 | BF43429F1B416FDD002FDE1F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 25 | BF4342A11B416FDD002FDE1F /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 26 | BF4342A41B416FDD002FDE1F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 27 | BF4342A61B416FDD002FDE1F /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 28 | BF4342A91B416FDD002FDE1F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | BF4342971B416FDD002FDE1F /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | BF0468101B44121F009EC2F9 /* Classes */ = { 43 | isa = PBXGroup; 44 | children = ( 45 | BF0468111B44121F009EC2F9 /* ExpandableLabel.swift */, 46 | ); 47 | name = Classes; 48 | path = ../../Classes; 49 | sourceTree = ""; 50 | }; 51 | BF4342911B416FDD002FDE1F = { 52 | isa = PBXGroup; 53 | children = ( 54 | BF43429C1B416FDD002FDE1F /* ExpandableLabelDemo */, 55 | BF43429B1B416FDD002FDE1F /* Products */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | BF43429B1B416FDD002FDE1F /* Products */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | BF43429A1B416FDD002FDE1F /* ExpandableLabelDemo.app */, 63 | ); 64 | name = Products; 65 | sourceTree = ""; 66 | }; 67 | BF43429C1B416FDD002FDE1F /* ExpandableLabelDemo */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | BF0468101B44121F009EC2F9 /* Classes */, 71 | BF43429F1B416FDD002FDE1F /* AppDelegate.swift */, 72 | BF4342A11B416FDD002FDE1F /* ViewController.swift */, 73 | BF38D1C71B42EE3E000091FD /* ExpandableCell.swift */, 74 | BF4342A31B416FDD002FDE1F /* Main.storyboard */, 75 | BF4342A61B416FDD002FDE1F /* Images.xcassets */, 76 | BF4342A81B416FDD002FDE1F /* LaunchScreen.xib */, 77 | BF43429D1B416FDD002FDE1F /* Supporting Files */, 78 | ); 79 | path = ExpandableLabelDemo; 80 | sourceTree = ""; 81 | }; 82 | BF43429D1B416FDD002FDE1F /* Supporting Files */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | BF43429E1B416FDD002FDE1F /* Info.plist */, 86 | ); 87 | name = "Supporting Files"; 88 | sourceTree = ""; 89 | }; 90 | /* End PBXGroup section */ 91 | 92 | /* Begin PBXNativeTarget section */ 93 | BF4342991B416FDD002FDE1F /* ExpandableLabelDemo */ = { 94 | isa = PBXNativeTarget; 95 | buildConfigurationList = BF4342B91B416FDD002FDE1F /* Build configuration list for PBXNativeTarget "ExpandableLabelDemo" */; 96 | buildPhases = ( 97 | BF4342961B416FDD002FDE1F /* Sources */, 98 | BF4342971B416FDD002FDE1F /* Frameworks */, 99 | BF4342981B416FDD002FDE1F /* Resources */, 100 | ); 101 | buildRules = ( 102 | ); 103 | dependencies = ( 104 | ); 105 | name = ExpandableLabelDemo; 106 | productName = ExpandableLabelDemo; 107 | productReference = BF43429A1B416FDD002FDE1F /* ExpandableLabelDemo.app */; 108 | productType = "com.apple.product-type.application"; 109 | }; 110 | /* End PBXNativeTarget section */ 111 | 112 | /* Begin PBXProject section */ 113 | BF4342921B416FDD002FDE1F /* Project object */ = { 114 | isa = PBXProject; 115 | attributes = { 116 | LastSwiftMigration = 0700; 117 | LastSwiftUpdateCheck = 0700; 118 | LastUpgradeCheck = 1010; 119 | ORGANIZATIONNAME = "Mathias Koehnke"; 120 | TargetAttributes = { 121 | BF4342991B416FDD002FDE1F = { 122 | CreatedOnToolsVersion = 6.3.2; 123 | LastSwiftMigration = 0900; 124 | }; 125 | }; 126 | }; 127 | buildConfigurationList = BF4342951B416FDD002FDE1F /* Build configuration list for PBXProject "ExpandableLabelDemo" */; 128 | compatibilityVersion = "Xcode 3.2"; 129 | developmentRegion = English; 130 | hasScannedForEncodings = 0; 131 | knownRegions = ( 132 | en, 133 | Base, 134 | ); 135 | mainGroup = BF4342911B416FDD002FDE1F; 136 | productRefGroup = BF43429B1B416FDD002FDE1F /* Products */; 137 | projectDirPath = ""; 138 | projectRoot = ""; 139 | targets = ( 140 | BF4342991B416FDD002FDE1F /* ExpandableLabelDemo */, 141 | ); 142 | }; 143 | /* End PBXProject section */ 144 | 145 | /* Begin PBXResourcesBuildPhase section */ 146 | BF4342981B416FDD002FDE1F /* Resources */ = { 147 | isa = PBXResourcesBuildPhase; 148 | buildActionMask = 2147483647; 149 | files = ( 150 | BF4342A51B416FDD002FDE1F /* Main.storyboard in Resources */, 151 | BF4342AA1B416FDD002FDE1F /* LaunchScreen.xib in Resources */, 152 | BF4342A71B416FDD002FDE1F /* Images.xcassets in Resources */, 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | }; 156 | /* End PBXResourcesBuildPhase section */ 157 | 158 | /* Begin PBXSourcesBuildPhase section */ 159 | BF4342961B416FDD002FDE1F /* Sources */ = { 160 | isa = PBXSourcesBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | BF0468121B44121F009EC2F9 /* ExpandableLabel.swift in Sources */, 164 | BF4342A21B416FDD002FDE1F /* ViewController.swift in Sources */, 165 | BF4342A01B416FDD002FDE1F /* AppDelegate.swift in Sources */, 166 | BF38D1C81B42EE3E000091FD /* ExpandableCell.swift in Sources */, 167 | ); 168 | runOnlyForDeploymentPostprocessing = 0; 169 | }; 170 | /* End PBXSourcesBuildPhase section */ 171 | 172 | /* Begin PBXVariantGroup section */ 173 | BF4342A31B416FDD002FDE1F /* Main.storyboard */ = { 174 | isa = PBXVariantGroup; 175 | children = ( 176 | BF4342A41B416FDD002FDE1F /* Base */, 177 | ); 178 | name = Main.storyboard; 179 | sourceTree = ""; 180 | }; 181 | BF4342A81B416FDD002FDE1F /* LaunchScreen.xib */ = { 182 | isa = PBXVariantGroup; 183 | children = ( 184 | BF4342A91B416FDD002FDE1F /* Base */, 185 | ); 186 | name = LaunchScreen.xib; 187 | sourceTree = ""; 188 | }; 189 | /* End PBXVariantGroup section */ 190 | 191 | /* Begin XCBuildConfiguration section */ 192 | BF4342B71B416FDD002FDE1F /* Debug */ = { 193 | isa = XCBuildConfiguration; 194 | buildSettings = { 195 | ALWAYS_SEARCH_USER_PATHS = NO; 196 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 197 | CLANG_CXX_LIBRARY = "libc++"; 198 | CLANG_ENABLE_MODULES = YES; 199 | CLANG_ENABLE_OBJC_ARC = YES; 200 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 201 | CLANG_WARN_BOOL_CONVERSION = YES; 202 | CLANG_WARN_COMMA = YES; 203 | CLANG_WARN_CONSTANT_CONVERSION = YES; 204 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 205 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 206 | CLANG_WARN_EMPTY_BODY = YES; 207 | CLANG_WARN_ENUM_CONVERSION = YES; 208 | CLANG_WARN_INFINITE_RECURSION = YES; 209 | CLANG_WARN_INT_CONVERSION = YES; 210 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 211 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 212 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 213 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 214 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 215 | CLANG_WARN_STRICT_PROTOTYPES = YES; 216 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 217 | CLANG_WARN_UNREACHABLE_CODE = YES; 218 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 219 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 220 | COPY_PHASE_STRIP = NO; 221 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 222 | ENABLE_STRICT_OBJC_MSGSEND = YES; 223 | ENABLE_TESTABILITY = YES; 224 | GCC_C_LANGUAGE_STANDARD = gnu99; 225 | GCC_DYNAMIC_NO_PIC = NO; 226 | GCC_NO_COMMON_BLOCKS = YES; 227 | GCC_OPTIMIZATION_LEVEL = 0; 228 | GCC_PREPROCESSOR_DEFINITIONS = ( 229 | "DEBUG=1", 230 | "$(inherited)", 231 | ); 232 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 233 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 234 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 235 | GCC_WARN_UNDECLARED_SELECTOR = YES; 236 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 237 | GCC_WARN_UNUSED_FUNCTION = YES; 238 | GCC_WARN_UNUSED_VARIABLE = YES; 239 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 240 | MTL_ENABLE_DEBUG_INFO = YES; 241 | ONLY_ACTIVE_ARCH = YES; 242 | SDKROOT = iphoneos; 243 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 244 | }; 245 | name = Debug; 246 | }; 247 | BF4342B81B416FDD002FDE1F /* Release */ = { 248 | isa = XCBuildConfiguration; 249 | buildSettings = { 250 | ALWAYS_SEARCH_USER_PATHS = NO; 251 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 252 | CLANG_CXX_LIBRARY = "libc++"; 253 | CLANG_ENABLE_MODULES = YES; 254 | CLANG_ENABLE_OBJC_ARC = YES; 255 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 256 | CLANG_WARN_BOOL_CONVERSION = YES; 257 | CLANG_WARN_COMMA = YES; 258 | CLANG_WARN_CONSTANT_CONVERSION = YES; 259 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 260 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 261 | CLANG_WARN_EMPTY_BODY = YES; 262 | CLANG_WARN_ENUM_CONVERSION = YES; 263 | CLANG_WARN_INFINITE_RECURSION = YES; 264 | CLANG_WARN_INT_CONVERSION = YES; 265 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 266 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 267 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 268 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 269 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 270 | CLANG_WARN_STRICT_PROTOTYPES = YES; 271 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 272 | CLANG_WARN_UNREACHABLE_CODE = YES; 273 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 274 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 275 | COPY_PHASE_STRIP = NO; 276 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 277 | ENABLE_NS_ASSERTIONS = NO; 278 | ENABLE_STRICT_OBJC_MSGSEND = YES; 279 | GCC_C_LANGUAGE_STANDARD = gnu99; 280 | GCC_NO_COMMON_BLOCKS = YES; 281 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 282 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 283 | GCC_WARN_UNDECLARED_SELECTOR = YES; 284 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 285 | GCC_WARN_UNUSED_FUNCTION = YES; 286 | GCC_WARN_UNUSED_VARIABLE = YES; 287 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 288 | MTL_ENABLE_DEBUG_INFO = NO; 289 | SDKROOT = iphoneos; 290 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 291 | VALIDATE_PRODUCT = YES; 292 | }; 293 | name = Release; 294 | }; 295 | BF4342BA1B416FDD002FDE1F /* Debug */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 299 | INFOPLIST_FILE = ExpandableLabelDemo/Info.plist; 300 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 301 | PRODUCT_BUNDLE_IDENTIFIER = "de.apploft.$(PRODUCT_NAME:rfc1034identifier)"; 302 | PRODUCT_NAME = "$(TARGET_NAME)"; 303 | SWIFT_VERSION = 4.2; 304 | }; 305 | name = Debug; 306 | }; 307 | BF4342BB1B416FDD002FDE1F /* Release */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 311 | INFOPLIST_FILE = ExpandableLabelDemo/Info.plist; 312 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 313 | PRODUCT_BUNDLE_IDENTIFIER = "de.apploft.$(PRODUCT_NAME:rfc1034identifier)"; 314 | PRODUCT_NAME = "$(TARGET_NAME)"; 315 | SWIFT_VERSION = 4.2; 316 | }; 317 | name = Release; 318 | }; 319 | /* End XCBuildConfiguration section */ 320 | 321 | /* Begin XCConfigurationList section */ 322 | BF4342951B416FDD002FDE1F /* Build configuration list for PBXProject "ExpandableLabelDemo" */ = { 323 | isa = XCConfigurationList; 324 | buildConfigurations = ( 325 | BF4342B71B416FDD002FDE1F /* Debug */, 326 | BF4342B81B416FDD002FDE1F /* Release */, 327 | ); 328 | defaultConfigurationIsVisible = 0; 329 | defaultConfigurationName = Release; 330 | }; 331 | BF4342B91B416FDD002FDE1F /* Build configuration list for PBXNativeTarget "ExpandableLabelDemo" */ = { 332 | isa = XCConfigurationList; 333 | buildConfigurations = ( 334 | BF4342BA1B416FDD002FDE1F /* Debug */, 335 | BF4342BB1B416FDD002FDE1F /* Release */, 336 | ); 337 | defaultConfigurationIsVisible = 0; 338 | defaultConfigurationName = Release; 339 | }; 340 | /* End XCConfigurationList section */ 341 | }; 342 | rootObject = BF4342921B416FDD002FDE1F /* Project object */; 343 | } 344 | -------------------------------------------------------------------------------- /ExpandableLabelDemo/ExpandableLabelDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // 4 | // Copyright (c) 2015 apploft. GmbH 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import UIKit 25 | 26 | @UIApplicationMain 27 | class AppDelegate: UIResponder, UIApplicationDelegate { 28 | 29 | var window: UIWindow? 30 | 31 | 32 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 33 | // Override point for customization after application launch. 34 | return true 35 | } 36 | 37 | func applicationWillResignActive(_ application: UIApplication) { 38 | // 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. 39 | // 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. 40 | } 41 | 42 | func applicationDidEnterBackground(_ application: UIApplication) { 43 | // 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. 44 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 45 | } 46 | 47 | func applicationWillEnterForeground(_ application: UIApplication) { 48 | // 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. 49 | } 50 | 51 | func applicationDidBecomeActive(_ application: UIApplication) { 52 | // 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. 53 | } 54 | 55 | func applicationWillTerminate(_ application: UIApplication) { 56 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 57 | } 58 | 59 | 60 | } 61 | 62 | -------------------------------------------------------------------------------- /ExpandableLabelDemo/ExpandableLabelDemo/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /ExpandableLabelDemo/ExpandableLabelDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /ExpandableLabelDemo/ExpandableLabelDemo/ExpandableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpandableCell.swift 3 | // 4 | // Copyright (c) 2015 apploft. GmbH 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import UIKit 25 | 26 | class ExpandableCell : UITableViewCell { 27 | 28 | 29 | @IBOutlet weak var expandableLabel: ExpandableLabel! 30 | 31 | override func prepareForReuse() { 32 | super.prepareForReuse() 33 | expandableLabel.collapsed = true 34 | expandableLabel.text = nil 35 | } 36 | } -------------------------------------------------------------------------------- /ExpandableLabelDemo/ExpandableLabelDemo/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" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /ExpandableLabelDemo/ExpandableLabelDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Label Demo 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /ExpandableLabelDemo/ExpandableLabelDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // 4 | // Copyright (c) 2015 apploft. GmbH 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import UIKit 25 | 26 | class ViewController: UITableViewController, ExpandableLabelDelegate { 27 | 28 | let numberOfCells : NSInteger = 12 29 | var states : Array! 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | 34 | states = [Bool](repeating: true, count: numberOfCells) 35 | tableView.estimatedRowHeight = 44 36 | tableView.rowHeight = UITableView.automaticDimension 37 | } 38 | 39 | override func viewDidAppear(_ animated: Bool) { 40 | super.viewDidAppear(animated) 41 | tableView.reloadData() 42 | } 43 | 44 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 45 | let currentSource = preparedSources()[indexPath.row] 46 | 47 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! ExpandableCell 48 | cell.expandableLabel.delegate = self 49 | 50 | cell.expandableLabel.setLessLinkWith(lessLink: "Close", attributes: [.foregroundColor:UIColor.red], position: currentSource.textAlignment) 51 | 52 | cell.layoutIfNeeded() 53 | 54 | cell.expandableLabel.shouldCollapse = true 55 | cell.expandableLabel.textReplacementType = currentSource.textReplacementType 56 | cell.expandableLabel.numberOfLines = currentSource.numberOfLines 57 | cell.expandableLabel.collapsed = states[indexPath.row] 58 | cell.expandableLabel.text = currentSource.text 59 | 60 | return cell 61 | } 62 | 63 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 64 | return states.count 65 | } 66 | 67 | func preparedSources() -> [(text: String, textReplacementType: ExpandableLabel.TextReplacementType, numberOfLines: Int, textAlignment: NSTextAlignment)] { 68 | return [(loremIpsumText(), .word, 3, .left), 69 | (textWithNewLinesInCollapsedLine(), .word, 2, .center), 70 | (textWithLongWordInCollapsedLine(), .character, 1, .right), 71 | (textWithVeryLongWords(), .character, 1, .left), 72 | (loremIpsumText(), .word, 4, .center), 73 | (loremIpsumText(), .character, 3, .right), 74 | (loremIpsumText(), .word, 2, .left), 75 | (loremIpsumText(), .character, 5, .center), 76 | (loremIpsumText(), .word, 3, .right), 77 | (loremIpsumText(), .character, 1, .left), 78 | (textWithShortWordsPerLine(), .character, 3, .center), 79 | (textEmojis(), .character, 3, .left)] 80 | } 81 | 82 | 83 | func loremIpsumText() -> String { 84 | return "On third line our text need be collapsed because we have ordinary text, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." 85 | } 86 | 87 | func textWithNewLinesInCollapsedLine() -> String { 88 | return "When u had new line specialChars \n More not appeared eirmod\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n tempor invidunt ut\n\n\n\n labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." 89 | } 90 | 91 | func textWithLongWordInCollapsedLine() -> String { 92 | return "When u had long word which not entered in one line More not appeared FooBaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaR tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." 93 | } 94 | 95 | func textWithVeryLongWords() -> String { 96 | return "FooBaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaR FooBaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaR FooBaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaR FooBaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaR Will show first line and will increase touch area for more voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." 97 | } 98 | 99 | func textWithShortWordsPerLine() -> String { 100 | return "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN" 101 | } 102 | 103 | func textEmojis() -> String { 104 | return "😂😄😃😊😍😗😜😅😓☺️😶🤦😒😁😟😵🙁🤔🤓☹️🙄😑😫😱🙂😧🤵😶👥👩‍❤️‍👩💖👨‍❤️‍💋‍👨💏👩‍👩‍👦‍👦👦👀👨‍👩‍👧‍👦👩‍❤️‍👩🗨🕴👩‍❤️‍💋‍👩👧☹️😠😤😆💚🙄🤒💋😿👄" 105 | } 106 | 107 | // 108 | // MARK: ExpandableLabel Delegate 109 | // 110 | 111 | func willExpandLabel(_ label: ExpandableLabel) { 112 | tableView.beginUpdates() 113 | } 114 | 115 | func didExpandLabel(_ label: ExpandableLabel) { 116 | let point = label.convert(CGPoint.zero, to: tableView) 117 | if let indexPath = tableView.indexPathForRow(at: point) as IndexPath? { 118 | states[indexPath.row] = false 119 | DispatchQueue.main.async { [weak self] in 120 | self?.tableView.scrollToRow(at: indexPath, at: .top, animated: true) 121 | } 122 | } 123 | tableView.endUpdates() 124 | } 125 | 126 | func willCollapseLabel(_ label: ExpandableLabel) { 127 | tableView.beginUpdates() 128 | } 129 | 130 | func didCollapseLabel(_ label: ExpandableLabel) { 131 | let point = label.convert(CGPoint.zero, to: tableView) 132 | if let indexPath = tableView.indexPathForRow(at: point) as IndexPath? { 133 | states[indexPath.row] = true 134 | DispatchQueue.main.async { [weak self] in 135 | self?.tableView.scrollToRow(at: indexPath, at: .top, animated: true) 136 | } 137 | } 138 | tableView.endUpdates() 139 | } 140 | } 141 | 142 | extension String { 143 | 144 | func specialPriceAttributedStringWith(_ color: UIColor) -> NSMutableAttributedString { 145 | let attributes = [NSAttributedString.Key.strikethroughStyle: NSNumber(value: NSUnderlineStyle.single.rawValue as Int), 146 | .foregroundColor: color, .font: fontForPrice()] 147 | return NSMutableAttributedString(attributedString: NSAttributedString(string: self, attributes: attributes)) 148 | } 149 | 150 | func priceAttributedStringWith(_ color: UIColor) -> NSAttributedString { 151 | let attributes = [NSAttributedString.Key.foregroundColor: color, .font: fontForPrice()] 152 | 153 | return NSAttributedString(string: self, attributes: attributes) 154 | } 155 | 156 | func priceAttributedString(_ color: UIColor) -> NSAttributedString { 157 | let attributes = [NSAttributedString.Key.foregroundColor: color] 158 | 159 | return NSAttributedString(string: self, attributes: attributes) 160 | } 161 | 162 | fileprivate func fontForPrice() -> UIFont { 163 | return UIFont(name: "Helvetica-Neue", size: 13) ?? UIFont() 164 | } 165 | } 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 apploft. GmbH 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExpandableLabel 2 | ExpandableLabel is a simple UILabel subclass that shows a tappable link if the content doesn't fit the specified number of lines. If touched, the label will expand to show the entire content. 3 | 4 | 5 | 6 | ## Maintenance 7 | This project is passively maintained. Pull Requests are welcome, will be reviewed, merged and released as new CocoaPod version as long as they don't break things. 8 | Nevertheless we don't have the resources to actively continue development, answer issues or give support for integration. 9 | 10 | # Installation 11 | 12 | ### [CocoaPods](https://guides.cocoapods.org/using/using-cocoapods.html) 13 | 14 | Add this to your Podfile: 15 | 16 | ```ruby 17 | pod "ExpandableLabel" 18 | ``` 19 | 20 | ### [Carthage](https://github.com/Carthage/Carthage) 21 | 22 | Add this to your Cartfile: 23 | 24 | ```ruby 25 | github "apploft/ExpandableLabel" 26 | ``` 27 | 28 | Run `carthage` to build the framework and drag the built `ExpandableLabel.framework` into your Xcode project. 29 | 30 | # Usage 31 | Using ExpandableLabel is very simple. In your storyboard, set the custom class of your UILabel to ExpandableLabel and set the desired number of lines (for the collapsed state): 32 | 33 | _**Note:** In Carthage, set Module to `ExpandableLabel`._ 34 | 35 | ```swift 36 | expandableLabel.numberOfLines = 3 37 | ``` 38 | 39 | Apart from that, one can modify the following settings: 40 | 41 | ##### delegate 42 | Set a delegate to get notified in case the link has been touched. 43 | 44 | ##### collapsed 45 | Set _true_ if the label should be collapsed or _false_ for expanded. 46 | 47 | ```swift 48 | expandableLabel.collapsed = true 49 | ``` 50 | 51 | ##### collapsedAttributedLink 52 | Set the link name (and attributes) that is shown when collapsed. 53 | 54 | ```swift 55 | expandableLabel.collapsedAttributedLink = NSAttributedString(string: "Read More") 56 | ``` 57 | 58 | ##### expandedAttributedLink 59 | Set the link name (and attributes) that is shown when expanded. 60 | It is optional and can be nil. 61 | 62 | ```swift 63 | expandableLabel.expandedAttributedLink = NSAttributedString(string: "Read Less") 64 | ``` 65 | 66 | ##### setLessLinkWith(lessLink: String, attributes: [String: AnyObject], position: NSTextAlignment?) 67 | 68 | Setter for expandedAttributedLink with caption, String attributes and optional horizontal alignment as NSTextAlignment. 69 | If the parameter position is nil, the collapse link will be inserted at the end of the text. 70 | 71 | ```swift 72 | expandableLabel.setLessLinkWith(lessLink: "Close", attributes: [NSForegroundColorAttributeName:UIColor.red], position: nil) 73 | ``` 74 | 75 | 76 | ##### ellipsis 77 | Set the ellipsis that appears just after the text and before the link. 78 | 79 | ```swift 80 | expandableLabel.ellipsis = NSAttributedString(string: "...") 81 | ``` 82 | 83 | 84 | # License 85 | ExpandableLabel is available under the MIT license. See the LICENSE file for more info. 86 | -------------------------------------------------------------------------------- /Resources/ExpandableLabel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apploft/ExpandableLabel/69643c349981c13764efe6be28aea5dc63c5bb81/Resources/ExpandableLabel.gif -------------------------------------------------------------------------------- /Resources/MoreLessExpand.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apploft/ExpandableLabel/69643c349981c13764efe6be28aea5dc63c5bb81/Resources/MoreLessExpand.gif -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | fastlane_version "1.66.0" 2 | 3 | desc "Release new pod version" 4 | lane :release do |options| 5 | skip_docs 6 | target_version = options[:version] 7 | raise "The version is missing. Use `fastlane release version:{version_number}`.`" if target_version.nil? 8 | 9 | path = options[:path] || Dir["../*.podspec"].first[3..Dir["../*.podspec"].first.length] 10 | raise "The podspec file path is missing. Place a podspec file in the root directory or use `fastlane release version:{version_number} path:{path_to_podspec}`.`" if path.nil? 11 | 12 | ensure_git_branch 13 | ensure_git_status_clean 14 | 15 | version_bump_podspec(path: path, version_number: target_version) 16 | 17 | git_commit(path: path, message: "Bump version to #{target_version}") 18 | add_git_tag tag: target_version 19 | 20 | push_to_git_remote 21 | pod_push 22 | end --------------------------------------------------------------------------------