Tusker/Tusker/Views/StatusContentLabel.swift

261 lines
8.5 KiB
Swift
Raw Normal View History

2018-08-26 18:49:22 +00:00
//
// StatusContentLabel.swift
// Tusker
//
// Created by Shadowfacts on 8/25/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
import SwiftSoup
class StatusContentLabel: UILabel {
override var text: String? {
didSet {
parseHTML()
}
}
override var attributedText: NSAttributedString? {
didSet {
updateTextStorage()
}
}
override var numberOfLines: Int {
didSet {
textContainer.maximumNumberOfLines = numberOfLines
}
}
override var lineBreakMode: NSLineBreakMode {
didSet {
textContainer.lineBreakMode = lineBreakMode
}
}
private var _customizing = true
2018-08-26 19:19:54 +00:00
2018-08-26 18:49:22 +00:00
private lazy var textStorage = NSTextStorage()
private lazy var layoutManager = NSLayoutManager()
private lazy var textContainer = NSTextContainer()
2018-08-26 19:19:54 +00:00
private var selectedLink: (range: NSRange, url: URL)?
2018-08-26 18:49:22 +00:00
private var links: [NSRange: URL] = [:]
override init(frame: CGRect) {
super.init(frame: frame)
_customizing = false
setupLabel()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
_customizing = false
setupLabel()
}
2018-08-26 19:19:54 +00:00
private func setupLabel() {
textStorage.addLayoutManager(layoutManager)
layoutManager.addTextContainer(textContainer)
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
isUserInteractionEnabled = true
}
2018-08-26 18:49:22 +00:00
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))
}
2018-08-26 19:19:54 +00:00
override func drawText(in rect: CGRect) {
let range = NSRange(location: 0, length: textStorage.length)
2018-08-26 18:49:22 +00:00
2018-08-26 19:19:54 +00:00
textContainer.size = rect.size
let origin = rect.origin
2018-08-26 18:49:22 +00:00
2018-08-26 19:19:54 +00:00
layoutManager.drawBackground(forGlyphRange: range, at: origin)
layoutManager.drawGlyphs(forGlyphRange: range, at: origin)
2018-08-26 18:49:22 +00:00
}
2018-08-26 19:19:54 +00:00
// MARK: - HTML parsing
2018-08-26 18:49:22 +00:00
private func parseHTML() {
if _customizing { return }
guard let text = text else { return }
let doc = try! SwiftSoup.parse(text)
let body = doc.body()!
let (attributedText, links) = attributedTextForHTMLNode(body)
self.links = links
let mutAttrString = NSMutableAttributedString(attributedString: attributedText)
mutAttrString.addAttribute(.font, value: font, range: NSRange(location: 0, length: mutAttrString.length))
self.attributedText = mutAttrString
}
private func attributedTextForHTMLNode(_ 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) = attributedTextForHTMLNode(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))")
}
}
2018-08-26 19:19:54 +00:00
// MARK: - Text Storage
2018-08-26 18:49:22 +00:00
private func updateTextStorage() {
2018-08-26 19:19:54 +00:00
if _customizing { return }
2018-08-26 18:49:22 +00:00
guard let attributedText = attributedText,
attributedText.length > 0 else {
links = [:]
textStorage.setAttributedString(NSAttributedString())
setNeedsDisplay()
return
}
textStorage.setAttributedString(attributedText)
2018-08-26 18:49:22 +00:00
_customizing = true
text = attributedText.string
_customizing = false
setNeedsDisplay()
}
2018-08-26 19:19:54 +00:00
// MARK: - Interaction
private func onTouch(_ touch: UITouch) -> Bool {
let location = touch.location(in: self)
var avoidSuperCall = false
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
}
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)
2018-08-26 18:49:22 +00:00
2018-08-26 19:19:54 +00:00
attributes[.foregroundColor] = isSelected ? nil : UIColor.blue
2018-08-26 18:49:22 +00:00
2018-08-26 19:19:54 +00:00
textStorage.addAttributes(attributes, range: selectedLink.range)
2018-08-26 18:49:22 +00:00
2018-08-26 19:19:54 +00:00
setNeedsDisplay()
2018-08-26 18:49:22 +00:00
}
}