More StatusContentLabel cleanup
This commit is contained in:
parent
40ba5c371a
commit
a025926c0f
|
@ -35,14 +35,13 @@ class StatusContentLabel: UILabel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var lineSpacing: CGFloat = 0
|
|
||||||
public var minimumLineHeight: CGFloat = 0
|
|
||||||
|
|
||||||
private var _customizing = true
|
private var _customizing = true
|
||||||
|
|
||||||
private lazy var textStorage = NSTextStorage()
|
private lazy var textStorage = NSTextStorage()
|
||||||
private lazy var layoutManager = NSLayoutManager()
|
private lazy var layoutManager = NSLayoutManager()
|
||||||
private lazy var textContainer = NSTextContainer()
|
private lazy var textContainer = NSTextContainer()
|
||||||
private var selectedElement: (range: NSRange, url: URL)?
|
|
||||||
|
private var selectedLink: (range: NSRange, url: URL)?
|
||||||
|
|
||||||
private var links: [NSRange: URL] = [:]
|
private var links: [NSRange: URL] = [:]
|
||||||
|
|
||||||
|
@ -58,11 +57,27 @@ class StatusContentLabel: UILabel {
|
||||||
setupLabel()
|
setupLabel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setupLabel() {
|
||||||
|
textStorage.addLayoutManager(layoutManager)
|
||||||
|
layoutManager.addTextContainer(textContainer)
|
||||||
|
textContainer.lineFragmentPadding = 0
|
||||||
|
textContainer.lineBreakMode = lineBreakMode
|
||||||
|
textContainer.maximumNumberOfLines = numberOfLines
|
||||||
|
isUserInteractionEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
updateTextStorage()
|
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) {
|
override func drawText(in rect: CGRect) {
|
||||||
let range = NSRange(location: 0, length: textStorage.length)
|
let range = NSRange(location: 0, length: textStorage.length)
|
||||||
|
|
||||||
|
@ -73,113 +88,7 @@ class StatusContentLabel: UILabel {
|
||||||
layoutManager.drawGlyphs(forGlyphRange: range, at: origin)
|
layoutManager.drawGlyphs(forGlyphRange: range, at: origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
override var intrinsicContentSize: CGSize {
|
// MARK: - HTML parsing
|
||||||
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<UITouch>, with event: UIEvent?) {
|
|
||||||
guard let touch = touches.first else { return }
|
|
||||||
if onTouch(touch) { return }
|
|
||||||
super.touchesBegan(touches, with: event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
||||||
guard let touch = touches.first else { return }
|
|
||||||
if onTouch(touch) { return }
|
|
||||||
super.touchesMoved(touches, with: event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
||||||
guard let touch = touches.first else { return }
|
|
||||||
if onTouch(touch) { return }
|
|
||||||
super.touchesCancelled(touches, with: event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func touchesEnded(_ touches: Set<UITouch>, 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
|
|
||||||
}
|
|
||||||
|
|
||||||
private func parseHTML() {
|
private func parseHTML() {
|
||||||
if _customizing { return }
|
if _customizing { return }
|
||||||
|
@ -235,8 +144,10 @@ class StatusContentLabel: UILabel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Text Storage
|
||||||
|
|
||||||
private func updateTextStorage() {
|
private func updateTextStorage() {
|
||||||
guard !_customizing else { return }
|
if _customizing { return }
|
||||||
guard let attributedText = attributedText,
|
guard let attributedText = attributedText,
|
||||||
attributedText.length > 0 else {
|
attributedText.length > 0 else {
|
||||||
links = [:]
|
links = [:]
|
||||||
|
@ -252,21 +163,98 @@ class StatusContentLabel: UILabel {
|
||||||
setNeedsDisplay()
|
setNeedsDisplay()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addLineBreak(_ attrString: NSAttributedString) -> NSMutableAttributedString {
|
// MARK: - Interaction
|
||||||
let mutAttrString = NSMutableAttributedString(attributedString: attrString)
|
|
||||||
|
|
||||||
var range = NSRange(location: 0, length: 0)
|
private func onTouch(_ touch: UITouch) -> Bool {
|
||||||
var attributes = mutAttrString.attributes(at: 0, effectiveRange: &range)
|
let location = touch.location(in: self)
|
||||||
|
var avoidSuperCall = false
|
||||||
|
|
||||||
let paragraphStyle = attributes[.paragraphStyle] as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
|
switch touch.phase {
|
||||||
paragraphStyle.lineBreakMode = .byWordWrapping
|
case .began, .moved:
|
||||||
paragraphStyle.alignment = textAlignment
|
if let link = link(at: location) {
|
||||||
paragraphStyle.lineSpacing = lineSpacing
|
if link.range.location != selectedLink?.range.location || link.range.length != selectedLink?.range.length {
|
||||||
paragraphStyle.minimumLineHeight = minimumLineHeight > 0 ? minimumLineHeight : self.font.pointSize * 1.14
|
updateAttributesWhenSelected(false)
|
||||||
attributes[.paragraphStyle] = paragraphStyle
|
selectedLink = link
|
||||||
mutAttrString.setAttributes(attributes, range: range)
|
updateAttributesWhenSelected(true)
|
||||||
|
}
|
||||||
|
avoidSuperCall = true
|
||||||
|
} else {
|
||||||
|
updateAttributesWhenSelected(false)
|
||||||
|
selectedLink = nil
|
||||||
|
}
|
||||||
|
case .ended:
|
||||||
|
guard let selectedLink = selectedLink else { return avoidSuperCall }
|
||||||
|
|
||||||
return mutAttrString
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return avoidSuperCall
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
guard let touch = touches.first else { return }
|
||||||
|
if onTouch(touch) { return }
|
||||||
|
super.touchesBegan(touches, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
guard let touch = touches.first else { return }
|
||||||
|
if onTouch(touch) { return }
|
||||||
|
super.touchesMoved(touches, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
guard let touch = touches.first else { return }
|
||||||
|
if onTouch(touch) { return }
|
||||||
|
super.touchesCancelled(touches, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesEnded(_ touches: Set<UITouch>, 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 }
|
||||||
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,93 +50,8 @@ class StatusTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
contentLabel.text = status.content
|
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() {
|
override func prepareForReuse() {
|
||||||
if let url = avatarURL {
|
if let url = avatarURL {
|
||||||
AvatarCache.shared.cancel(url)
|
AvatarCache.shared.cancel(url)
|
||||||
|
|
Loading…
Reference in New Issue