diff --git a/.travis.yml b/.travis.yml index eff32e98..3dccd3ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: objective-c -osx_image: xcode10.2 +osx_image: xcode12.2 before_install: - gem install bundler - gem update bundler diff --git a/ActiveLabel.podspec b/ActiveLabel.podspec index 753e6a71..56b7226a 100644 --- a/ActiveLabel.podspec +++ b/ActiveLabel.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'ActiveLabel' - s.version = '1.1.0' + s.version = '1.1.5' s.author = { 'Optonaut' => 'hello@optonaut.co' } s.homepage = 'https://github.com/optonaut/ActiveLabel.swift' @@ -12,8 +12,8 @@ Pod::Spec.new do |s| UILabel drop-in replacement supporting Hashtags (#), Mentions (@), URLs (http://) and custom regex patterns, written in Swift Features - * Up-to-date: Swift 5.0 and Xcode 10.2 - * Default support for Hashtags, Mentions, Links + * Swift 5.0 (1.1.0+) and 4.2 (1.0.1) + * Default support for **Hashtags, Mentions, Links, Emails** * Support for custom types via regex * Ability to enable highlighting only for the desired types * Ability to trim urls diff --git a/ActiveLabel.xcodeproj/project.pbxproj b/ActiveLabel.xcodeproj/project.pbxproj index 7c722bd8..311e4a1c 100644 --- a/ActiveLabel.xcodeproj/project.pbxproj +++ b/ActiveLabel.xcodeproj/project.pbxproj @@ -492,6 +492,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MARKETING_VERSION = 1.1.5; PRODUCT_BUNDLE_IDENTIFIER = optonaut.ActiveLabel; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -513,6 +514,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MARKETING_VERSION = 1.1.5; PRODUCT_BUNDLE_IDENTIFIER = optonaut.ActiveLabel; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/ActiveLabel/ActiveBuilder.swift b/ActiveLabel/ActiveBuilder.swift index 40971c8f..ed4916d6 100644 --- a/ActiveLabel/ActiveBuilder.swift +++ b/ActiveLabel/ActiveBuilder.swift @@ -20,6 +20,8 @@ struct ActiveBuilder { return createElements(from: text, for: type, range: range, filterPredicate: filterPredicate) case .custom: return createElements(from: text, for: type, range: range, minLength: 1, filterPredicate: filterPredicate) + case .email: + return createElements(from: text, for: type, range: range, filterPredicate: filterPredicate) } } diff --git a/ActiveLabel/ActiveLabel.swift b/ActiveLabel/ActiveLabel.swift index 01861adf..073d4532 100644 --- a/ActiveLabel/ActiveLabel.swift +++ b/ActiveLabel/ActiveLabel.swift @@ -87,6 +87,10 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy customTapHandlers[type] = handler } + open func handleEmailTap(_ handler: @escaping (String) -> ()) { + emailTapHandler = handler + } + open func removeHandle(for type: ActiveType) { switch type { case .hashtag: @@ -97,6 +101,8 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy urlTapHandler = nil case .custom: customTapHandlers[type] = nil + case .email: + emailTapHandler = nil } } @@ -181,8 +187,11 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy // MARK: - Auto layout open override var intrinsicContentSize: CGSize { - let superSize = super.intrinsicContentSize - textContainer.size = CGSize(width: superSize.width, height: CGFloat.greatestFiniteMagnitude) + guard let text = text, !text.isEmpty else { + return .zero + } + + textContainer.size = CGSize(width: self.preferredMaxLayoutWidth, height: CGFloat.greatestFiniteMagnitude) let size = layoutManager.usedRect(for: textContainer) return CGSize(width: ceil(size.width), height: ceil(size.height)) } @@ -193,7 +202,7 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy var avoidSuperCall = false switch touch.phase { - case .began, .moved: + case .began, .moved, .regionEntered, .regionMoved: if let element = element(at: location) { if element.range.location != selectedElement?.range.location || element.range.length != selectedElement?.range.length { updateAttributesWhenSelected(false) @@ -205,7 +214,7 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy updateAttributesWhenSelected(false) selectedElement = nil } - case .ended: + case .ended, .regionExited: guard let selectedElement = selectedElement else { return avoidSuperCall } switch selectedElement.element { @@ -213,6 +222,7 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy case .hashtag(let hashtag): didTapHashtag(hashtag) case .url(let originalURL, _): didTapStringURL(originalURL) case .custom(let element): didTap(element, for: selectedElement.type) + case .email(let element): didTapStringEmail(element) } let when = DispatchTime.now() + Double(Int64(0.25 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) @@ -240,6 +250,7 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy internal var mentionTapHandler: ((String) -> ())? internal var hashtagTapHandler: ((String) -> ())? internal var urlTapHandler: ((URL) -> ())? + internal var emailTapHandler: ((String) -> ())? internal var customTapHandlers: [ActiveType : ((String) -> ())] = [:] fileprivate var mentionFilterPredicate: ((String) -> Bool)? @@ -321,6 +332,7 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy case .hashtag: attributes[NSAttributedString.Key.foregroundColor] = hashtagColor case .url: attributes[NSAttributedString.Key.foregroundColor] = URLColor case .custom: attributes[NSAttributedString.Key.foregroundColor] = customColor[type] ?? defaultCustomColor + case .email: attributes[NSAttributedString.Key.foregroundColor] = URLColor } if let highlightFont = hightlightFont { @@ -403,6 +415,7 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy case .custom: let possibleSelectedColor = customSelectedColor[selectedElement.type] ?? customColor[selectedElement.type] selectedColor = possibleSelectedColor ?? defaultCustomColor + case .email: selectedColor = URLSelectedColor ?? URLColor } attributes[NSAttributedString.Key.foregroundColor] = selectedColor } else { @@ -412,6 +425,7 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy case .hashtag: unselectedColor = hashtagColor case .url: unselectedColor = URLColor case .custom: unselectedColor = customColor[selectedElement.type] ?? defaultCustomColor + case .email: unselectedColor = URLColor } attributes[NSAttributedString.Key.foregroundColor] = unselectedColor } @@ -503,6 +517,14 @@ typealias ElementTuple = (range: NSRange, element: ActiveElement, type: ActiveTy urlHandler(url) } + fileprivate func didTapStringEmail(_ stringEmail: String) { + guard let emailHandler = emailTapHandler else { + delegate?.didSelect(stringEmail, type: .email) + return + } + emailHandler(stringEmail) + } + fileprivate func didTap(_ element: String, for type: ActiveType) { guard let elementHandler = customTapHandlers[type] else { delegate?.didSelect(element, type: type) diff --git a/ActiveLabel/ActiveType.swift b/ActiveLabel/ActiveType.swift index dc329544..55ee66fc 100644 --- a/ActiveLabel/ActiveType.swift +++ b/ActiveLabel/ActiveType.swift @@ -11,6 +11,7 @@ import Foundation enum ActiveElement { case mention(String) case hashtag(String) + case email(String) case url(original: String, trimmed: String) case custom(String) @@ -18,6 +19,7 @@ enum 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 .custom: return custom(text) } @@ -28,6 +30,7 @@ public enum ActiveType { case mention case hashtag case url + case email case custom(pattern: String) var pattern: String { @@ -35,6 +38,7 @@ public enum ActiveType { case .mention: return RegexParser.mentionPattern case .hashtag: return RegexParser.hashtagPattern case .url: return RegexParser.urlPattern + case .email: return RegexParser.emailPattern case .custom(let regex): return regex } } @@ -46,6 +50,7 @@ extension ActiveType: Hashable, Equatable { case .mention: hasher.combine(-1) case .hashtag: hasher.combine(-2) case .url: hasher.combine(-3) + case .email: hasher.combine(-4) case .custom(let regex): hasher.combine(regex) } } @@ -56,6 +61,7 @@ public func ==(lhs: ActiveType, rhs: ActiveType) -> Bool { case (.mention, .mention): return true case (.hashtag, .hashtag): return true case (.url, .url): return true + case (.email, .email): return true case (.custom(let pattern1), .custom(let pattern2)): return pattern1 == pattern2 default: return false } diff --git a/ActiveLabel/Info.plist b/ActiveLabel/Info.plist index a6f720ec..ca23c84f 100644 --- a/ActiveLabel/Info.plist +++ b/ActiveLabel/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.1 + $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion diff --git a/ActiveLabel/RegexParser.swift b/ActiveLabel/RegexParser.swift index b0ad4a4b..fe9bd7ab 100644 --- a/ActiveLabel/RegexParser.swift +++ b/ActiveLabel/RegexParser.swift @@ -12,6 +12,7 @@ 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 urlPattern = "(^|[\\s.:;?\\-\\]<\\(])" + "((https?://|www\\.|pic\\.)[-\\w;/?:@&=+$\\|\\_.!~*\\|'()\\[\\]%#,☺]+[\\w/#](\\(\\))?)" + "(?=$|[\\s',\\|\\(\\).:;?\\-\\[\\]>\\)])" diff --git a/ActiveLabelTests/ActiveTypeTests.swift b/ActiveLabelTests/ActiveTypeTests.swift index d18ce7ac..55f1da8b 100644 --- a/ActiveLabelTests/ActiveTypeTests.swift +++ b/ActiveLabelTests/ActiveTypeTests.swift @@ -37,6 +37,7 @@ class ActiveTypeTests: XCTestCase { case .hashtag(let hashtag): return hashtag case .url(let url, _): return url case .custom(let element): return element + case .email(let element): return element } } @@ -47,6 +48,7 @@ class ActiveTypeTests: XCTestCase { case .hashtag: return .hashtag case .url: return .url case .custom: return customEmptyType + case .email: return .email } } diff --git a/README.md b/README.md index 56f8c4ab..3d9795f3 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # ActiveLabel.swift [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Build Status](https://travis-ci.org/optonaut/ActiveLabel.swift.svg)](https://travis-ci.org/optonaut/ActiveLabel.swift) -UILabel drop-in replacement supporting Hashtags (#), Mentions (@), URLs (http://) and custom regex patterns, written in Swift +UILabel drop-in replacement supporting Hashtags (#), Mentions (@), URLs (http://), Emails and custom regex patterns, written in Swift ## Features -* Swift 5.0 (1.1.0) and 4.2 (1.0.1) -* Default support for **Hashtags, Mentions, Links** +* Swift 5.0 (1.1.0+) and 4.2 (1.0.1) +* Default support for **Hashtags, Mentions, Links, Emails** * Support for **custom types** via regex * Ability to enable highlighting only for the desired types * Ability to trim urls @@ -15,14 +15,13 @@ UILabel drop-in replacement supporting Hashtags (#), Mentions (@), URLs (http:// ![](ActiveLabelDemo/demo.gif) - ## Install (iOS 10+) ### Carthage Add the following to your `Cartfile` and follow [these instructions](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) -``` +```sh github "optonaut/ActiveLabel.swift" ``` @@ -44,7 +43,7 @@ import ActiveLabel let label = ActiveLabel() label.numberOfLines = 0 -label.enabledTypes = [.mention, .hashtag, .url] +label.enabledTypes = [.mention, .hashtag, .url, .email] label.text = "This is a post with #hashtags and a @userhandle." label.textColor = .black label.handleHashtagTap { hashtag in @@ -56,13 +55,12 @@ label.handleHashtagTap { hashtag in ```swift let customType = ActiveType.custom(pattern: "\\swith\\b") //Regex that looks for "with" -label.enabledTypes = [.mention, .hashtag, .url, customType] +label.enabledTypes = [.mention, .hashtag, .url, .email, customType] label.text = "This is a post with #hashtags and a @userhandle." label.customColor[customType] = UIColor.purple label.customSelectedColor[customType] = UIColor.green - -label.handleCustomTap(for: customType) { element in - print("Custom type tapped: \(element)") +label.handleCustomTap(for: customType) { element in + print("Custom type tapped: \(element)") } ``` @@ -71,12 +69,11 @@ label.handleCustomTap(for: customType) { element in By default, an ActiveLabel instance has the following configuration ```swift -label.enabledTypes = [.mention, .hashtag, .url] +label.enabledTypes = [.mention, .hashtag, .url, .email] ``` But feel free to enable/disable to fit your requirements - ## Batched customization When using ActiveLabel, it is recommended to use the `customize(block:)` method to customize it. The reason is that ActiveLabel is reacting to each property that you set. So if you set 3 properties, the textContainer is refreshed 3 times. @@ -140,6 +137,12 @@ label.handleHashtagTap { hashtag in print("\(hashtag) tapped") } label.handleURLTap { url in UIApplication.shared.openURL(url) } ``` +##### `handleEmailTap: (String) -> ()` + +```swift +label.handleEmailTap { email in print("\(email) tapped") } +``` + ##### `handleCustomTap(for type: ActiveType, handler: (String) -> ())` ```swift