mirror of
https://github.com/MessageKit/MessageKit.git
synced 2026-02-06 19:03:19 +00:00
447 lines
15 KiB
Swift
447 lines
15 KiB
Swift
/*
|
|
MIT License
|
|
|
|
Copyright (c) 2017-2018 MessageKit
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
*/
|
|
|
|
import UIKit
|
|
|
|
open class MessageLabel: UILabel {
|
|
|
|
// MARK: - Private Properties
|
|
|
|
private lazy var layoutManager: NSLayoutManager = {
|
|
let layoutManager = NSLayoutManager()
|
|
layoutManager.addTextContainer(self.textContainer)
|
|
return layoutManager
|
|
}()
|
|
|
|
private lazy var textContainer: NSTextContainer = {
|
|
let textContainer = NSTextContainer()
|
|
textContainer.lineFragmentPadding = 0
|
|
textContainer.maximumNumberOfLines = self.numberOfLines
|
|
textContainer.lineBreakMode = self.lineBreakMode
|
|
textContainer.size = self.bounds.size
|
|
return textContainer
|
|
}()
|
|
|
|
private lazy var textStorage: NSTextStorage = {
|
|
let textStorage = NSTextStorage()
|
|
textStorage.addLayoutManager(self.layoutManager)
|
|
return textStorage
|
|
}()
|
|
|
|
private lazy var rangesForDetectors: [DetectorType: [(NSRange, MessageTextCheckingType)]] = [:]
|
|
|
|
private var isConfiguring: Bool = false
|
|
|
|
// MARK: - Public Properties
|
|
|
|
open weak var delegate: MessageLabelDelegate?
|
|
|
|
open var enabledDetectors: [DetectorType] = [] {
|
|
didSet {
|
|
setTextStorage(attributedText, shouldParse: true)
|
|
}
|
|
}
|
|
|
|
open override var attributedText: NSAttributedString? {
|
|
didSet {
|
|
setTextStorage(attributedText, shouldParse: true)
|
|
}
|
|
}
|
|
|
|
open override var text: String? {
|
|
didSet {
|
|
setTextStorage(attributedText, shouldParse: true)
|
|
}
|
|
}
|
|
|
|
open override var font: UIFont! {
|
|
didSet {
|
|
setTextStorage(attributedText, shouldParse: false)
|
|
}
|
|
}
|
|
|
|
open override var textColor: UIColor! {
|
|
didSet {
|
|
setTextStorage(attributedText, shouldParse: false)
|
|
}
|
|
}
|
|
|
|
open override var lineBreakMode: NSLineBreakMode {
|
|
didSet {
|
|
textContainer.lineBreakMode = lineBreakMode
|
|
if !isConfiguring { setNeedsDisplay() }
|
|
}
|
|
}
|
|
|
|
open override var numberOfLines: Int {
|
|
didSet {
|
|
textContainer.maximumNumberOfLines = numberOfLines
|
|
if !isConfiguring { setNeedsDisplay() }
|
|
}
|
|
}
|
|
|
|
open override var textAlignment: NSTextAlignment {
|
|
didSet {
|
|
setTextStorage(attributedText, shouldParse: false)
|
|
}
|
|
}
|
|
|
|
open var textInsets: UIEdgeInsets = .zero {
|
|
didSet {
|
|
if !isConfiguring { setNeedsDisplay() }
|
|
}
|
|
}
|
|
|
|
private var attributesNeedUpdate = false
|
|
|
|
public static var defaultAttributes: [NSAttributedStringKey: Any] = {
|
|
return [
|
|
NSAttributedStringKey.foregroundColor: UIColor.darkText,
|
|
NSAttributedStringKey.underlineStyle: NSUnderlineStyle.styleSingle.rawValue,
|
|
NSAttributedStringKey.underlineColor: UIColor.darkText
|
|
]
|
|
}()
|
|
|
|
open internal(set) var addressAttributes: [NSAttributedStringKey: Any] = defaultAttributes
|
|
|
|
open internal(set) var dateAttributes: [NSAttributedStringKey: Any] = defaultAttributes
|
|
|
|
open internal(set) var phoneNumberAttributes: [NSAttributedStringKey: Any] = defaultAttributes
|
|
|
|
open internal(set) var urlAttributes: [NSAttributedStringKey: Any] = defaultAttributes
|
|
|
|
open internal(set) var transitInformationAttributes: [NSAttributedStringKey: Any] = defaultAttributes
|
|
|
|
public func setAttributes(_ attributes: [NSAttributedStringKey: Any], detector: DetectorType) {
|
|
switch detector {
|
|
case .phoneNumber:
|
|
phoneNumberAttributes = attributes
|
|
case .address:
|
|
addressAttributes = attributes
|
|
case .date:
|
|
dateAttributes = attributes
|
|
case .url:
|
|
urlAttributes = attributes
|
|
case .transitInformation:
|
|
transitInformationAttributes = attributes
|
|
}
|
|
if isConfiguring {
|
|
attributesNeedUpdate = true
|
|
} else {
|
|
updateAttributes(for: [detector])
|
|
}
|
|
}
|
|
|
|
// MARK: - Initializers
|
|
|
|
public override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
self.numberOfLines = 0
|
|
self.lineBreakMode = .byWordWrapping
|
|
}
|
|
|
|
public required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// MARK: - Open Methods
|
|
|
|
open override func drawText(in rect: CGRect) {
|
|
|
|
let insetRect = UIEdgeInsetsInsetRect(rect, textInsets)
|
|
textContainer.size = CGSize(width: insetRect.width, height: rect.height)
|
|
|
|
let origin = insetRect.origin
|
|
let range = layoutManager.glyphRange(for: textContainer)
|
|
|
|
layoutManager.drawBackground(forGlyphRange: range, at: origin)
|
|
layoutManager.drawGlyphs(forGlyphRange: range, at: origin)
|
|
}
|
|
|
|
// MARK: - Public Methods
|
|
|
|
public func configure(block: () -> Void) {
|
|
isConfiguring = true
|
|
block()
|
|
if attributesNeedUpdate {
|
|
updateAttributes(for: enabledDetectors)
|
|
}
|
|
attributesNeedUpdate = false
|
|
isConfiguring = false
|
|
setNeedsDisplay()
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
|
|
private func setTextStorage(_ newText: NSAttributedString?, shouldParse: Bool) {
|
|
|
|
guard let newText = newText, newText.length > 0 else {
|
|
textStorage.setAttributedString(NSAttributedString())
|
|
setNeedsDisplay()
|
|
return
|
|
}
|
|
|
|
let style = paragraphStyle(for: newText)
|
|
let range = NSRange(location: 0, length: newText.length)
|
|
|
|
let mutableText = NSMutableAttributedString(attributedString: newText)
|
|
mutableText.addAttribute(.paragraphStyle, value: style, range: range)
|
|
|
|
if shouldParse {
|
|
rangesForDetectors.removeAll()
|
|
let results = parse(text: mutableText)
|
|
setRangesForDetectors(in: results)
|
|
}
|
|
|
|
for (detector, rangeTuples) in rangesForDetectors {
|
|
if enabledDetectors.contains(detector) {
|
|
let attributes = detectorAttributes(for: detector)
|
|
rangeTuples.forEach { (range, _) in
|
|
mutableText.addAttributes(attributes, range: range)
|
|
}
|
|
}
|
|
}
|
|
|
|
let modifiedText = NSAttributedString(attributedString: mutableText)
|
|
textStorage.setAttributedString(modifiedText)
|
|
|
|
if !isConfiguring { setNeedsDisplay() }
|
|
|
|
}
|
|
|
|
private func paragraphStyle(for text: NSAttributedString) -> NSParagraphStyle {
|
|
guard text.length > 0 else { return NSParagraphStyle() }
|
|
|
|
var range = NSRange(location: 0, length: text.length)
|
|
let existingStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: &range) as? NSMutableParagraphStyle
|
|
let style = existingStyle ?? NSMutableParagraphStyle()
|
|
|
|
style.lineBreakMode = lineBreakMode
|
|
style.alignment = textAlignment
|
|
|
|
return style
|
|
}
|
|
|
|
private func updateAttributes(for detectors: [DetectorType]) {
|
|
|
|
guard let attributedText = attributedText, attributedText.length > 0 else { return }
|
|
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText)
|
|
|
|
for detector in detectors {
|
|
guard let rangeTuples = rangesForDetectors[detector] else { continue }
|
|
|
|
for (range, _) in rangeTuples {
|
|
let attributes = detectorAttributes(for: detector)
|
|
mutableAttributedString.addAttributes(attributes, range: range)
|
|
}
|
|
|
|
let updatedString = NSAttributedString(attributedString: mutableAttributedString)
|
|
textStorage.setAttributedString(updatedString)
|
|
}
|
|
}
|
|
|
|
private func detectorAttributes(for detectorType: DetectorType) -> [NSAttributedStringKey: Any] {
|
|
|
|
switch detectorType {
|
|
case .address:
|
|
return addressAttributes
|
|
case .date:
|
|
return dateAttributes
|
|
case .phoneNumber:
|
|
return phoneNumberAttributes
|
|
case .url:
|
|
return urlAttributes
|
|
case .transitInformation:
|
|
return transitInformationAttributes
|
|
}
|
|
|
|
}
|
|
|
|
private func detectorAttributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [NSAttributedStringKey: Any] {
|
|
switch checkingResultType {
|
|
case .address:
|
|
return addressAttributes
|
|
case .date:
|
|
return dateAttributes
|
|
case .phoneNumber:
|
|
return phoneNumberAttributes
|
|
case .link:
|
|
return urlAttributes
|
|
case .transitInformation:
|
|
return transitInformationAttributes
|
|
default:
|
|
fatalError(MessageKitError.unrecognizedCheckingResult)
|
|
}
|
|
}
|
|
|
|
// MARK: - Parsing Text
|
|
|
|
private func parse(text: NSAttributedString) -> [NSTextCheckingResult] {
|
|
guard enabledDetectors.isEmpty == false else { return [] }
|
|
let checkingTypes = enabledDetectors.reduce(0) { $0 | $1.textCheckingType.rawValue }
|
|
let detector = try? NSDataDetector(types: checkingTypes)
|
|
let range = NSRange(location: 0, length: text.length)
|
|
return detector?.matches(in: text.string, options: [], range: range) ?? []
|
|
}
|
|
|
|
private func setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) {
|
|
|
|
guard checkingResults.isEmpty == false else { return }
|
|
|
|
for result in checkingResults {
|
|
|
|
switch result.resultType {
|
|
case .address:
|
|
var ranges = rangesForDetectors[.address] ?? []
|
|
let tuple: (NSRange, MessageTextCheckingType) = (result.range, .addressComponents(result.addressComponents))
|
|
ranges.append(tuple)
|
|
rangesForDetectors.updateValue(ranges, forKey: .address)
|
|
case .date:
|
|
var ranges = rangesForDetectors[.date] ?? []
|
|
let tuple: (NSRange, MessageTextCheckingType) = (result.range, .date(result.date))
|
|
ranges.append(tuple)
|
|
rangesForDetectors.updateValue(ranges, forKey: .date)
|
|
case .phoneNumber:
|
|
var ranges = rangesForDetectors[.phoneNumber] ?? []
|
|
let tuple: (NSRange, MessageTextCheckingType) = (result.range, .phoneNumber(result.phoneNumber))
|
|
ranges.append(tuple)
|
|
rangesForDetectors.updateValue(ranges, forKey: .phoneNumber)
|
|
case .link:
|
|
var ranges = rangesForDetectors[.url] ?? []
|
|
let tuple: (NSRange, MessageTextCheckingType) = (result.range, .link(result.url))
|
|
ranges.append(tuple)
|
|
rangesForDetectors.updateValue(ranges, forKey: .url)
|
|
case .transitInformation:
|
|
var ranges = rangesForDetectors[.transitInformation] ?? []
|
|
let tuple: (NSRange, MessageTextCheckingType) = (result.range, .transitInfoComponents(result.components))
|
|
ranges.append(tuple)
|
|
rangesForDetectors.updateValue(ranges, forKey: .transitInformation)
|
|
|
|
default:
|
|
fatalError("Received an unrecognized NSTextCheckingResult.CheckingType")
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Gesture Handling
|
|
|
|
private func stringIndex(at location: CGPoint) -> Int? {
|
|
guard textStorage.length > 0 else { return nil }
|
|
|
|
var location = location
|
|
let textOffset = CGPoint(x: textInsets.left, y: textInsets.right)
|
|
|
|
location.x -= textOffset.x
|
|
location.y -= textOffset.y
|
|
|
|
let index = layoutManager.glyphIndex(for: location, in: textContainer)
|
|
|
|
let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: index, effectiveRange: nil)
|
|
|
|
var characterIndex: Int?
|
|
|
|
if lineRect.contains(location) {
|
|
characterIndex = layoutManager.characterIndexForGlyph(at: index)
|
|
}
|
|
|
|
return characterIndex
|
|
|
|
}
|
|
|
|
func handleGesture(_ touchLocation: CGPoint) -> Bool {
|
|
|
|
guard let index = stringIndex(at: touchLocation) else { return false }
|
|
|
|
for (detectorType, ranges) in rangesForDetectors {
|
|
for (range, value) in ranges {
|
|
if range.contains(index) {
|
|
handleGesture(for: detectorType, value: value)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
private func handleGesture(for detectorType: DetectorType, value: MessageTextCheckingType) {
|
|
|
|
switch value {
|
|
case let .addressComponents(addressComponents):
|
|
var transformedAddressComponents = [String: String]()
|
|
guard let addressComponents = addressComponents else { return }
|
|
addressComponents.forEach { (key, value) in
|
|
transformedAddressComponents[key.rawValue] = value
|
|
}
|
|
handleAddress(transformedAddressComponents)
|
|
case let .phoneNumber(phoneNumber):
|
|
guard let phoneNumber = phoneNumber else { return }
|
|
handlePhoneNumber(phoneNumber)
|
|
case let .date(date):
|
|
guard let date = date else { return }
|
|
handleDate(date)
|
|
case let .link(url):
|
|
guard let url = url else { return }
|
|
handleURL(url)
|
|
case let .transitInfoComponents(transitInformation):
|
|
var transformedTransitInformation = [String: String]()
|
|
guard let transitInformation = transitInformation else { return }
|
|
transitInformation.forEach { (key, value) in
|
|
transformedTransitInformation[key.rawValue] = value
|
|
}
|
|
handleTransitInformation(transformedTransitInformation)
|
|
}
|
|
}
|
|
|
|
private func handleAddress(_ addressComponents: [String: String]) {
|
|
delegate?.didSelectAddress(addressComponents)
|
|
}
|
|
|
|
private func handleDate(_ date: Date) {
|
|
delegate?.didSelectDate(date)
|
|
}
|
|
|
|
private func handleURL(_ url: URL) {
|
|
delegate?.didSelectURL(url)
|
|
}
|
|
|
|
private func handlePhoneNumber(_ phoneNumber: String) {
|
|
delegate?.didSelectPhoneNumber(phoneNumber)
|
|
}
|
|
|
|
private func handleTransitInformation(_ components: [String: String]) {
|
|
delegate?.didSelectTransitInformation(components)
|
|
}
|
|
|
|
}
|
|
|
|
private enum MessageTextCheckingType {
|
|
case addressComponents([NSTextCheckingKey: String]?)
|
|
case date(Date?)
|
|
case phoneNumber(String?)
|
|
case link(URL?)
|
|
case transitInfoComponents([NSTextCheckingKey: String]?)
|
|
}
|