mirror of
https://github.com/MessageKit/MessageKit.git
synced 2026-02-06 19:03:19 +00:00
dde838630a
Signed-off-by: Candost Dagdeviren <candostdagdeviren@gmail.com>
466 lines
16 KiB
Swift
466 lines
16 KiB
Swift
/*
|
|
MIT License
|
|
|
|
Copyright (c) 2017 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, UIGestureRecognizerDelegate {
|
|
|
|
// 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, Any?)]] = [:]
|
|
|
|
// MARK: - Public Properties
|
|
|
|
open weak var delegate: MessageLabelDelegate?
|
|
|
|
open var enabledDetectors: [DetectorType] = [.phoneNumber, .address, .date, .url]
|
|
|
|
open override var attributedText: NSAttributedString? {
|
|
didSet {
|
|
guard attributedText != oldValue else { return }
|
|
setTextStorage()
|
|
}
|
|
}
|
|
|
|
open override var text: String? {
|
|
didSet {
|
|
guard text != oldValue else { return }
|
|
setTextStorage()
|
|
}
|
|
}
|
|
|
|
open override var font: UIFont! {
|
|
didSet {
|
|
guard font != oldValue else { return }
|
|
guard let attributedText = attributedText else { return }
|
|
textStorage.setAttributedString(attributedText)
|
|
setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
open override var textColor: UIColor! {
|
|
didSet {
|
|
guard textColor != oldValue else { return }
|
|
guard let attributedText = attributedText else { return }
|
|
textStorage.setAttributedString(attributedText)
|
|
setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
open override var lineBreakMode: NSLineBreakMode {
|
|
didSet {
|
|
guard lineBreakMode != oldValue else { return }
|
|
textContainer.lineBreakMode = lineBreakMode
|
|
setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
open override var numberOfLines: Int {
|
|
didSet {
|
|
guard numberOfLines != oldValue else { return }
|
|
textContainer.maximumNumberOfLines = numberOfLines
|
|
setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
open override var textAlignment: NSTextAlignment {
|
|
didSet {
|
|
guard textAlignment != oldValue else { return }
|
|
setTextStorage()
|
|
}
|
|
}
|
|
|
|
open var textInsets: UIEdgeInsets = .zero {
|
|
didSet {
|
|
guard textInsets != oldValue else { return }
|
|
setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
open var addressAttributes: [NSAttributedStringKey: Any] = [:] {
|
|
didSet {
|
|
updateAttributes(for: .address)
|
|
setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
open var dateAttributes: [NSAttributedStringKey: Any] = [:] {
|
|
didSet {
|
|
updateAttributes(for: .date)
|
|
setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
open var phoneNumberAttributes: [NSAttributedStringKey: Any] = [:] {
|
|
didSet {
|
|
updateAttributes(for: .phoneNumber)
|
|
setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
open var urlAttributes: [NSAttributedStringKey: Any] = [:] {
|
|
didSet {
|
|
updateAttributes(for: .url)
|
|
setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
// MARK: - Initializers
|
|
|
|
public override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
// Message Label Specific
|
|
self.numberOfLines = 0
|
|
self.lineBreakMode = .byWordWrapping
|
|
|
|
let defaultAttributes: [NSAttributedStringKey: Any] = [
|
|
NSAttributedStringKey.foregroundColor: self.textColor,
|
|
NSAttributedStringKey.underlineStyle: NSUnderlineStyle.styleSingle.rawValue,
|
|
NSAttributedStringKey.underlineColor: self.textColor
|
|
]
|
|
|
|
self.addressAttributes = defaultAttributes
|
|
self.dateAttributes = defaultAttributes
|
|
self.phoneNumberAttributes = defaultAttributes
|
|
self.urlAttributes = defaultAttributes
|
|
|
|
setupGestureRecognizers()
|
|
|
|
}
|
|
|
|
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
|
|
|
|
// MARK: UIGestureRecognizer Delegate
|
|
|
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
return true
|
|
}
|
|
|
|
//swiftlint:disable cyclomatic_complexity
|
|
// Yeah we're disabling this because the whole file is a mess :D
|
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
|
|
let touchLocation = touch.location(in: self)
|
|
|
|
switch true {
|
|
case gestureRecognizer.view != self.superview && gestureRecognizer.view != self:
|
|
return true
|
|
case gestureRecognizer.view == self.superview:
|
|
guard let index = stringIndex(at: touchLocation) else { return true }
|
|
for (_, ranges) in rangesForDetectors {
|
|
for (nsRange, _) in ranges {
|
|
guard let range = Range(nsRange) else { return true }
|
|
if range.contains(index) { return false }
|
|
}
|
|
}
|
|
return true
|
|
case gestureRecognizer.view == self:
|
|
guard let index = stringIndex(at: touchLocation) else { return false }
|
|
for (_, ranges) in rangesForDetectors {
|
|
for (nsRange, _) in ranges {
|
|
guard let range = Range(nsRange) else { return false }
|
|
if range.contains(index) { return true }
|
|
}
|
|
}
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
|
|
private func setTextStorage() {
|
|
|
|
// Anytime we update the text storage we need to clear the previous ranges
|
|
rangesForDetectors.removeAll()
|
|
|
|
guard let attributedText = attributedText, attributedText.length > 0 else {
|
|
textStorage.setAttributedString(NSAttributedString())
|
|
setNeedsDisplay()
|
|
return
|
|
}
|
|
|
|
guard let checkingResults = parse(text: attributedText, for: enabledDetectors), checkingResults.isEmpty == false else {
|
|
let textWithParagraphAttributes = addParagraphStyleAttribute(to: attributedText)
|
|
textStorage.setAttributedString(textWithParagraphAttributes)
|
|
setNeedsDisplay()
|
|
return
|
|
}
|
|
|
|
setRangesForDetectors(in: checkingResults)
|
|
|
|
let textWithDetectorAttributes = addDetectorAttributes(to: attributedText, for: checkingResults)
|
|
let textWithParagraphAttributes = addParagraphStyleAttribute(to: textWithDetectorAttributes)
|
|
|
|
textStorage.setAttributedString(textWithParagraphAttributes)
|
|
|
|
setNeedsDisplay()
|
|
|
|
}
|
|
|
|
private func addParagraphStyleAttribute(to text: NSAttributedString) -> NSAttributedString {
|
|
|
|
let mutableAttributedString = NSMutableAttributedString(attributedString: text)
|
|
var textRange = NSRange(location: 0, length: 0)
|
|
|
|
let paragraphStyle = text.attribute(NSAttributedStringKey.paragraphStyle, at: 0, effectiveRange: &textRange) as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
|
|
paragraphStyle.lineBreakMode = lineBreakMode
|
|
paragraphStyle.alignment = textAlignment
|
|
|
|
mutableAttributedString.addAttribute(NSAttributedStringKey.paragraphStyle, value: paragraphStyle, range: textRange)
|
|
|
|
return mutableAttributedString
|
|
|
|
}
|
|
|
|
private func addDetectorAttributes(to text: NSAttributedString, for checkingResults: [NSTextCheckingResult]) -> NSAttributedString {
|
|
|
|
let mutableAttributedString = NSMutableAttributedString(attributedString: text)
|
|
|
|
checkingResults.forEach { result in
|
|
let attributes = detectorAttributes(for: result.resultType)
|
|
mutableAttributedString.addAttributes(attributes, range: result.range)
|
|
}
|
|
|
|
return mutableAttributedString
|
|
}
|
|
|
|
private func updateAttributes(for detectorType: DetectorType) {
|
|
|
|
guard let attributedText = attributedText, attributedText.length > 0 else { return }
|
|
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText)
|
|
|
|
guard let ranges = rangesForDetectors[detectorType] else { return }
|
|
|
|
ranges.forEach { (range, _) in
|
|
let attributes = detectorAttributes(for: detectorType)
|
|
mutableAttributedString.addAttributes(attributes, range: range)
|
|
}
|
|
|
|
textStorage.setAttributedString(mutableAttributedString)
|
|
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
}
|
|
|
|
private func detectorAttributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [NSAttributedStringKey: Any] {
|
|
switch checkingResultType {
|
|
case NSTextCheckingResult.CheckingType.address:
|
|
return addressAttributes
|
|
case NSTextCheckingResult.CheckingType.date:
|
|
return dateAttributes
|
|
case NSTextCheckingResult.CheckingType.phoneNumber:
|
|
return phoneNumberAttributes
|
|
case NSTextCheckingResult.CheckingType.link:
|
|
return urlAttributes
|
|
default:
|
|
fatalError("Received an unrecognized NSTextCheckingResult.CheckingType")
|
|
}
|
|
}
|
|
|
|
// MARK: - Parsing Text
|
|
|
|
private func parse(text: NSAttributedString, for detectorTypes: [DetectorType]) -> [NSTextCheckingResult]? {
|
|
guard detectorTypes.isEmpty == false else { return nil }
|
|
let checkingTypes = detectorTypes.reduce(0) { $0 | $1.textCheckingType.rawValue }
|
|
let detector = try? NSDataDetector(types: checkingTypes)
|
|
|
|
return detector?.matches(in: text.string, options: [], range: NSRange(location: 0, length: text.length))
|
|
}
|
|
|
|
private func setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) {
|
|
|
|
for result in checkingResults {
|
|
|
|
switch result.resultType {
|
|
case NSTextCheckingResult.CheckingType.address:
|
|
var ranges = rangesForDetectors[.address] ?? []
|
|
let tuple = (result.range, result.addressComponents) as (NSRange, Any?)
|
|
ranges.append(tuple)
|
|
rangesForDetectors.updateValue(ranges, forKey: .address)
|
|
case NSTextCheckingResult.CheckingType.date:
|
|
var ranges = rangesForDetectors[.date] ?? []
|
|
let tuple = (result.range, result.date) as (NSRange, Any?)
|
|
ranges.append(tuple)
|
|
rangesForDetectors.updateValue(ranges, forKey: .date)
|
|
case NSTextCheckingResult.CheckingType.phoneNumber:
|
|
var ranges = rangesForDetectors[.phoneNumber] ?? []
|
|
let tuple = (result.range, result.phoneNumber) as (NSRange, Any?)
|
|
ranges.append(tuple)
|
|
rangesForDetectors.updateValue(ranges, forKey: .phoneNumber)
|
|
case NSTextCheckingResult.CheckingType.link:
|
|
var ranges = rangesForDetectors[.url] ?? []
|
|
let tuple = (result.range, result.url) as (NSRange, Any?)
|
|
ranges.append(tuple)
|
|
rangesForDetectors.updateValue(ranges, forKey: .url)
|
|
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 glyphIndex = layoutManager.glyphIndex(for: location, in: textContainer)
|
|
|
|
let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: glyphIndex, effectiveRange: nil)
|
|
|
|
if lineRect.contains(location) {
|
|
return layoutManager.characterIndexForGlyph(at: glyphIndex)
|
|
} else {
|
|
return nil
|
|
}
|
|
|
|
}
|
|
|
|
private func setupGestureRecognizers() {
|
|
|
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
|
|
addGestureRecognizer(tapGesture)
|
|
tapGesture.delegate = self
|
|
|
|
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
|
|
addGestureRecognizer(longPressGesture)
|
|
tapGesture.delegate = self
|
|
|
|
isUserInteractionEnabled = true
|
|
}
|
|
|
|
@objc func handleGesture(_ gesture: UIGestureRecognizer) {
|
|
|
|
let touchLocation = gesture.location(ofTouch: 0, in: self)
|
|
guard let index = stringIndex(at: touchLocation) else { return }
|
|
|
|
for (detectorType, ranges) in rangesForDetectors {
|
|
for (nsRange, value) in ranges {
|
|
guard let range = Range(nsRange) else { return }
|
|
if range.contains(index) {
|
|
handleGesture(for: detectorType, value: value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleGesture(for detectorType: DetectorType, value: Any?) {
|
|
|
|
switch detectorType {
|
|
case .address:
|
|
guard let addressComponents = value as? [String: String] else { return }
|
|
handleAddress(addressComponents)
|
|
case .phoneNumber:
|
|
guard let phoneNumber = value as? String else { return }
|
|
handlePhoneNumber(phoneNumber)
|
|
case .date:
|
|
guard let date = value as? Date else { return }
|
|
handleDate(date)
|
|
case .url:
|
|
guard let url = value as? URL else { return }
|
|
handleURL(url)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
}
|