Uitextview Highlight All Matches Using Swift

UITextView highlight all matches using swift

Obligatory NSRegularExpression based solution.

let searchString = "this"
let baseString = "This is some string that contains the word \"this\" more than once. This substring has multiple cases. ThisthisThIs."

let attributed = NSMutableAttributedString(string: baseString)

var error: NSError?
let regex = NSRegularExpression(pattern: searchString, options: .CaseInsensitive, error: &error)

if let regexError = error {
println("Oh no! \(regexError)")
} else {
for match in regex?.matchesInString(baseString, options: NSMatchingOptions.allZeros, range: NSRange(location: 0, length: baseString.utf16Count)) as [NSTextCheckingResult] {
attributed.addAttribute(NSBackgroundColorAttributeName, value: UIColor.yellowColor(), range: match.range)
}

textView.attributedText = attributed
}

Highlight UITextView text

You can enumerate (enumerate(_:in:option:)) on the NSAttributedString.Key.backgroundColor to find change only when it has a background.
Then, you can use a regex, or a while loop with range(of:) to find where they are, and remove the .backgroundColor on them:

With sample code on Playgrounds:

func highlights() -> UITextView {

let tv = UITextView(frame: CGRect(x: 0, y: 0, width: 300, height: 200))
tv.backgroundColor = .orange

let text = "Hello world! How are you today?\nLet's start do some testing.\nAnd this is a long paragraph just to see it to the end of the line."
let attributes: [NSAttributedString.Key: Any] = [.font: UIFont.boldSystemFont(ofSize: 15.0),
.backgroundColor: UIColor.systemPink]

let first = NSAttributedString(string: text, attributes: attributes)
let second = NSMutableAttributedString(string: text, attributes: attributes)
guard let regex = try? NSRegularExpression(pattern: "\n", options: []) else { return tv }
second.enumerateAttribute(.backgroundColor, in: NSRange(location: 0, length: second.length), options: []) { attribute, range, stop in
guard attribute as? UIColor != nil else { return }
guard let subrange = Range(range, in: second.string) else { return }
let substring = String(second.string[subrange])
let ranges = regex.matches(in: substring, options: [], range: NSRange(location: 0, length: substring.utf16.count))
ranges.forEach {
second.removeAttribute(.backgroundColor, range: $0.range)
}
}
let total = NSMutableAttributedString()
total.append(first)
total.append(NSAttributedString(string: "\nNormal Text, nothing to see here\n"))
total.append(second)
total.append(NSAttributedString(string: "\nNormal Text, nothing to see here\n"))
tv.attributedText = total
return tv
}
let tv = highlights()

Sample Image

Side note:
I didn't handle the case if you have in the string "\n \n", that might need some changes in the regex pattern.
After a quick test, then NSRegularExpression(pattern: "\n(\\s+\n)*", options: []) might do the trick.

Highlight specific UITextView text base on UITextField search (swift2)

There are some changes required in your code to get this to work.

In the searchCode() function, let range = match.rangeAtIndex(1) will give an app crash for index out of bound.

You can simply use the match.range to add an attribute:

if let match = matches.first {

attributed.addAttribute(NSBackgroundColorAttributeName, value: UIColor.yellowColor(), range: match.range)
}

Just correcting this line will make your code work, but this will highlight only the first match in the text view.

To highlight all the matches, you can change this block

if let match = matches.first {
let range = match.rangeAtIndex(1)
if let swiftRange = rangeFromNSRange(range, forString: baselowercase) {
attributed.addAttribute(NSBackgroundColorAttributeName, value: UIColor.yellowColor(), range: match.range)
}
}

and add a loop on matches array, to iterate through all matches and add an attribute for highlighting them.

for match in matches {
attributed.addAttribute(NSBackgroundColorAttributeName, value: UIColor.yellowColor(), range: match.range)
}

So the complete code for searchCode() will be:

func searchCode(){
let keyword = self.searchcodetxt.text
let lowercasekeyword = keyword!.lowercaseString
let baseString = webcode.text
let baselowercase = baseString!.lowercaseString
let attributed = NSMutableAttributedString(string: baseString)

do {
let regex = try NSRegularExpression(pattern: lowercasekeyword, options: NSRegularExpressionOptions.CaseInsensitive)
let matches = regex.matchesInString(baselowercase, options: [], range: NSMakeRange(0, baselowercase.characters.count))

for match in matches {
attributed.addAttribute(NSBackgroundColorAttributeName, value: UIColor.yellowColor(), range: match.range)
}

webcode.attributedText = attributed
} catch {
// regex was bad!
let alertView:UIAlertView = UIAlertView()
alertView.title = "Keywords error!"
alertView.message = "Please use another keywords"
alertView.delegate = self
alertView.addButtonWithTitle("OK")
alertView.show()
// Delay the dismissal by 5 seconds
let delay = 5.0 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
alertView.dismissWithClickedButtonIndex(-1, animated: true)
})
}
}

Now, you would not require the rangeFromNSRange() method.

