From a025926c0f5d68174efdb01307a9e7c67c896365 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 26 Aug 2018 15:19:54 -0400 Subject: [PATCH] More StatusContentLabel cleanup --- Tusker/Views/StatusContentLabel.swift | 236 ++++++++++++------------- Tusker/Views/StatusTableViewCell.swift | 85 --------- 2 files changed, 112 insertions(+), 209 deletions(-) diff --git a/Tusker/Views/StatusContentLabel.swift b/Tusker/Views/StatusContentLabel.swift index 9cd078f8..bd016d72 100644 --- a/Tusker/Views/StatusContentLabel.swift +++ b/Tusker/Views/StatusContentLabel.swift @@ -35,14 +35,13 @@ class StatusContentLabel: UILabel { } } - public var lineSpacing: CGFloat = 0 - public var minimumLineHeight: CGFloat = 0 - private var _customizing = true + private lazy var textStorage = NSTextStorage() private lazy var layoutManager = NSLayoutManager() private lazy var textContainer = NSTextContainer() - private var selectedElement: (range: NSRange, url: URL)? + + private var selectedLink: (range: NSRange, url: URL)? private var links: [NSRange: URL] = [:] @@ -58,11 +57,27 @@ class StatusContentLabel: UILabel { setupLabel() } + private func setupLabel() { + textStorage.addLayoutManager(layoutManager) + layoutManager.addTextContainer(textContainer) + textContainer.lineFragmentPadding = 0 + textContainer.lineBreakMode = lineBreakMode + textContainer.maximumNumberOfLines = numberOfLines + isUserInteractionEnabled = true + } + override func awakeFromNib() { super.awakeFromNib() updateTextStorage() } + override var intrinsicContentSize: CGSize { + let superSize = super.intrinsicContentSize + textContainer.size = CGSize(width: superSize.width, height: .greatestFiniteMagnitude) + let size = layoutManager.usedRect(for: textContainer) + return CGSize(width: ceil(size.width), height: ceil(size.height)) + } + override func drawText(in rect: CGRect) { let range = NSRange(location: 0, length: textStorage.length) @@ -73,113 +88,7 @@ class StatusContentLabel: UILabel { layoutManager.drawGlyphs(forGlyphRange: range, at: origin) } - override var intrinsicContentSize: CGSize { - let superSize = super.intrinsicContentSize - textContainer.size = CGSize(width: superSize.width, height: .greatestFiniteMagnitude) - let size = layoutManager.usedRect(for: textContainer) - return CGSize(width: ceil(size.width), height: ceil(size.height)) - } - - private func element(at location: CGPoint) -> (range: NSRange, url: URL)? { - guard textStorage.length > 0 else { return nil } - - let boundingRect = layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: textStorage.length), in: textContainer) - guard boundingRect.contains(location) else { - return nil - } - - let index = layoutManager.glyphIndex(for: location, in: textContainer) - - for (range, url) in links { - if index >= range.location && index <= range.location + range.length { - return (range, url) - } - } - - return nil - } - - private func updateAttributesWhenSelected(_ isSelected: Bool) { - guard let selectedElement = selectedElement else { return } - - var attributes = textStorage.attributes(at: 0, effectiveRange: nil) - - attributes[.foregroundColor] = isSelected ? nil : UIColor.blue - - textStorage.addAttributes(attributes, range: selectedElement.range) - - setNeedsDisplay() - } - - private func onTouch(_ touch: UITouch) -> Bool { - let location = touch.location(in: self) - var avoidSuperCall = false - - switch touch.phase { - case .began, .moved: - if let element = element(at: location) { - if element.range.location != selectedElement?.range.location || element.range.length != selectedElement?.range.length { - updateAttributesWhenSelected(false) - selectedElement = element - updateAttributesWhenSelected(true) - } - avoidSuperCall = true - } else { - updateAttributesWhenSelected(false) - selectedElement = nil - } - case .ended: - guard let selectedElement = selectedElement else { return avoidSuperCall } - - print("tapped \(selectedElement)") - - let when = DispatchTime.now() + Double(Int64(0.25 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) - DispatchQueue.main.asyncAfter(deadline: when) { - self.updateAttributesWhenSelected(false) - self.selectedElement = nil - } - case .cancelled: - updateAttributesWhenSelected(false) - selectedElement = nil - case .stationary: - break - } - - return avoidSuperCall - } - - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - guard let touch = touches.first else { return } - if onTouch(touch) { return } - super.touchesBegan(touches, with: event) - } - - override func touchesMoved(_ touches: Set, with event: UIEvent?) { - guard let touch = touches.first else { return } - if onTouch(touch) { return } - super.touchesMoved(touches, with: event) - } - - override func touchesCancelled(_ touches: Set, with event: UIEvent?) { - guard let touch = touches.first else { return } - if onTouch(touch) { return } - super.touchesCancelled(touches, with: event) - } - - override func touchesEnded(_ touches: Set, with event: UIEvent?) { - guard let touch = touches.first else { return } - if onTouch(touch) { return } - super.touchesEnded(touches, with: event) - } - - private func setupLabel() { - textStorage.addLayoutManager(layoutManager) - layoutManager.addTextContainer(textContainer) - textContainer.lineFragmentPadding = 0 - textContainer.lineBreakMode = lineBreakMode - textContainer.maximumNumberOfLines = numberOfLines - isUserInteractionEnabled = true - } + // MARK: - HTML parsing private func parseHTML() { if _customizing { return } @@ -235,8 +144,10 @@ class StatusContentLabel: UILabel { } } + // MARK: - Text Storage + private func updateTextStorage() { - guard !_customizing else { return } + if _customizing { return } guard let attributedText = attributedText, attributedText.length > 0 else { links = [:] @@ -252,21 +163,98 @@ class StatusContentLabel: UILabel { setNeedsDisplay() } - private func addLineBreak(_ attrString: NSAttributedString) -> NSMutableAttributedString { - let mutAttrString = NSMutableAttributedString(attributedString: attrString) + // MARK: - Interaction + + private func onTouch(_ touch: UITouch) -> Bool { + let location = touch.location(in: self) + var avoidSuperCall = false - var range = NSRange(location: 0, length: 0) - var attributes = mutAttrString.attributes(at: 0, effectiveRange: &range) + switch touch.phase { + case .began, .moved: + if let link = link(at: location) { + if link.range.location != selectedLink?.range.location || link.range.length != selectedLink?.range.length { + updateAttributesWhenSelected(false) + selectedLink = link + updateAttributesWhenSelected(true) + } + avoidSuperCall = true + } else { + updateAttributesWhenSelected(false) + selectedLink = nil + } + case .ended: + guard let selectedLink = selectedLink else { return avoidSuperCall } + + print("tapped \(selectedLink)") + + let when = DispatchTime.now() + Double(Int64(0.25 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) + DispatchQueue.main.asyncAfter(deadline: when) { + self.updateAttributesWhenSelected(false) + self.selectedLink = nil + } + case .cancelled: + updateAttributesWhenSelected(false) + selectedLink = nil + case .stationary: + break + } - let paragraphStyle = attributes[.paragraphStyle] as? NSMutableParagraphStyle ?? NSMutableParagraphStyle() - paragraphStyle.lineBreakMode = .byWordWrapping - paragraphStyle.alignment = textAlignment - paragraphStyle.lineSpacing = lineSpacing - paragraphStyle.minimumLineHeight = minimumLineHeight > 0 ? minimumLineHeight : self.font.pointSize * 1.14 - attributes[.paragraphStyle] = paragraphStyle - mutAttrString.setAttributes(attributes, range: range) + return avoidSuperCall + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + guard let touch = touches.first else { return } + if onTouch(touch) { return } + super.touchesBegan(touches, with: event) + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + guard let touch = touches.first else { return } + if onTouch(touch) { return } + super.touchesMoved(touches, with: event) + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + guard let touch = touches.first else { return } + if onTouch(touch) { return } + super.touchesCancelled(touches, with: event) + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + guard let touch = touches.first else { return } + if onTouch(touch) { return } + super.touchesEnded(touches, with: event) + } + + private func link(at location: CGPoint) -> (range: NSRange, url: URL)? { + guard textStorage.length > 0 else { return nil } - return mutAttrString + let boundingRect = layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: textStorage.length), in: textContainer) + guard boundingRect.contains(location) else { + return nil + } + + let index = layoutManager.glyphIndex(for: location, in: textContainer) + + for (range, url) in links { + if index >= range.location && index <= range.location + range.length { + return (range, url) + } + } + + return nil + } + + private func updateAttributesWhenSelected(_ isSelected: Bool) { + guard let selectedLink = selectedLink else { return } + + var attributes = textStorage.attributes(at: 0, effectiveRange: nil) + + attributes[.foregroundColor] = isSelected ? nil : UIColor.blue + + textStorage.addAttributes(attributes, range: selectedLink.range) + + setNeedsDisplay() } } diff --git a/Tusker/Views/StatusTableViewCell.swift b/Tusker/Views/StatusTableViewCell.swift index 31876366..4d991c4f 100644 --- a/Tusker/Views/StatusTableViewCell.swift +++ b/Tusker/Views/StatusTableViewCell.swift @@ -50,92 +50,7 @@ class StatusTableViewCell: UITableViewCell { } contentLabel.text = status.content - -// let doc = try! SwiftSoup.parse(status.content) -// let body = doc.body()! -//// print("---") -//// print(status.content) -//// print("---") -// -// let (text, links) = attributedTextForNode(body) -// self.links = links -// contentLabel.attributedText = text -// contentLabel.isUserInteractionEnabled = true -// contentLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTapOnContentLabel(_:)))) -// -// // https://stackoverflow.com/a/28519273/4731558 -// layoutManager = NSLayoutManager() -// textContainer = NSTextContainer(size: .zero) -// textStorage = NSTextStorage(attributedString: text) -// -// layoutManager.addTextContainer(textContainer) -// textStorage.addLayoutManager(layoutManager) -// -// textContainer.lineFragmentPadding = 0 -// textContainer.lineBreakMode = contentLabel.lineBreakMode -// textContainer.maximumNumberOfLines = contentLabel.numberOfLines } - -// func attributedTextForNode(_ node: Node) -> (NSAttributedString, [NSRange: URL]) { -// switch node { -// case let node as TextNode: -// return (NSAttributedString(string: node.text()), [:]) -// case let node as Element: -// var links = [NSRange: URL]() -// let attributed = NSMutableAttributedString() -// node.getChildNodes().forEach { child in -// let (text, childLinks) = attributedTextForNode(child) -// childLinks.forEach { range, url in -// let newRange = NSRange(location: range.location + attributed.length, length: range.length) -// links[newRange] = url -// } -// attributed.append(text) -// } -// -// switch node.tagName() { -// case "br": -// attributed.append(NSAttributedString(string: "\n")) -// case "a": -// if let link = try? node.attr("href"), -// let url = URL(string: link) { -// let linkRange = NSRange(location: 0, length: attributed.length) -// let linkAttributes: [NSAttributedString.Key: Any] = [ -// .foregroundColor: UIColor.blue -// ] -// attributed.setAttributes(linkAttributes, range: linkRange) -// -// links[linkRange] = url -// } -// default: -// break -// } -// -// return (attributed, links) -// default: -// fatalError("Unexpected node type: \(type(of: node))") -// } -// } - -// @objc func handleTapOnContentLabel(_ tapGesture: UITapGestureRecognizer) { -// guard let view = tapGesture.view else { fatalError() } -// -// let locationOfTouchInLabel = tapGesture.location(in: view) -// let labelSize = view.bounds.size -// let textBoundingBox = layoutManager.usedRect(for: textContainer) -// let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, -// y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y) -// let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, -// y: locationOfTouchInLabel.y - textContainerOffset.y) -// let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) -// -// let link = links.first { (range, _) -> Bool in -// range.contains(indexOfCharacter) -// } -// -// if let (_, url) = link { -// print("Open URL: \(url)") -// } -// } override func prepareForReuse() { if let url = avatarURL {