Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save keicoder/9545512 to your computer and use it in GitHub Desktop.
Save keicoder/9545512 to your computer and use it in GitHub Desktop.
objective-c : UITextView subclass for supporting string, regex search and highlighting
//UITextView subclass for supporting string, regex search and highlighting
//Created by Ivano Bilenchi on 05/11/13
//ICAppDelegate.h
@interface ICAppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@end
//ICAppDelegate.m
#import "ICViewController.h"
@implementation ICAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
ICViewController *rootController = [[ICViewController alloc] init];
self.window.rootViewController = rootController;
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
@end
//ICViewController.m
#import "ICTextView.h"
@interface ICViewController ()
{
ICTextView *_textView;
UISearchBar *_searchBar;
}
@end
@implementation ICViewController
#pragma mark - Self
- (void)loadView
{
/**
* 프레임과 바운즈 차이 보기
* applicationFrame are: 0.000000, 20.000000, 320.000000, 460.000000
* applicationBounds are: 0.000000, 0.000000, 320.000000, 480.000000
CGRect applicationFrame = [[UIScreen mainScreen] applicationFrame];
NSLog (@"applicationFrame are: %f, %f, %f, %f", applicationFrame.origin.x, applicationFrame.origin.y, applicationFrame.size.width, applicationFrame.size.height);
CGRect applicationBounds = [[UIScreen mainScreen] bounds];
NSLog (@"applicationBounds are: %f, %f, %f, %f", applicationBounds.origin.x, applicationBounds.origin.y, applicationBounds.size.width, applicationBounds.size.height);
**/
CGRect tempFrame = [[UIScreen mainScreen] applicationFrame];
CGFloat statusBarOffset = 20.0; // iOS7
_searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0.0, statusBarOffset, tempFrame.size.width, 44.0)]; //x, y, width, height
_searchBar.delegate = self;
UIView *mainView = [[UIView alloc] initWithFrame:tempFrame];
CGFloat keyboardHeight = 216.0; // lazy
CGFloat searchBarHeight = _searchBar.frame.size.height;
_textView = [[ICTextView alloc] initWithFrame:tempFrame];
UIEdgeInsets tempInsets = UIEdgeInsetsMake(searchBarHeight, 0.0, keyboardHeight, 0.0); //top, left, bottom, right
_textView.contentInset = tempInsets;
_textView.scrollIndicatorInsets = tempInsets;
[mainView addSubview:_textView];
[mainView addSubview:_searchBar];
self.view = mainView;
}
- (void)viewDidLoad
{
[super viewDidLoad];
_textView.text = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"ICTextView" ofType:@"h"] encoding:NSUTF8StringEncoding error:NULL];
_textView.font = [UIFont systemFontOfSize:14.0];
[_searchBar becomeFirstResponder];
}
#pragma mark - UISearchBarDelegate
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
if (!searchText || [searchText isEqualToString:@""])
{
[_textView resetSearch];
return;
}
[_textView scrollToString:searchText searchOptions:NSRegularExpressionCaseInsensitive];
}
- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar
{
[_textView becomeFirstResponder];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[_textView scrollToString:searchBar.text searchOptions:NSRegularExpressionCaseInsensitive];
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
searchBar.text = nil;
[_textView resetSearch];
}
@end
//ICViewController.h
@interface ICViewController : UIViewController <UISearchBarDelegate>
@end
//ICTextView.h - 1.0.2
//Methods to account for contentInsets in iOS 7
//Contains workarounds to many known iOS 7 UITextView bugs
//Installation
//1) just grab the ICTextView.h and ICTextView.m files and put them in project.
//2) ICTextView requires the QuartzCore framework.
//3) #import "ICTextView.h" and It's ready to go.
//Configuration:
//See comments in the `#pragma mark - Configuration` section.
/**
*
* Usage:
* ------
*
* Search:
* -------
* Searches can be performed via the `scrollToMatch:searchOptions:range:` and `scrollToString:searchOptions:range:` methods.
* `scrollToMatch:` performs regex searches, while `scrollToString:` searches for string literals.
* Both search methods are regex-powered, and therefore make use of `NSRegularExpressionOptions`.
* The `rangeOfFoundString` property contains the range of the current search match.
* You can get the actual string by calling the `foundString` method.
* The `resetSearch` method lets you restore the search variables to their starting values, effectively resetting the search.
* Calls to `resetSearch` cause the highlights to be deallocated, regardless of the `maxHighlightedMatches` variable.
* After this method has been called, ICTextView stops highlighting results until a new search is performed.
* Content insets methods:
* -----------------------
* The `scrollRangeToVisible:consideringInsets:` and `scrollRectToVisible:animated:consideringInsets:` methods let you scroll
* until a certain range or rect is visible, eventually accounting for content insets.
* This was the default behavior for `scrollRangeToVisible:` before iOS 7, but it has changed since (possibly because of a bug).
* This method calls `scrollRangeToVisible:` in iOS 6.x and below, and has a custom implementation in iOS 7.
* The other methods are pretty much self-explanatory. See the `#pragma mark - Misc` section for further info.
* iOS 7 UITextView Bugfixes
* -------------------------
* Long story short, iOS 7 completely broke `UITextView`. `ICTextView` contains fixes for some very common issues:
*
* - NSTextContainer bugfix: `UITextView` initialized via `initWithFrame:` had an erratic behavior due to an uninitialized or wrong `NSTextContainer`
* - Caret bugfix: the caret didn't consider `contentInset` and often went out of the visible area
* - characterRangeAtPoint bugfix: `characterRangeAtPoint:` always returned `nil`
* These fixes, combined with the custom methods to account for `contentInset`, should make working with `ICTextView` much more bearable
* than working with the standard `UITextView`.
**/
#import <UIKit/UIKit.h>
@interface ICTextView : UITextView
#pragma mark - Configuration
// Color of the primary search highlight (default = RGB 150/200/255)
@property (strong, nonatomic) UIColor *primaryHighlightColor;
// Color of the secondary search highlights (default = RGB 215/240/255)
@property (strong, nonatomic) UIColor *secondaryHighlightColor;
// Highlight corner radius (default = fontSize * 0.2)
@property (nonatomic) CGFloat highlightCornerRadius;
// Toggles highlights for search results (default = YES // NO = only scrolls)
@property (nonatomic) BOOL highlightSearchResults;
// Maximum number of cached highlighted matches (default = 100)
// Note 1: setting this too high will impact memory usage
// Note 2: this value is indicative. More search results will be highlighted if they are on-screen
@property (nonatomic) NSUInteger maxHighlightedMatches;
// Delay for the auto-refresh while scrolling feature (default = 0.2 // min = 0.1 // off = 0.0)
// Note: decreasing/disabling this may improve performance when self.text is very big
@property (nonatomic) NSTimeInterval scrollAutoRefreshDelay;
// Range of string found during last search ({0, 0} on init and after resetSearch // {NSNotFound, 0} if not found)
@property (nonatomic, readonly) NSRange rangeOfFoundString;
#pragma mark - Methods
#pragma mark -- Search --
// Returns string found during last search
- (NSString *)foundString;
// Resets search, starts from top
- (void)resetSearch;
// Scrolls to regex match (returns YES if found, NO otherwise)
- (BOOL)scrollToMatch:(NSString *)pattern;
- (BOOL)scrollToMatch:(NSString *)pattern searchOptions:(NSRegularExpressionOptions)options;
- (BOOL)scrollToMatch:(NSString *)pattern searchOptions:(NSRegularExpressionOptions)options range:(NSRange)range;
// Scrolls to string (returns YES if found, NO otherwise)
- (BOOL)scrollToString:(NSString *)stringToFind;
- (BOOL)scrollToString:(NSString *)stringToFind searchOptions:(NSRegularExpressionOptions)options;
- (BOOL)scrollToString:(NSString *)stringToFind searchOptions:(NSRegularExpressionOptions)options range:(NSRange)range;
#pragma mark -- Misc --
// Scrolls to visible range, eventually considering insets
- (void)scrollRangeToVisible:(NSRange)range consideringInsets:(BOOL)considerInsets;
// Scrolls to visible rect, eventually considering insets
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated consideringInsets:(BOOL)considerInsets;
// Returns visible range, with start and end position, eventually considering insets
- (NSRange)visibleRangeConsideringInsets:(BOOL)considerInsets;
- (NSRange)visibleRangeConsideringInsets:(BOOL)considerInsets startPosition:(UITextPosition *__autoreleasing *)startPosition endPosition:(UITextPosition *__autoreleasing *)endPosition;
// Returns visible rect, eventually considering insets
- (CGRect)visibleRectConsideringInsets:(BOOL)considerInsets;
@end
//ICTextView.m - 1.0.2
#import "ICTextView.h"
#import <QuartzCore/QuartzCore.h>
// Document subview tag
enum
{
ICTagTextSubview = 181337
};
// Private iVars
@interface ICTextView ()
{
// Highlights
NSMutableDictionary *_highlightsByRange;
NSMutableArray *_primaryHighlights;
NSMutableOrderedSet *_secondaryHighlights;
// Work variables
NSRegularExpression *_regex;
NSTimer *_autoRefreshTimer;
NSRange _searchRange;
NSUInteger _scanIndex;
BOOL _performedNewScroll;
BOOL _shouldUpdateScanIndex;
// TODO: remove iOS 7 bugfixes when an official fix is available
BOOL _appliedCharacterRangeAtPointBugfix;
}
@end
// Search results highlighting supported starting from iOS 5.x
static BOOL _highlightingSupported;
@implementation ICTextView
#pragma mark - Synthesized properties
@synthesize primaryHighlightColor = _primaryHighlightColor;
@synthesize secondaryHighlightColor = _secondaryHighlightColor;
@synthesize highlightCornerRadius = _highlightCornerRadius;
@synthesize highlightSearchResults = _highlightSearchResults;
@synthesize maxHighlightedMatches = _maxHighlightedMatches;
@synthesize scrollAutoRefreshDelay = _scrollAutoRefreshDelay;
@synthesize rangeOfFoundString = _rangeOfFoundString;
#pragma mark - Class methods
+ (void)initialize
{
if (self == [ICTextView class])
_highlightingSupported = [self conformsToProtocol:@protocol(UITextInput)];
}
#pragma mark - Private methods
// Adds highlight at rect (returns highlight UIView)
- (UIView *)addHighlightAtRect:(CGRect)frame
{
UIView *highlight = [[UIView alloc] initWithFrame:frame];
highlight.layer.cornerRadius = _highlightCornerRadius < 0.0 ? frame.size.height * 0.2 : _highlightCornerRadius;
highlight.backgroundColor = _secondaryHighlightColor;
[_secondaryHighlights addObject:highlight];
[self insertSubview:highlight belowSubview:[self viewWithTag:ICTagTextSubview]];
return highlight;
}
// Adds highlight at text range (returns array of highlights for text range)
- (NSMutableArray *)addHighlightAtTextRange:(UITextRange *)textRange
{
NSMutableArray *highlightsForRange = [[NSMutableArray alloc] init];
// iOS 6.x and newer implementation
CGRect previousRect = CGRectZero;
NSArray *highlightRects = [self selectionRectsForRange:textRange];
// Merges adjacent rects
for (UITextSelectionRect *selectionRect in highlightRects)
{
CGRect currentRect = selectionRect.rect;
if ((currentRect.origin.y == previousRect.origin.y) && (currentRect.origin.x == CGRectGetMaxX(previousRect)) && (currentRect.size.height == previousRect.size.height))
{
// Adjacent, add to previous rect
previousRect = CGRectMake(previousRect.origin.x, previousRect.origin.y, previousRect.size.width + currentRect.size.width, previousRect.size.height);
}
else
{
// Not adjacent, add previous rect to highlights array
[highlightsForRange addObject:[self addHighlightAtRect:previousRect]];
previousRect = currentRect;
}
}
// Adds last highlight
[highlightsForRange addObject:[self addHighlightAtRect:previousRect]];
return highlightsForRange;
}
// Highlights occurrences of found string in visible range masked by the user specified range
- (void)highlightOccurrencesInMaskedVisibleRange
{
// Regex search
if (_regex)
{
if (_performedNewScroll)
{
// Initial data
UITextPosition *visibleStartPosition;
NSRange visibleRange = [self visibleRangeConsideringInsets:YES startPosition:&visibleStartPosition endPosition:NULL];
// Performs search in masked range
NSRange maskedRange = NSIntersectionRange(_searchRange, visibleRange);
NSMutableArray *rangeValues = [[NSMutableArray alloc] init];
[_regex enumerateMatchesInString:self.text options:0 range:maskedRange usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){
NSValue *rangeValue = [NSValue valueWithRange:match.range];
[rangeValues addObject:rangeValue];
}];
///// ADDS SECONDARY HIGHLIGHTS /////
// Array must have elements
if (rangeValues.count)
{
// Removes already present highlights
NSMutableArray *rangesArray = [rangeValues mutableCopy];
NSMutableIndexSet *indexesToRemove = [[NSMutableIndexSet alloc] init];
[rangeValues enumerateObjectsUsingBlock:^(NSValue *rangeValue, NSUInteger idx, BOOL *stop){
if ([_highlightsByRange objectForKey:rangeValue])
[indexesToRemove addIndex:idx];
}];
[rangesArray removeObjectsAtIndexes:indexesToRemove];
indexesToRemove = nil;
// Filtered array must have elements
if (rangesArray.count)
{
// Gets text range of first result
NSValue *firstRangeValue = [rangesArray objectAtIndex:0];
NSRange previousRange = [firstRangeValue rangeValue];
UITextPosition *start = [self positionFromPosition:visibleStartPosition offset:(previousRange.location - visibleRange.location)];
UITextPosition *end = [self positionFromPosition:start offset:previousRange.length];
UITextRange *textRange = [self textRangeFromPosition:start toPosition:end];
// First range
[_highlightsByRange setObject:[self addHighlightAtTextRange:textRange] forKey:firstRangeValue];
if (rangesArray.count > 1)
{
// Loops through ranges
for (NSUInteger idx = 1; idx < rangesArray.count; idx++)
{
NSValue *rangeValue = [rangesArray objectAtIndex:idx];
NSRange range = [rangeValue rangeValue];
start = [self positionFromPosition:end offset:range.location - (previousRange.location + previousRange.length)];
end = [self positionFromPosition:start offset:range.length];
textRange = [self textRangeFromPosition:start toPosition:end];
[_highlightsByRange setObject:[self addHighlightAtTextRange:textRange] forKey:rangeValue];
previousRange = range;
}
}
// Memory management
NSInteger remaining = _maxHighlightedMatches - _highlightsByRange.count;
if (remaining < 0)
{
NSInteger tempMin = visibleRange.location - visibleRange.length;
NSUInteger min = tempMin > 0 ? tempMin : 0;
NSUInteger max = min + 3 * visibleRange.length;
// Scans highlighted ranges
NSMutableArray *keysToRemove = [[NSMutableArray alloc] init];
[_highlightsByRange enumerateKeysAndObjectsUsingBlock:^(NSValue *rangeValue, NSArray *highlightsForRange, BOOL *stop){
// Removes ranges too far from visible range
NSUInteger location = [rangeValue rangeValue].location;
if ((location < min || location > max) && location != _rangeOfFoundString.location)
{
for (UIView *hl in highlightsForRange)
{
[hl removeFromSuperview];
[_secondaryHighlights removeObject:hl];
}
[keysToRemove addObject:rangeValue];
}
}];
[_highlightsByRange removeObjectsForKeys:keysToRemove];
}
}
}
// Eventually updates _scanIndex to match visible range
if (_shouldUpdateScanIndex)
_scanIndex = visibleRange.location + (_regex ? visibleRange.length : 0);
}
// Sets primary highlight
[self setPrimaryHighlightAtRange:_rangeOfFoundString];
}
}
// Convenience method used in init overrides
- (void)initialize
{
_highlightCornerRadius = -1.0;
_highlightsByRange = [[NSMutableDictionary alloc] init];
_highlightSearchResults = YES;
_maxHighlightedMatches = 100;
_scrollAutoRefreshDelay = 0.2;
_primaryHighlights = [[NSMutableArray alloc] init];
_primaryHighlightColor = [UIColor colorWithRed:150.0/255.0 green:200.0/255.0 blue:1.0 alpha:1.0];
_secondaryHighlights = [[NSMutableOrderedSet alloc] init];
_secondaryHighlightColor = [UIColor colorWithRed:215.0/255.0 green:240.0/255.0 blue:1.0 alpha:1.0];
// Detects _UITextContainerView or UIWebDocumentView (subview with text) for highlight placement
for (UIView *view in self.subviews)
{
if ([view isKindOfClass:NSClassFromString(@"_UITextContainerView")] || [view isKindOfClass:NSClassFromString(@"UIWebDocumentView")])
{
view.tag = ICTagTextSubview;
break;
}
}
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(textChanged)
name:UITextViewTextDidChangeNotification
object:self];
}
// Initializes highlights
- (void)initializeHighlights
{
[self initializePrimaryHighlights];
[self initializeSecondaryHighlights];
}
// Initializes primary highlights
- (void)initializePrimaryHighlights
{
// Moves primary highlights to secondary highlights array
for (UIView *hl in _primaryHighlights)
{
hl.backgroundColor = _secondaryHighlightColor;
[_secondaryHighlights addObject:hl];
}
[_primaryHighlights removeAllObjects];
}
// Initializes secondary highlights
- (void)initializeSecondaryHighlights
{
// Removes secondary highlights from their superview
for (UIView *hl in _secondaryHighlights)
[hl removeFromSuperview];
[_secondaryHighlights removeAllObjects];
// Removes all objects in _highlightsByRange, eventually except _rangeOfFoundString (primary)
if (_primaryHighlights.count)
{
NSValue *rangeValue = [NSValue valueWithRange:_rangeOfFoundString];
NSMutableArray *primaryHighlights = [_highlightsByRange objectForKey:rangeValue];
[_highlightsByRange removeAllObjects];
[_highlightsByRange setObject:primaryHighlights forKey:rangeValue];
}
else
[_highlightsByRange removeAllObjects];
// Sets _performedNewScroll status in order to refresh the highlights
_performedNewScroll = YES;
}
// TODO: remove iOS 7 characterRangeAtPoint: bugfix when an official fix is available
#ifdef __IPHONE_7_0
- (void)characterRangeAtPointBugFix
{
[self select:self];
[self setSelectedTextRange:nil];
_appliedCharacterRangeAtPointBugfix = YES;
}
#endif
// Called when scroll animation has ended
- (void)scrollEnded
{
// Refreshes highlights
[self highlightOccurrencesInMaskedVisibleRange];
// Disables auto-refresh timer
[_autoRefreshTimer invalidate];
_autoRefreshTimer = nil;
// scrollView has finished scrolling
_performedNewScroll = NO;
}
// Sets primary highlight
- (void)setPrimaryHighlightAtRange:(NSRange)range
{
[self initializePrimaryHighlights];
NSValue *rangeValue = [NSValue valueWithRange:range];
NSMutableArray *highlightsForRange = [_highlightsByRange objectForKey:rangeValue];
for (UIView *hl in highlightsForRange)
{
hl.backgroundColor = _primaryHighlightColor;
[_primaryHighlights addObject:hl];
[_secondaryHighlights removeObject:hl];
}
}
// TODO: remove iOS 7 caret bugfix when an official fix is available
#ifdef __IPHONE_7_0
- (void)textChanged
{
UITextRange *selectedTextRange = self.selectedTextRange;
if (selectedTextRange)
[self scrollRectToVisible:[self caretRectForPosition:selectedTextRange.end] animated:NO consideringInsets:YES];
}
#endif
#pragma mark - Overrides
// TODO: remove iOS 7 characterRangeAtPoint: bugfix when an official fix is available
- (void)awakeFromNib
{
[self characterRangeAtPointBugFix];
}
// Resets search if editable
- (BOOL)becomeFirstResponder
{
if (self.editable)
[self resetSearch];
// return [super becomeFirstResponder];
return YES;
}
// TODO: remove iOS 7 caret bugfix when an official fix is available
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
// Init overrides for custom initialization
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self && _highlightingSupported)
[self initialize];
return self;
}
- (id)initWithFrame:(CGRect)frame
{
return [self initWithFrame:frame textContainer:nil];
}
// TODO: remove iOS 7 NSTextContainer bugfix when an official fix is available
#ifdef __IPHONE_7_0
- (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer
{
NSTextStorage *textStorage = [[NSTextStorage alloc] init];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
if (!textContainer)
textContainer = [[NSTextContainer alloc] initWithSize:frame.size];
textContainer.heightTracksTextView = YES;
[layoutManager addTextContainer:textContainer];
self = [super initWithFrame:frame textContainer:textContainer];
if (self && _highlightingSupported)
[self initialize];
return self;
}
#endif
// Executed while scrollView is scrolling
- (void)setContentOffset:(CGPoint)contentOffset
{
[super setContentOffset:contentOffset];
if (_highlightingSupported && _highlightSearchResults)
{
// scrollView has scrolled
_performedNewScroll = YES;
// _shouldUpdateScanIndex check
if (!_shouldUpdateScanIndex)
_shouldUpdateScanIndex = ([self.panGestureRecognizer velocityInView:self].y != 0.0);
// Eventually starts auto-refresh timer
if (_regex && _scrollAutoRefreshDelay && !_autoRefreshTimer)
{
_autoRefreshTimer = [NSTimer timerWithTimeInterval:_scrollAutoRefreshDelay target:self selector:@selector(highlightOccurrencesInMaskedVisibleRange) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_autoRefreshTimer forMode:UITrackingRunLoopMode];
}
// Cancels previous request and performs new one
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(scrollEnded) object:nil];
[self performSelector:@selector(scrollEnded) withObject:nil afterDelay:0.1];
}
}
// Resets highlights on frame change
- (void)setFrame:(CGRect)frame
{
if (_highlightingSupported && _highlightsByRange.count)
[self initializeHighlights];
[super setFrame:frame];
}
// Doesn't allow _scrollAutoRefreshDelay values between 0.0 and 0.1
- (void)setScrollAutoRefreshDelay:(NSTimeInterval)scrollAutoRefreshDelay
{
_scrollAutoRefreshDelay = (scrollAutoRefreshDelay > 0.0 && scrollAutoRefreshDelay < 0.1) ? 0.1 : scrollAutoRefreshDelay;
}
// TODO: remove iOS 7 caret bugfix when an official fix is available
- (void)setSelectedTextRange:(UITextRange *)selectedTextRange
{
[super setSelectedTextRange:selectedTextRange];
[self scrollRectToVisible:[self caretRectForPosition:selectedTextRange.end] animated:NO consideringInsets:YES];
}
// TODO: remove iOS 7 characterRangeAtPoint: bugfix when an official fix is available
- (void)setText:(NSString *)text
{
[super setText:text];
[self characterRangeAtPointBugFix];
}
#pragma mark - Public methods
#pragma mark -- Search --
// Returns string found during last search
- (NSString *)foundString
{
return [self.text substringWithRange:_rangeOfFoundString];
}
// Resets search, starts from top
- (void)resetSearch
{
if (_highlightingSupported)
{
[self initializeHighlights];
[_autoRefreshTimer invalidate];
_autoRefreshTimer = nil;
}
_rangeOfFoundString = NSMakeRange(0,0);
_regex = nil;
_scanIndex = 0;
_searchRange = NSMakeRange(0,0);
}
#pragma mark ---- Regex search ----
// Scroll to regex match (returns YES if found, NO otherwise)
- (BOOL)scrollToMatch:(NSString *)pattern
{
return [self scrollToMatch:pattern searchOptions:0 range:NSMakeRange(0, self.text.length)];
}
- (BOOL)scrollToMatch:(NSString *)pattern searchOptions:(NSRegularExpressionOptions)options
{
return [self scrollToMatch:pattern searchOptions:options range:NSMakeRange(0, self.text.length)];
}
- (BOOL)scrollToMatch:(NSString *)pattern searchOptions:(NSRegularExpressionOptions)options range:(NSRange)range
{
// Calculates a valid range
range = NSIntersectionRange(NSMakeRange(0, self.text.length), range);
// Preliminary checks
BOOL abort = NO;
if (!pattern)
{
#if DEBUG
NSLog(@"Pattern cannot be nil.");
#endif
abort = YES;
}
else if (range.length == 0)
{
#if DEBUG
NSLog(@"Specified range is out of bounds.");
#endif
abort = YES;
}
if (abort)
{
[self resetSearch];
return NO;
}
// Optimization and coherence checks
BOOL samePattern = [pattern isEqualToString:_regex.pattern];
BOOL sameOptions = (options == _regex.options);
BOOL sameSearchRange = NSEqualRanges(range, _searchRange);
// Sets new search range
_searchRange = range;
// Creates regex
NSError *error;
_regex = [[NSRegularExpression alloc] initWithPattern:pattern options:options error:&error];
if (error)
{
#if DEBUG
NSLog(@"Error while creating regex: %@", error);
#endif
[self resetSearch];
return NO;
}
// Resets highlights
if (_highlightingSupported && _highlightSearchResults)
{
[self initializePrimaryHighlights];
if (!(samePattern && sameOptions && sameSearchRange))
[self initializeSecondaryHighlights];
}
// Scan index logic
if (sameSearchRange && sameOptions)
{
// Same search pattern, go to next match
if (samePattern)
_scanIndex += _rangeOfFoundString.length;
// Scan index out of range
if (_scanIndex < range.location || _scanIndex >= (range.location + range.length))
_scanIndex = range.location;
}
else
_scanIndex = range.location;
// Gets match
NSRange matchRange = [_regex rangeOfFirstMatchInString:self.text options:0 range:NSMakeRange(_scanIndex, range.location + range.length - _scanIndex)];
// Match not found
if (matchRange.location == NSNotFound)
{
_rangeOfFoundString = NSMakeRange(NSNotFound, 0);
if (_scanIndex)
{
// Starts from top
_scanIndex = range.location;
return [self scrollToMatch:pattern searchOptions:options range:range];
}
_regex = nil;
return NO;
}
// Match found, saves state
_rangeOfFoundString = matchRange;
_scanIndex = matchRange.location;
_shouldUpdateScanIndex = NO;
// Adds highlights
if (_highlightingSupported && _highlightSearchResults)
[self highlightOccurrencesInMaskedVisibleRange];
// Scrolls
[self scrollRangeToVisible:matchRange consideringInsets:YES];
return YES;
}
#pragma mark ---- String search ----
// Scroll to string (returns YES if found, NO otherwise)
- (BOOL)scrollToString:(NSString *)stringToFind
{
return [self scrollToString:stringToFind searchOptions:0 range:NSMakeRange(0, self.text.length)];
}
- (BOOL)scrollToString:(NSString *)stringToFind searchOptions:(NSRegularExpressionOptions)options
{
return [self scrollToString:stringToFind searchOptions:options range:NSMakeRange(0, self.text.length)];
}
- (BOOL)scrollToString:(NSString *)stringToFind searchOptions:(NSRegularExpressionOptions)options range:(NSRange)range
{
// Preliminary check
if (!stringToFind)
{
#if DEBUG
NSLog(@"Search string cannot be nil.");
#endif
[self resetSearch];
return NO;
}
// Escapes metacharacters
stringToFind = [NSRegularExpression escapedPatternForString:stringToFind];
// These checks allow better automatic search on UITextField or UISearchBar text change
if (_regex)
{
NSString *lcStringToFind = [stringToFind lowercaseString];
NSString *lcFoundString = [_regex.pattern lowercaseString];
if (!([lcStringToFind hasPrefix:lcFoundString] || [lcFoundString hasPrefix:lcStringToFind]))
_scanIndex += _rangeOfFoundString.length;
}
// Performs search
return [self scrollToMatch:stringToFind searchOptions:options range:range];
}
#pragma mark -- Misc --
// Scrolls to visible range, eventually considering insets
- (void)scrollRangeToVisible:(NSRange)range consideringInsets:(BOOL)considerInsets
{
// Calculates rect for range
UITextPosition *startPosition = [self positionFromPosition:self.beginningOfDocument offset:range.location];
UITextPosition *endPosition = [self positionFromPosition:startPosition offset:range.length];
UITextRange *textRange = [self textRangeFromPosition:startPosition toPosition:endPosition];
CGRect rect = [self firstRectForRange:textRange];
// Scrolls to visible rect
[self scrollRectToVisible:rect animated:YES consideringInsets:YES];
}
// Scrolls to visible rect, eventually considering insets
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated consideringInsets:(BOOL)considerInsets
{
// Gets bounds and calculates visible rect
CGRect bounds = self.bounds;
UIEdgeInsets contentInset = self.contentInset;
CGRect visibleRect = [self visibleRectConsideringInsets:YES];
// Do not scroll if rect is on screen
if (!CGRectContainsRect(visibleRect, rect))
{
CGPoint contentOffset = self.contentOffset;
// Calculates new contentOffset
if (rect.origin.y < visibleRect.origin.y)
// rect precedes bounds, scroll up
contentOffset.y = rect.origin.y - contentInset.top;
else
// rect follows bounds, scroll down
contentOffset.y = rect.origin.y + contentInset.bottom + rect.size.height - bounds.size.height;
[self setContentOffset:contentOffset animated:animated];
}
}
// Returns visible range, eventually considering insets
- (NSRange)visibleRangeConsideringInsets:(BOOL)considerInsets
{
return [self visibleRangeConsideringInsets:considerInsets startPosition:NULL endPosition:NULL];
}
// Returns visible range, with start and end position, eventually considering insets
- (NSRange)visibleRangeConsideringInsets:(BOOL)considerInsets startPosition:(UITextPosition *__autoreleasing *)startPosition endPosition:(UITextPosition *__autoreleasing *)endPosition
{
CGRect visibleRect = [self visibleRectConsideringInsets:considerInsets];
CGPoint startPoint = visibleRect.origin;
CGPoint endPoint = CGPointMake(CGRectGetMaxX(visibleRect), CGRectGetMaxY(visibleRect));
UITextPosition *start = [self characterRangeAtPoint:startPoint].start;
UITextPosition *end = [self characterRangeAtPoint:endPoint].end;
if (startPosition)
*startPosition = start;
if (endPosition)
*endPosition = end;
return NSMakeRange([self offsetFromPosition:self.beginningOfDocument toPosition:start], [self offsetFromPosition:start toPosition:end]);
}
// Returns visible rect, eventually considering insets
- (CGRect)visibleRectConsideringInsets:(BOOL)considerInsets
{
CGRect bounds = self.bounds;
if (considerInsets)
{
UIEdgeInsets contentInset = self.contentInset;
CGRect visibleRect = self.bounds;
visibleRect.origin.x += contentInset.left;
visibleRect.origin.y += contentInset.top;
visibleRect.size.width -= (contentInset.left + contentInset.right);
visibleRect.size.height -= (contentInset.top + contentInset.bottom);
return visibleRect;
}
return bounds;
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment