Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ActiveLabel.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
C1D15C791D7C9B610041D119 /* StringTrimExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D15C781D7C9B610041D119 /* StringTrimExtension.swift */; };
C1D15C7B1D7C9B7E0041D119 /* ActiveBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D15C7A1D7C9B7E0041D119 /* ActiveBuilder.swift */; };
C1E867D61C3D7AEA00FD687A /* RegexParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E867D51C3D7AEA00FD687A /* RegexParser.swift */; };
CE3BE94027F4F509006A3145 /* StringSubscriptExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3BE93E27F4F509006A3145 /* StringSubscriptExtension.swift */; };
CE3BE94127F4F509006A3145 /* Timestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3BE93F27F4F509006A3145 /* Timestamp.swift */; };
E267FA251DB3A34900EEAC4C /* ActiveLabel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F0249A21B9989B1005D8035 /* ActiveLabel.framework */; };
E267FA261DB3A34900EEAC4C /* ActiveLabel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8F0249A21B9989B1005D8035 /* ActiveLabel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -74,6 +76,8 @@
C1D15C781D7C9B610041D119 /* StringTrimExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringTrimExtension.swift; sourceTree = "<group>"; };
C1D15C7A1D7C9B7E0041D119 /* ActiveBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveBuilder.swift; sourceTree = "<group>"; };
C1E867D51C3D7AEA00FD687A /* RegexParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegexParser.swift; sourceTree = "<group>"; };
CE3BE93E27F4F509006A3145 /* StringSubscriptExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringSubscriptExtension.swift; sourceTree = "<group>"; };
CE3BE93F27F4F509006A3145 /* Timestamp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Timestamp.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -131,7 +135,9 @@
8F0249D21B998C00005D8035 /* ActiveLabel.swift */,
C1D15C7A1D7C9B7E0041D119 /* ActiveBuilder.swift */,
C1E867D51C3D7AEA00FD687A /* RegexParser.swift */,
CE3BE93E27F4F509006A3145 /* StringSubscriptExtension.swift */,
C1D15C781D7C9B610041D119 /* StringTrimExtension.swift */,
CE3BE93F27F4F509006A3145 /* Timestamp.swift */,
8F0249A71B9989B1005D8035 /* Info.plist */,
);
path = ActiveLabel;
Expand Down Expand Up @@ -305,6 +311,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CE3BE94127F4F509006A3145 /* Timestamp.swift in Sources */,
CE3BE94027F4F509006A3145 /* StringSubscriptExtension.swift in Sources */,
C1D15C791D7C9B610041D119 /* StringTrimExtension.swift in Sources */,
8F0249D31B998C00005D8035 /* ActiveLabel.swift in Sources */,
8F0249D51B998D21005D8035 /* ActiveType.swift in Sources */,
Expand Down
8 changes: 5 additions & 3 deletions ActiveLabel/ActiveBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ struct ActiveBuilder {
return createElements(from: text, for: type, range: range, minLength: 1, filterPredicate: filterPredicate)
case .email:
return createElements(from: text, for: type, range: range, filterPredicate: filterPredicate)
case .timestamp:
return createElements(from: text, for: type, range: range, filterPredicate: filterPredicate)
}
}

Expand All @@ -38,7 +40,7 @@ struct ActiveBuilder {

guard let maxLength = maximumLength, word.count > maxLength else {
let range = maximumLength == nil ? match.range : (text as NSString).range(of: word)
let element = ActiveElement.create(with: type, text: word)
let element = ActiveElement.create(with: type, range: range, text: word, fullText: text)
elements.append((range, element, type))
continue
}
Expand Down Expand Up @@ -67,7 +69,7 @@ struct ActiveBuilder {
let word = nsstring.substring(with: match.range)
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if filterPredicate?(word) ?? true {
let element = ActiveElement.create(with: type, text: word)
let element = ActiveElement.create(with: type, range: match.range, text: word, fullText: text)
elements.append((match.range, element, type))
}
}
Expand All @@ -93,7 +95,7 @@ struct ActiveBuilder {
}

if filterPredicate?(word) ?? true {
let element = ActiveElement.create(with: type, text: word)
let element = ActiveElement.create(with: type, range: match.range, text: word, fullText: text)
elements.append((match.range, element, type))
}
}
Expand Down
38 changes: 37 additions & 1 deletion ActiveLabel/ActiveLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy
@IBInspectable open var hashtagSelectedColor: UIColor? {
didSet { updateTextStorage(parseText: false) }
}
@IBInspectable open var timestampColor: UIColor = .blue {
didSet { updateTextStorage(parseText: false) }
}
@IBInspectable open var timestampSelectedColor: UIColor? {
didSet { updateTextStorage(parseText: false) }
}
@IBInspectable open var URLColor: UIColor = .blue {
didSet { updateTextStorage(parseText: false) }
}
Expand Down Expand Up @@ -90,6 +96,10 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy
open func handleEmailTap(_ handler: @escaping (String) -> ()) {
emailTapHandler = handler
}

open func handleTimestampTap(_ handler: @escaping (Timestamp) -> ()) {
timestampTapHandler = handler
}

open func removeHandle(for type: ActiveType) {
switch type {
Expand All @@ -103,6 +113,8 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy
customTapHandlers[type] = nil
case .email:
emailTapHandler = nil
case .timestamp:
timestampTapHandler = nil
}
}

Expand All @@ -115,6 +127,11 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy
hashtagFilterPredicate = predicate
updateTextStorage()
}

open func filterTimestamp(_ predicate: @escaping (String) -> Bool) {
timestampFilterPredicate = predicate
updateTextStorage()
}

// MARK: - override UILabel properties
override open var text: String? {
Expand Down Expand Up @@ -223,6 +240,7 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy
case .url(let originalURL, _): didTapStringURL(originalURL)
case .custom(let element): didTap(element, for: selectedElement.type)
case .email(let element): didTapStringEmail(element)
case .timestamp(let element): didTapTimestamp(element)
}

let when = DispatchTime.now() + Double(Int64(0.25 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
Expand Down Expand Up @@ -251,10 +269,12 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy
internal var hashtagTapHandler: ((String) -> ())?
internal var urlTapHandler: ((URL) -> ())?
internal var emailTapHandler: ((String) -> ())?
internal var timestampTapHandler: ((Timestamp) -> ())?
internal var customTapHandlers: [ActiveType : ((String) -> ())] = [:]

fileprivate var mentionFilterPredicate: ((String) -> Bool)?
fileprivate var hashtagFilterPredicate: ((String) -> Bool)?
fileprivate var timestampFilterPredicate: ((String) -> Bool)?

fileprivate var selectedElement: ElementTuple?
fileprivate var heightCorrection: CGFloat = 0
Expand Down Expand Up @@ -333,6 +353,7 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy
case .url: attributes[NSAttributedString.Key.foregroundColor] = URLColor
case .custom: attributes[NSAttributedString.Key.foregroundColor] = customColor[type] ?? defaultCustomColor
case .email: attributes[NSAttributedString.Key.foregroundColor] = URLColor
case .timestamp: attributes[NSAttributedString.Key.foregroundColor] = timestampColor
}

if let highlightFont = hightlightFont {
Expand Down Expand Up @@ -371,8 +392,13 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy
filter = mentionFilterPredicate
} else if type == .hashtag {
filter = hashtagFilterPredicate
} else if type == .timestamp {
filter = timestampFilterPredicate
}
let hashtagElements = ActiveBuilder.createElements(type: type, from: textString, range: textRange, filterPredicate: filter)
let hashtagElements = ActiveBuilder
.createElements(type: type, from: textString, range: textRange, filterPredicate: filter)
.filter { $0.type != .timestamp || Timestamp.filter(at: $0.range, with: textString) }

activeElements[type] = hashtagElements
}

Expand Down Expand Up @@ -416,6 +442,7 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy
let possibleSelectedColor = customSelectedColor[selectedElement.type] ?? customColor[selectedElement.type]
selectedColor = possibleSelectedColor ?? defaultCustomColor
case .email: selectedColor = URLSelectedColor ?? URLColor
case .timestamp: selectedColor = timestampSelectedColor ?? timestampColor
}
attributes[NSAttributedString.Key.foregroundColor] = selectedColor
} else {
Expand All @@ -426,6 +453,7 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy
case .url: unselectedColor = URLColor
case .custom: unselectedColor = customColor[selectedElement.type] ?? defaultCustomColor
case .email: unselectedColor = URLColor
case .timestamp: unselectedColor = timestampColor
}
attributes[NSAttributedString.Key.foregroundColor] = unselectedColor
}
Expand Down Expand Up @@ -524,6 +552,14 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy
}
emailHandler(stringEmail)
}

