Skip to content

Commit

Permalink
Merge pull request #40 from optonaut/feature/improveTableViewPerformance
Browse files Browse the repository at this point in the history
Improve table view performance
  • Loading branch information
polqf committed Jan 29, 2016
2 parents 29cc0c2 + c4a1449 commit ccbb286
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 156 deletions.
10 changes: 7 additions & 3 deletions ActiveLabel.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@

/* Begin PBXBuildFile section */
8F0249A61B9989B1005D8035 /* ActiveLabel.h in Headers */ = {isa = PBXBuildFile; fileRef = 8F0249A51B9989B1005D8035 /* ActiveLabel.h */; settings = {ATTRIBUTES = (Public, ); }; };
8F0249AD1B9989B1005D8035 /* ActiveLabel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F0249A21B9989B1005D8035 /* ActiveLabel.framework */; settings = {ASSET_TAGS = (); }; };
8F0249AD1B9989B1005D8035 /* ActiveLabel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F0249A21B9989B1005D8035 /* ActiveLabel.framework */; };
8F0249B21B9989B1005D8035 /* ActiveTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0249B11B9989B1005D8035 /* ActiveTypeTests.swift */; };
8F0249C31B998A66005D8035 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0249C21B998A66005D8035 /* AppDelegate.swift */; };
8F0249C51B998A66005D8035 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0249C41B998A66005D8035 /* ViewController.swift */; };
8F0249C81B998A66005D8035 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8F0249C61B998A66005D8035 /* Main.storyboard */; };
8F0249CA1B998A66005D8035 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8F0249C91B998A66005D8035 /* Assets.xcassets */; };
8F0249CD1B998A66005D8035 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8F0249CB1B998A66005D8035 /* LaunchScreen.storyboard */; };
8F0249D31B998C00005D8035 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0249D21B998C00005D8035 /* ActiveLabel.swift */; settings = {ASSET_TAGS = (); }; };
8F0249D51B998D21005D8035 /* ActiveType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0249D41B998D21005D8035 /* ActiveType.swift */; settings = {ASSET_TAGS = (); }; };
8F0249D31B998C00005D8035 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0249D21B998C00005D8035 /* ActiveLabel.swift */; };
8F0249D51B998D21005D8035 /* ActiveType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0249D41B998D21005D8035 /* ActiveType.swift */; };
C1E867D61C3D7AEA00FD687A /* RegexParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E867D51C3D7AEA00FD687A /* RegexParser.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -45,6 +46,7 @@
8F0249CE1B998A66005D8035 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8F0249D21B998C00005D8035 /* ActiveLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = "<group>"; };
8F0249D41B998D21005D8035 /* ActiveType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveType.swift; sourceTree = "<group>"; };
C1E867D51C3D7AEA00FD687A /* RegexParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegexParser.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -99,6 +101,7 @@
8F0249A51B9989B1005D8035 /* ActiveLabel.h */,
8F0249D21B998C00005D8035 /* ActiveLabel.swift */,
8F0249D41B998D21005D8035 /* ActiveType.swift */,
C1E867D51C3D7AEA00FD687A /* RegexParser.swift */,
8F0249A71B9989B1005D8035 /* Info.plist */,
);
path = ActiveLabel;
Expand Down Expand Up @@ -268,6 +271,7 @@
files = (
8F0249D31B998C00005D8035 /* ActiveLabel.swift in Sources */,
8F0249D51B998D21005D8035 /* ActiveType.swift in Sources */,
C1E867D61C3D7AEA00FD687A /* RegexParser.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
136 changes: 43 additions & 93 deletions ActiveLabel/ActiveLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,55 +18,26 @@ public protocol ActiveLabelDelegate: class {
// MARK: - public properties
public weak var delegate: ActiveLabelDelegate?

@IBInspectable public var mentionEnabled: Bool = true {
didSet {
updateTextStorage()
}
}
@IBInspectable public var hashtagEnabled: Bool = true {
didSet {
updateTextStorage()
}
}
@IBInspectable public var URLEnabled: Bool = true {
didSet {
updateTextStorage()
}
}
@IBInspectable public var mentionColor: UIColor = .blueColor() {
didSet {
updateTextStorage()
}
didSet { updateTextStorage(parseText: false) }
}
@IBInspectable public var mentionSelectedColor: UIColor? {
didSet {
updateTextStorage()
}
didSet { updateTextStorage(parseText: false) }
}
@IBInspectable public var hashtagColor: UIColor = .blueColor() {
didSet {
updateTextStorage()
}
didSet { updateTextStorage(parseText: false) }
}
@IBInspectable public var hashtagSelectedColor: UIColor? {
didSet {
updateTextStorage()
}
didSet { updateTextStorage(parseText: false) }
}
@IBInspectable public var URLColor: UIColor = .blueColor() {
didSet {
updateTextStorage()
}
didSet { updateTextStorage(parseText: false) }
}
@IBInspectable public var URLSelectedColor: UIColor? {
didSet {
updateTextStorage()
}
didSet { updateTextStorage(parseText: false) }
}
@IBInspectable public var lineSpacing: Float? {
didSet {
updateTextStorage()
}
didSet { updateTextStorage(parseText: false) }
}

// MARK: - public methods
Expand All @@ -84,33 +55,23 @@ public protocol ActiveLabelDelegate: class {

// MARK: - override UILabel properties
override public var text: String? {
didSet {
updateTextStorage()
}
didSet { updateTextStorage() }
}

override public var attributedText: NSAttributedString? {
didSet {
updateTextStorage()
}
didSet { updateTextStorage() }
}

override public var font: UIFont! {
didSet {
updateTextStorage()
}
didSet { updateTextStorage(parseText: false) }
}

override public var textColor: UIColor! {
didSet {
updateTextStorage()
}
didSet { updateTextStorage(parseText: false) }
}

override public var textAlignment: NSTextAlignment {
didSet {
updateTextStorage()
}
didSet { updateTextStorage(parseText: false)}
}

// MARK: - init functions
Expand Down Expand Up @@ -196,7 +157,7 @@ public protocol ActiveLabelDelegate: class {
private lazy var textStorage = NSTextStorage()
private lazy var layoutManager = NSLayoutManager()
private lazy var textContainer = NSTextContainer()
private lazy var activeElements: [ActiveType: [(range: NSRange, element: ActiveElement)]] = [
internal lazy var activeElements: [ActiveType: [(range: NSRange, element: ActiveElement)]] = [
.Mention: [],
.Hashtag: [],
.URL: [],
Expand All @@ -210,27 +171,27 @@ public protocol ActiveLabelDelegate: class {
userInteractionEnabled = true
}

private func updateTextStorage() {
guard let attributedText = attributedText else {
return
}

private func updateTextStorage(parseText parseText: Bool = true) {
// clean up previous active elements
for (type, _) in activeElements {
activeElements[type]?.removeAll()
}

guard attributedText.length > 0 else {
guard let attributedText = attributedText
where attributedText.length > 0 else {
return
}

let mutAttrString = addLineBreak(attributedText)
parseTextAndExtractActiveElements(mutAttrString)
addLinkAttribute(mutAttrString)

textStorage.setAttributedString(mutAttrString)

if parseText {
for (type, _) in activeElements {
activeElements[type]?.removeAll()
}
parseTextAndExtractActiveElements(mutAttrString)
}

setNeedsDisplay()
dispatch_async(dispatch_get_main_queue()) {
self.addLinkAttribute(mutAttrString)
self.textStorage.setAttributedString(mutAttrString)
self.setNeedsDisplay()
}
}

private func textOrigin(inRect rect: CGRect) -> CGPoint {
Expand Down Expand Up @@ -268,34 +229,23 @@ public protocol ActiveLabelDelegate: class {

/// use regex check all link ranges
private func parseTextAndExtractActiveElements(attrString: NSAttributedString) {
let textString = attrString.string as NSString
let textLength = textString.length
var searchRange = NSMakeRange(0, textLength)
let textString = attrString.string
let textLength = textString.utf16.count
let textRange = NSRange(location: 0, length: textLength)

for word in textString.componentsSeparatedByCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) {
let element = activeElement(word)

if case .None = element {
continue
}

let elementRange = textString.rangeOfString(word, options: .LiteralSearch, range: searchRange)
defer {
let startIndex = elementRange.location + elementRange.length
searchRange = NSMakeRange(startIndex, textLength - startIndex)
}

switch element {
case .Mention where mentionEnabled:
activeElements[.Mention]?.append((elementRange, element))
case .Hashtag where hashtagEnabled:
activeElements[.Hashtag]?.append((elementRange, element))
case .URL where URLEnabled:
activeElements[.URL]?.append((elementRange, element))
default: ()
}
}
//URLS
let urlElements = ActiveBuilder.createURLElements(fromText: textString, range: textRange)
activeElements[.URL]?.appendContentsOf(urlElements)

//HASHTAGS
let hashtagElements = ActiveBuilder.createHashtagElements(fromText: textString, range: textRange)
activeElements[.Hashtag]?.appendContentsOf(hashtagElements)

//MENTIONS
let mentionElements = ActiveBuilder.createMentionElements(fromText: textString, range: textRange)
activeElements[.Mention]?.appendContentsOf(mentionElements)
}


/// add line break mode
private func addLineBreak(attrString: NSAttributedString) -> NSMutableAttributedString {
Expand Down
80 changes: 42 additions & 38 deletions ActiveLabel/ActiveType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,49 +22,53 @@ public enum ActiveType {
case None
}

func activeElement(word: String) -> ActiveElement {
if let url = reduceRightToURL(word) {
return .URL(url)
}
struct ActiveBuilder {

if word.characters.count < 2 {
return .None
static func createMentionElements(fromText text: String, range: NSRange) -> [(range: NSRange, element: ActiveElement)] {
let mentions = RegexParser.getMentions(fromText: text, range: range)
let nsstring = text as NSString
var elements: [(range: NSRange, element: ActiveElement)] = []

for mention in mentions where mention.range.length > 2 {
let range = NSRange(location: mention.range.location + 1, length: mention.range.length - 1)
var word = nsstring.substringWithRange(range)
if word.hasPrefix("@") {
word.removeAtIndex(word.startIndex)
}
let element = ActiveElement.Mention(word)
elements.append((mention.range, element))
}
return elements
}

// remove # or @ sign and reduce to alpha numeric string (also allowed: _)
guard let allowedWord = reduceRightToAllowed(word.substringFromIndex(word.startIndex.advancedBy(1))) else {
return .None
static func createHashtagElements(fromText text: String, range: NSRange) -> [(range: NSRange, element: ActiveElement)] {
let hashtags = RegexParser.getHashtags(fromText: text, range: range)
let nsstring = text as NSString
var elements: [(range: NSRange, element: ActiveElement)] = []

for hashtag in hashtags where hashtag.range.length > 2 {
let range = NSRange(location: hashtag.range.location + 1, length: hashtag.range.length - 1)
var word = nsstring.substringWithRange(range)
if word.hasPrefix("#") {
word.removeAtIndex(word.startIndex)
}
let element = ActiveElement.Hashtag(word)
elements.append((hashtag.range, element))
}
return elements
}

if word.hasPrefix("@") {
return .Mention(allowedWord)
} else if word.hasPrefix("#") {
return .Hashtag(allowedWord)
} else {
return .None
}
}

private func reduceRightToURL(str: String) -> String? {
if let urlDetector = try? NSDataDetector(types: NSTextCheckingType.Link.rawValue) {
let nsStr = str as NSString
let results = urlDetector.matchesInString(str, options: .ReportCompletion, range: NSRange(location: 0, length: nsStr.length))
if let result = results.map({ nsStr.substringWithRange($0.range) }).first {
return result
static func createURLElements(fromText text: String, range: NSRange) -> [(range: NSRange, element: ActiveElement)] {
let urls = RegexParser.getURLs(fromText: text, range: range)
let nsstring = text as NSString
var elements: [(range: NSRange, element: ActiveElement)] = []

for url in urls where url.range.length > 2 {
let word = nsstring.substringWithRange(url.range)
.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
let element = ActiveElement.URL(word)
elements.append((url.range, element))
}
return elements
}
return nil
}

private func reduceRightToAllowed(str: String) -> String? {
if let regex = try? NSRegularExpression(pattern: "^[a-z0-9_]*", options: [.CaseInsensitive]) {
let nsStr = str as NSString
let results = regex.matchesInString(str, options: [], range: NSRange(location: 0, length: nsStr.length))
if let result = results.map({ nsStr.substringWithRange($0.range) }).first {
if !result.isEmpty {
return result
}
}
}
return nil
}
36 changes: 36 additions & 0 deletions ActiveLabel/RegexParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// RegexParser.swift
// ActiveLabel
//
// Created by Pol Quintana on 06/01/16.
// Copyright © 2016 Optonaut. All rights reserved.
//

import Foundation

struct RegexParser {

static let urlPattern = "(^|[\\s.:;?\\-\\]<\\(])" +
"((https?://|www.|pic.)[-\\w;/?:@&=+$\\|\\_.!~*\\|'()\\[\\]%#,☺]+[\\w/#](\\(\\))?)" +
"(?=$|[\\s',\\|\\(\\).:;?\\-\\[\\]>\\)])"

static let hashtagRegex = try? NSRegularExpression(pattern: "(?:^|\\s|$)#[a-z0-9_]*", options: [.CaseInsensitive])
static let mentionRegex = try? NSRegularExpression(pattern: "(?:^|\\s|$|[.])@[a-z0-9_]*", options: [.CaseInsensitive])
static let urlDetector = try? NSRegularExpression(pattern: urlPattern, options: [.CaseInsensitive])

static func getMentions(fromText text: String, range: NSRange) -> [NSTextCheckingResult] {
guard let mentionRegex = mentionRegex else { return [] }
return mentionRegex.matchesInString(text, options: [], range: range)
}

static func getHashtags(fromText text: String, range: NSRange) -> [NSTextCheckingResult] {
guard let hashtagRegex = hashtagRegex else { return [] }
return hashtagRegex.matchesInString(text, options: [], range: range)
}

static func getURLs(fromText text: String, range: NSRange) -> [NSTextCheckingResult] {
guard let urlDetector = urlDetector else { return [] }
return urlDetector.matchesInString(text, options: [], range: range)
}

}

0 comments on commit ccbb286

Please sign in to comment.