How can I change style of some words in my UITextView one by one in Swift?

Use a timer. Stash matches in a property. Stash the base unhighlighted attributed string in a property. Now have your timer highlight the first match and call itself again in 1 second, highlighting up to the second match and repeat until there are no matches left.

    func highlight (to index: Int = 0) {
guard index < matches.count else {
return
}
let titleDict: NSDictionary = [NSForegroundColorAttributeName: orangeColor]
let attributedString = NSMutableAttributedString(attributedString: storedAttributedString)
for i in 0..< index {
let matchRange = matches[i].rangeAt(0)
attributedString.addAttributes(titleDict as! [String : AnyObject], range: matchRange)
}
self.attributedText = attributedString
let _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
self.highlight(to: index + 1)
}
}

Using a CALayer to highlight text in a UITextView which spans multiple lines

I've found the solution:

Instead of using enumerateEnclosingRectsForGlyphRange: I use the NSString method enumerateSubstringsInRange:options:usingBlock:

I enumerate the line fragments as usual, but instead of trying to use the enclosing rect enumeration method, I enumerate each character on the line while building the rect for the layer.

-(void)drawLayerForTextHighlightWithString:(NSString*)string {

for (CALayer* eachLayer in [self highlightLayers]) {
[eachLayer removeFromSuperlayer];
}

NSLayoutManager* manager = [[self textView]layoutManager];

// Find the string
NSRange match = [[[self textView]text]rangeOfString:string options:
NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch | NSWidthInsensitiveSearch];

// Convert it to a glyph range
NSRange matchingGlyphRange = [manager glyphRangeForCharacterRange:match actualCharacterRange:NULL];

// Enumerate each line in that glyph range (this will fire for each line that the match spans)
[manager enumerateLineFragmentsForGlyphRange:matchingGlyphRange usingBlock:
^(CGRect lineRect, CGRect usedRect, NSTextContainer *textContainer, NSRange lineRange, BOOL *stop) {

// currentRange uses NSIntersectionRange to return the range of the text that is on the current line
NSRange currentRange = NSIntersectionRange(lineRange, matchingGlyphRange);

// This rect will be built by enumerating each character in the line, and adding to it's width
__block CGRect finalLineRect = CGRectZero;

// Here we use enumerateSubstringsInRange:... to go through each glyph and build the final rect for the line
[[[self textView]text]enumerateSubstringsInRange:currentRange options:NSStringEnumerationByComposedCharacterSequences usingBlock:
^(NSString* substring, NSRange substringRange, NSRange enclostingRange, BOOL* stop) {

// The range of the single glyph being enumerated
NSRange singleGlyphRange = [manager glyphRangeForCharacterRange:substringRange actualCharacterRange:NULL];

// get the rect for that glyph
CGRect glyphRect = [manager boundingRectForGlyphRange:singleGlyphRange inTextContainer:textContainer];

// check to see if this is the first iteration, if not add the width to the final rect for the line
if (CGRectEqualToRect(finalLineRect, CGRectZero)) {
finalLineRect = glyphRect;
} else {
finalLineRect.size.width += glyphRect.size.width;
}

}];

// once we get the rect for the line, draw the layer
UIEdgeInsets textContainerInset = [[self textView]textContainerInset];
finalLineRect.origin.x += textContainerInset.left;
finalLineRect.origin.y += textContainerInset.top;

CALayer* roundRect = [CALayer layer];
[roundRect setFrame:finalLineRect];
[roundRect setBounds:finalLineRect];

[roundRect setCornerRadius:5.0f];
[roundRect setBackgroundColor:[[UIColor blueColor]CGColor]];
[roundRect setOpacity:0.2f];
[roundRect setBorderColor:[[UIColor blackColor]CGColor]];
[roundRect setBorderWidth:3.0f];
[roundRect setShadowColor:[[UIColor blackColor]CGColor]];
[roundRect setShadowOffset:CGSizeMake(20.0f, 20.0f)];
[roundRect setShadowOpacity:1.0f];
[roundRect setShadowRadius:10.0f];

[[[self textView]layer]addSublayer:roundRect];
[[self highlightLayers]addObject:roundRect];

// continues for each line
}];

}

I'm still working on multiple matches, i'll update the code once I get that working.

Swift + Search for a string and go (set focus) to its ocurrence

First get the CGRect of the highlighted text using:

let highlightRect = textView.firstRectForRange(highlightRange)

Then scroll focus to the CGRect with:

textView.scrollRectToVisible(highlightRect, animated: true)

If you need to convert an NSRange to a UITextRange in order to use the firstRectForRange method. Use something like this function:

func convertRange(range: NSRange, forTextView textView: UITextView) -> (UITextRange){
let beginning = textView.beginningOfDocument
let start = textView.positionFromPosition(beginning, offset: range.location)!
let end = textView.positionFromPosition(start, offset: range.length)!
return textView.textRangeFromPosition(start, toPosition: end)!
}


Related Topics



Leave a reply



Submit