fileprivate func didTapTimestamp(_ timestamp: Timestamp) {
guard let timestampTapHandler = timestampTapHandler else {
delegate?.didSelect(timestamp.timeString, type: .timestamp)
return
}
timestampTapHandler(timestamp)
}

fileprivate func didTap(_ element: String, for type: ActiveType) {
guard let elementHandler = customTapHandlers[type] else {
Expand Down
8 changes: 7 additions & 1 deletion ActiveLabel/ActiveType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ enum ActiveElement {
case hashtag(String)
case email(String)
case url(original: String, trimmed: String)
case timestamp(Timestamp)
case custom(String)

static func create(with activeType: ActiveType, text: String) -> ActiveElement {
static func create(with activeType: ActiveType, range: NSRange, text: String, fullText: String) -> ActiveElement {
switch activeType {
case .mention: return mention(text)
case .hashtag: return hashtag(text)
case .email: return email(text)
case .url: return url(original: text, trimmed: text)
case .timestamp: return timestamp(.create(text, range: range, in: fullText))
case .custom: return custom(text)
}
}
Expand All @@ -31,6 +33,7 @@ public enum ActiveType {
case hashtag
case url
case email
case timestamp
case custom(pattern: String)

var pattern: String {
Expand All @@ -39,6 +42,7 @@ public enum ActiveType {
case .hashtag: return RegexParser.hashtagPattern
case .url: return RegexParser.urlPattern
case .email: return RegexParser.emailPattern
case .timestamp: return RegexParser.timestampPattern
case .custom(let regex): return regex
}
}
Expand All @@ -51,6 +55,7 @@ extension ActiveType: Hashable, Equatable {
case .hashtag: hasher.combine(-2)
case .url: hasher.combine(-3)
case .email: hasher.combine(-4)
case .timestamp: hasher.combine(-5)
case .custom(let regex): hasher.combine(regex)
}
}
Expand All @@ -62,6 +67,7 @@ public func ==(lhs: ActiveType, rhs: ActiveType) -> Bool {
case (.hashtag, .hashtag): return true
case (.url, .url): return true
case (.email, .email): return true
case (.timestamp, .timestamp): return true
case (.custom(let pattern1), .custom(let pattern2)): return pattern1 == pattern2
default: return false
}
Expand Down
8 changes: 8 additions & 0 deletions ActiveLabel/RegexParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ struct RegexParser {
static let hashtagPattern = "(?:^|\\s|$)#[\\p{L}0-9_]*"
static let mentionPattern = "(?:^|\\s|$|[.])@[\\p{L}0-9_]*"
static let emailPattern = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
static let timestampPattern = "([0-9][0-9]{0,1}:)?[0-5]{0,1}[0-9]{1}:[0-5]{1}[0-9]{1}"
static let timestampChapterTitlePattern = "[^\\s](.*[^\\s])(\n|$)"
static let urlPattern = "(^|[\\s.:;?\\-\\]<\\(])" +
"((https?://|www\\.|pic\\.)[-\\w;/?:@&=+$\\|\\_.!~*\\|'()\\[\\]%#,☺]+[\\w/#](\\(\\))?)" +
"(?=$|[\\s',\\|\\(\\).:;?\\-\\[\\]>\\)])"
Expand All @@ -24,6 +26,12 @@ struct RegexParser {
return elementRegex.matches(in: text, options: [], range: range)
}

static func isMatchToAnyPatterns(from text: String, range: NSRange) -> Bool {
[hashtagPattern, mentionPattern, emailPattern, timestampPattern, urlPattern]
.compactMap { getElements(from: text, with: $0, range: range).first }
.count > 0
}

private static func regularExpression(for pattern: String) -> NSRegularExpression? {
if let regex = cachedRegularExpressions[pattern] {
return regex
Expand Down
23 changes: 23 additions & 0 deletions ActiveLabel/StringSubscriptExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// StringSubscriptExtension.swift
// ActiveLabel
//
// Created by Steve Kim on 2022/03/31.
// Copyright © 2022 Optonaut. All rights reserved.
//

import Foundation

extension String {
subscript(at range: NSRange?) -> Self? {
guard let range = range else { return nil }
let endIndexOf = range.location + range.length - 1
let isSafeRange = range.location >= 0 && endIndexOf < count

guard isSafeRange else { return nil }

let startIndex = index(self.startIndex, offsetBy: range.location)
let endIndex = index(self.startIndex, offsetBy: range.location + range.length)
return Self(self[startIndex..<endIndex])
}
}
Loading