[DetectorType] add .mention, .hashtag and .custon() to detect your own pattern

[DetectorType] Put mention and hashtag as var

[CHANGELOG] Update

[DetectorType] Update to have at least 4 characters and more

[CHANGELOG] Update

[DetectorType]

[MessageLabel] run NSDataDetector once

[MessageLabel] Run NSDataDetector once

[Testing] Add testing and pass rangesOfDetectors as internal

[Refactoring]

[Filter] only .custom

[Examples]
This commit is contained in:
Julien K
2018-10-26 12:15:27 +02:00
parent 095abe0223
commit 77e9ccda46
8 changed files with 284 additions and 20 deletions
+3
View File
@@ -15,6 +15,9 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa
### Added
- **Breaking Change** Added `.hashtag`, .`mention` to detect theses pattern inside the `messageLabel`. We also add `.custom(pattern: YOUR_PATTERN)` to `DetectorType` to manage and deal with your own regular expression.
[#913](https://github.com/MessageKit/MessageKit/pull/913) by [@JulienKode](https://github.com/julienkode).
- Added support for detection and handling of `NSLink`s inside of messages.
[#815](https://github.com/MessageKit/MessageKit/pull/815) by [@jnic](https://github.com/jnic)
@@ -46,7 +46,7 @@ final internal class SampleData {
let messageImages: [UIImage] = [#imageLiteral(resourceName: "img1"), #imageLiteral(resourceName: "img2")]
let messageTypes = ["Text", "Text", "Text", "AttributedText", "Location", "Photo", "Emoji", "Video", "URL", "Phone", "Custom"]
let messageTypes = ["Text", "Text", "Text", "AttributedText", "Location", "Photo", "Emoji", "Video", "URL", "Phone", "Mention", "Hashtag", "Custom"]
let emojis = [
"👍",
@@ -150,6 +150,10 @@ final internal class SampleData {
return MockMessage(text: "https://github.com/MessageKit", sender: sender, messageId: uniqueID, date: date)
case "Phone":
return MockMessage(text: "123-456-7890", sender: sender, messageId: uniqueID, date: date)
case "Mention":
return MockMessage(text: "@messagekit", sender: sender, messageId: uniqueID, date: date)
case "Hashtag":
return MockMessage(text: "#messagekit", sender: sender, messageId: uniqueID, date: date)
case "Custom":
return MockMessage(custom: "Someone left the conversation", sender: system, messageId: uniqueID, date: date)
default:
@@ -258,11 +258,14 @@ extension AdvancedExampleViewController: MessagesDisplayDelegate {
}
func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] {
return MessageLabel.defaultAttributes
switch detector {
case .hashtag, .mention: return [.foregroundColor: UIColor.blue]
default: return MessageLabel.defaultAttributes
}
}
func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] {
return [.url, .address, .phoneNumber, .date, .transitInformation]
return [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag]
}
// MARK: - All Messages
@@ -49,11 +49,14 @@ extension BasicExampleViewController: MessagesDisplayDelegate {
}
func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] {
return MessageLabel.defaultAttributes
switch detector {
case .hashtag, .mention: return [.foregroundColor: UIColor.blue]
default: return MessageLabel.defaultAttributes
}
}
func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] {
return [.url, .address, .phoneNumber, .date, .transitInformation]
return [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag]
}
// MARK: - All Messages
+32 -6
View File
@@ -24,19 +24,19 @@
import Foundation
public enum DetectorType {
public enum DetectorType: Hashable {
case address
case date
case phoneNumber
case url
case transitInformation
case custom(regex: NSRegularExpression)
// MARK: - Not supported yet
//case mention
//case hashtag
//case custom
// swiftlint:disable force_try
public static var hashtag = DetectorType.custom(regex: try! NSRegularExpression(pattern: "#[a-zA-Z0-9]{4,}", options: []))
public static var mention = DetectorType.custom(regex: try! NSRegularExpression(pattern: "@[a-zA-Z0-9]{4,}", options: []))
// swiftlint:enable force_try
internal var textCheckingType: NSTextCheckingResult.CheckingType {
switch self {
@@ -45,6 +45,32 @@ public enum DetectorType {
case .phoneNumber: return .phoneNumber
case .url: return .link
case .transitInformation: return .transitInformation
case .custom: return .regularExpression
}
}
/// Simply check if the detector type is a .custom
public var isCustom: Bool {
switch self {
case .custom: return true
default: return false
}
}
///The hashValue of the `DetectorType` so we can conform to `Hashable` and be sorted.
public var hashValue: Int {
return self.toInt()
}
/// Return an 'Int' value for each `DetectorType` type so `DetectorType` can conform to `Hashable`
private func toInt() -> Int {
switch self {
case .address: return 0
case .date: return 1
case .phoneNumber: return 2
case .url: return 3
case .transitInformation: return 4
case .custom(let regex): return regex.hashValue
}
}
@@ -56,6 +56,25 @@ public protocol MessageLabelDelegate: AnyObject {
/// - Parameters:
/// - transitInformation: The selected transit information.
func didSelectTransitInformation(_ transitInformation: [String: String])
/// Triggered when a tap occurs on a mention
///
/// - Parameters:
/// - mention: The selected mention
func didSelectMention(_ mention: String)
/// Triggered when a tap occurs on a hashtag
///
/// - Parameters:
/// - mention: The selected hashtag
func didSelectHashtag(_ hashtag: String)
/// Triggered when a tap occurs on a custom regular expression
///
/// - Parameters:
/// - pattern: the pattern of the regular expression
/// - match: part that match with the regular expression
func didSelectCustom(_ pattern: String, match: String?)
}
@@ -71,4 +90,10 @@ public extension MessageLabelDelegate {
func didSelectTransitInformation(_ transitInformation: [String: String]) {}
func didSelectMention(_ mention: String) {}
func didSelectHashtag(_ hashtag: String) {}
func didSelectCustom(_ pattern: String, match: String?) {}
}
+86 -7
View File
@@ -49,7 +49,7 @@ open class MessageLabel: UILabel {
return textStorage
}()
private lazy var rangesForDetectors: [DetectorType: [(NSRange, MessageTextCheckingType)]] = [:]
internal lazy var rangesForDetectors: [DetectorType: [(NSRange, MessageTextCheckingType)]] = [:]
private var isConfiguring: Bool = false
@@ -134,6 +134,12 @@ open class MessageLabel: UILabel {
open internal(set) var urlAttributes: [NSAttributedString.Key: Any] = defaultAttributes
open internal(set) var transitInformationAttributes: [NSAttributedString.Key: Any] = defaultAttributes
open internal(set) var hashtagAttributes: [NSAttributedString.Key: Any] = defaultAttributes
open internal(set) var mentionAttributes: [NSAttributedString.Key: Any] = defaultAttributes
open internal(set) var customAttributes: [NSRegularExpression: [NSAttributedString.Key: Any]] = [:]
public func setAttributes(_ attributes: [NSAttributedString.Key: Any], detector: DetectorType) {
switch detector {
@@ -147,6 +153,12 @@ open class MessageLabel: UILabel {
urlAttributes = attributes
case .transitInformation:
transitInformationAttributes = attributes
case .mention:
mentionAttributes = attributes
case .hashtag:
hashtagAttributes = attributes
case .custom(let regex):
customAttributes[regex] = attributes
}
if isConfiguring {
attributesNeedUpdate = true
@@ -276,6 +288,12 @@ open class MessageLabel: UILabel {
return urlAttributes
case .transitInformation:
return transitInformationAttributes
case .mention:
return mentionAttributes
case .hashtag:
return hashtagAttributes
case .custom(let regex):
return customAttributes[regex] ?? MessageLabel.defaultAttributes
}
}
@@ -301,10 +319,24 @@ open class MessageLabel: UILabel {
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)
let matches = detector?.matches(in: text.string, options: [], range: range) ?? []
var matches = [NSTextCheckingResult]()
// Get matches of all .custom DetectorType and add it to matches array
let regexs = enabledDetectors
.filter { $0.isCustom }
.map { parseForMatches(with: $0, in: text, for: range) }
.joined()
matches.append(contentsOf: regexs)
// Get all Checking Types of detectors, except for .custom because they contain their own regex
let detectorCheckingTypes = enabledDetectors
.filter{ !$0.isCustom }
.reduce(0) { $0 | $1.textCheckingType.rawValue }
if detectorCheckingTypes > 0, let detector = try? NSDataDetector(types: detectorCheckingTypes) {
let detectorMatches = detector.matches(in: text.string, options: [], range: range)
matches.append(contentsOf: detectorMatches)
}
guard enabledDetectors.contains(.url) else {
return matches
@@ -322,6 +354,24 @@ open class MessageLabel: UILabel {
return results
}
/**
Take a custom detector and apply the matching to the text
- Parameters: detector: `DetectorType` that you want to execute
- Parameters: text: where we apply the regular expression
- Parameters: range: where we apply the regular expression
- Returns: an array of `NSTextCheckingResult` that contains all maching for this detector or nil if it fails
*/
private func parseForMatches(with detector: DetectorType, in text: NSAttributedString, for range: NSRange) -> [NSTextCheckingResult] {
switch detector {
case .custom(let regex):
return regex.matches(in: text.string, options: [], range: range)
default:
fatalError("You must pass a .custom DetectorType")
}
}
private func setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) {
guard checkingResults.isEmpty == false else { return }
@@ -354,7 +404,13 @@ open class MessageLabel: UILabel {
let tuple: (NSRange, MessageTextCheckingType) = (result.range, .transitInfoComponents(result.components))
ranges.append(tuple)
rangesForDetectors.updateValue(ranges, forKey: .transitInformation)
case .regularExpression:
guard let text = text, let regex = result.regularExpression, let range = Range(result.range, in: text) else { return }
let detector = DetectorType.custom(regex: regex)
var ranges = rangesForDetectors[detector] ?? []
let tuple: (NSRange, MessageTextCheckingType) = (result.range, .custom(pattern: regex.pattern, match: String(text[range])))
ranges.append(tuple)
rangesForDetectors.updateValue(ranges, forKey: detector)
default:
fatalError("Received an unrecognized NSTextCheckingResult.CheckingType")
}
@@ -428,6 +484,16 @@ open class MessageLabel: UILabel {
transformedTransitInformation[key.rawValue] = value
}
handleTransitInformation(transformedTransitInformation)
case let .custom(pattern, match):
guard let match = match else { return }
switch detectorType {
case .hashtag:
handleHashtag(match)
case .mention:
handleHashtag(match)
default:
handleCustom(pattern, match: match)
}
}
}
@@ -450,13 +516,26 @@ open class MessageLabel: UILabel {
private func handleTransitInformation(_ components: [String: String]) {
delegate?.didSelectTransitInformation(components)
}
private func handleHashtag(_ hashtag: String) {
delegate?.didSelectHashtag(hashtag)
}
private func handleMention(_ mention: String) {
delegate?.didSelectMention(mention)
}
private func handleCustom(_ pattern: String, match: String) {
delegate?.didSelectCustom(pattern, match: match)
}
}
private enum MessageTextCheckingType {
internal enum MessageTextCheckingType {
case addressComponents([NSTextCheckingKey: String]?)
case date(Date?)
case phoneNumber(String?)
case link(URL?)
case transitInfoComponents([NSTextCheckingKey: String]?)
case custom(pattern: String, match: String?)
}
+123 -2
View File
@@ -38,7 +38,90 @@ final class MessageLabelSpec: QuickSpec {
messageLabel = MessageLabel()
}
// describe("text recognized by a DetectorType") {
describe("text recognized by a DetectorType") {
let mentionsList = ["@julienkode", "@facebook", "@google", "@1234"]
let hashtagsList = ["#julienkode", "#facebook", "#google", "#1234"]
var detector: DetectorType!
var key: NSAttributedString.Key!
var attributes: [NSAttributedString.Key: Any]!
context("Mention detection") {
beforeEach {
detector = DetectorType.mention
key = NSAttributedString.Key(rawValue: "Mention")
attributes = [key: "MentionDetected"]
}
it("match with multiples alpha and numerics") {
let text = mentionsList.joined(separator: " #test ")
self.set(text: text, and: [detector], with: attributes, to: messageLabel)
let matches = self.extractCustomDetectors(for: detector, with: messageLabel)
expect(matches).to(equal(mentionsList))
}
it("is invalid") {
let invalids = hashtagsList.joined(separator: " ")
self.set(text: invalids, and: [detector], with: attributes, to: messageLabel)
let matches = self.extractCustomDetectors(for: detector, with: messageLabel)
expect(matches.count).to(equal(0))
}
}
context("Hashtag detection") {
beforeEach {
detector = DetectorType.hashtag
key = NSAttributedString.Key(rawValue: "Hashtag")
attributes = [key: "HashtagDetected"]
}
it("match with multiples alpha and numerics") {
let text = hashtagsList.joined(separator: " @test ")
self.set(text: text, and: [detector], with: attributes, to: messageLabel)
let matches = self.extractCustomDetectors(for: detector, with: messageLabel)
expect(matches).to(equal(hashtagsList))
}
it("is invalid") {
let invalids = mentionsList.joined(separator: " ")
self.set(text: invalids, and: [detector], with: attributes, to: messageLabel)
let matches = self.extractCustomDetectors(for: detector, with: messageLabel)
expect(matches.count).to(equal(0))
}
}
context("Custom detection") {
let shouldPass = ["1234", "1", "09876"]
let shouldFailed = ["abcd", "a", "!!!", ";"]
beforeEach {
detector = DetectorType.custom(regex: try! NSRegularExpression(pattern: "[0-9]+", options: .caseInsensitive))
key = NSAttributedString.Key(rawValue: "Custom")
attributes = [key: "CustomDetected"]
}
it("must match with one or more numerics") {
let text = shouldPass.joined(separator: " ")
self.set(text: text, and: [detector], with: attributes, to: messageLabel)
let matches = self.extractCustomDetectors(for: detector, with: messageLabel)
expect(matches).to(equal(shouldPass))
}
it("must failed with any non numerics characters") {
let invalids = shouldFailed.joined(separator: " ")
self.set(text: invalids, and: [detector], with: attributes, to: messageLabel)
let matches = self.extractCustomDetectors(for: detector, with: messageLabel)
expect(matches.count).to(equal(0))
}
}
// context("address detection is enabled") {
// it("applies addressAttributes to text") {
// let expectedColor = UIColor.blue
@@ -83,7 +166,7 @@ final class MessageLabelSpec: QuickSpec {
// expect(textFont).to(equal(expectedFont))
// }
// }
// }
}
describe("the synchronization between text and attributedText") {
context("when attributedText is set to a non-nil value") {
@@ -237,6 +320,44 @@ final class MessageLabelSpec: QuickSpec {
}
}
}
// MARK: - Private helpers API for Detectors
/**
Takes a given `DetectorType` and extract matches from a `MessageLabel`
- Parameters: detector: `DetectorType` that you want to extract
- Parameters: label: `MessageLabel` where you want to get matches
- Returns: an array of `String` that contains all matches for the given detector
*/
private func extractCustomDetectors(for detector: DetectorType, with label: MessageLabel) -> [String] {
guard let detection = label.rangesForDetectors[detector] else { return [] }
return detection.compactMap ({ (range, messageChecking) -> String? in
switch messageChecking {
case .custom(_, let match):
return match
default:
return nil
}
})
}
/**
Simply set text, detectors and attriutes to a given label
- Parameters: text: `String` that will be applied to the label
- Parameters: detector: `DetectorType` that you want to apply to the label
- Parameters: attributes: `[NSAttributedString.Key: Any]` that you want to apply to the label
- Parameters: label: `MessageLabel` that takes the previous parameters
*/
private func set(text: String, and detectors: [DetectorType], with attributes: [NSAttributedString.Key: Any], to label: MessageLabel) {
label.mentionAttributes = attributes
label.enabledDetectors = detectors
label.text = text
}
}
// MARK: - Helpers