Reputation: 693
I'm trying to add a number to the beginning of each row whenever the user enters a newline. I'd like the numbers to go in order (like in an ordered list), but with my current code, if the user does not add a new line at the end but instead adds the line in the middle of the UITextView
, it will continue counting from where it left off at the bottom -- meaning that the NSUInteger
I made increments and doesn't take into account that the user did not make the new line at the end.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
if ([text isEqualToString:@"\n"])
{
numbered ++;
NSString *string = [NSString stringWithFormat:@"\n%lu ", (unsigned long)numbered];
[self insertXEveryNewLine:range :textView :string];
return NO;
}
return YES;
}
- (BOOL)insertXEveryNewLine:(NSRange)place :(UITextView *)View :(NSString *)something
{
NSRange cursor = NSMakeRange(place.location + 3, 0);
NSMutableString *mutableT = [NSMutableString stringWithString:View.text];
[mutableT insertString:something atIndex:place.location];
[View setText:mutableT];
return NO;
}
The code I just posted adds a numbered list which increases by 1 every new line. Now, if you try adding a new line in the middle, not at the end, it will increase by 1 from the last line number, it won't increase from the previous line number. For example, if the user adds 6 lines to the UITextView
, then the user goes to line #3 and adds a new line, it will display #7 after line #3, because every time the user makes a new line numbered
gets increased by 1.
Edit
When the user adds a new line after line 1, I want all lines to update. Hope this is clearer.
Upvotes: 3
Views: 2893
Reputation: 3167
Going to post up some of my own code here, though the requirements are somewhat different.
Basically I also needed to have automatically created and indexed bullet points, but bullet points aren't the only content being created and therefore a 1. shouldn't be placed on the first line off the bat. Also, the user can input unordered lists as well as ordered lists. Finally, I wanted an empty bulleted line to be cleared if the user hit return on it.
Screenshot to explain:
And the code:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
//
// When the user hits the return key, to initiate a new line, we will do some processing
// to see if we are continuing a list of unordered or ordered bullet points.
// As an extra requirement, if the user hits return on an empty bullet point, then we clear the bullet point.
//
if text.first == Character("\n") {
let fullText = textView.text as NSString
let precedingText = fullText.substring(to: range.upperBound)
let precedingLines = precedingText.components(separatedBy: .newlines)
guard let precedingLineString = precedingLines.last else { return true }
let precedingLineNSString = precedingLineString as NSString
//
// This code will check for the prescence of a filled bullet point on the preceding line,
// in the format of `1. Bullet Point Text`, or `- A dashed bullet point`
// If this is found, then the new line will automatically gain it's own indexed bullet point.
//
// Pattern: [Line Beginning] {([Numbers] [Full Stop]) or [Bullet Character: -+*]} [Single Space Character] [All Characters] [Line End]
guard let filledLineRegex = try? NSRegularExpression(pattern: "^(?:(?:(\\d+).)|([-+*]))\\s.+$", options: .anchorsMatchLines) else { return true }
let options = NSRegularExpression.MatchingOptions(rawValue: 0)
let precedingLineRange = NSMakeRange(0, precedingLineNSString.length)
if let match = filledLineRegex.matches(in: precedingLineString, options: options, range: precedingLineRange).first {
// Matched on an ordered bullet: "1. Some Text"
let digitRange = match.range(at: 1)
if digitRange.location != NSNotFound {
let substring = precedingLineNSString.substring(with: digitRange)
if let previousIndex = Int(substring) {
let newIndex = previousIndex + 1
let newText = "\(text)\(newIndex). "
let newFullText = fullText.replacingCharacters(in: range, with: newText)
textView.text = newFullText
let estimatedCursor = NSMakeRange(range.location + newText.count, 0)
textView.selectedRange = estimatedCursor
return false
}
}
// Matched on an unordered bullet: "- Some Text"
let bulletRange = match.range(at: 2)
if bulletRange.location != NSNotFound {
let bulletString = precedingLineNSString.substring(with: bulletRange)
let newText = "\(text)\(bulletString) "
let newFullText = fullText.replacingCharacters(in: range, with: newText)
textView.text = newFullText
let estimatedCursor = NSMakeRange(range.location + newText.count, 0)
textView.selectedRange = estimatedCursor
return false
}
}
//
// In this scenario we are checking if the user has hit return on an empty bullet point line such as
// `1. `, `- `, or `+ `. If this is the case, the the user is signifying that they wish to insert a regular paragraph
// and that the bullet point index should be removed.
//
guard let emptyLineRegex = try? NSRegularExpression(pattern: "^((\\d+.)|[-+*])\\s?$", options: .anchorsMatchLines) else { return true }
if let _ = emptyLineRegex.matches(in: precedingLineString, options: options, range: precedingLineRange).first {
let updatingRange = (precedingText as NSString).range(of: precedingLineString, options: .backwards)
let newFullText = fullText.replacingCharacters(in: updatingRange, with: "")
textView.text = newFullText
let estimatedCursor = NSMakeRange(updatingRange.location, 0)
textView.selectedRange = estimatedCursor
return false
}
}
return true
}
Upvotes: 4
Reputation: 37300
This scenario is actually more logistically complicated than I'd anticipated because dynamically creating a numbered list upon user entry, requires a code that handles many various scenarios relating to deletion, insertions, cursor position, etc. But at the heart of my answer, this code basically works by separating the text view string into "line" components separated by "\n", removes the current trailing numbers from each line, re-adds the appropriate numbers in order, then recombines the string to go back into the text view.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
// Add "1" when the user starts typing into the text field
if (range.location == 0 && textView.text.length == 0) {
// If the user simply presses enter, ignore the newline
// entry, but add "1" to the start of the line.
if ([text isEqualToString:@"\n"]) {
[textView setText:@"1 "];
NSRange cursor = NSMakeRange(range.location + 3, 0);
textView.selectedRange = cursor;
return NO;
}
// In all other scenarios, append the replacement text.
else {
[textView setText:[NSString stringWithFormat:@"1 %@", text]];
}
}
// goBackOneLine is a Boolean to indicate whether the cursor
// should go back 1 line; set to YES in the case that the
// user has deleted the number at the start of the line
bool goBackOneLine = NO;
// Get a string representation of the current line number
// in order to calculate cursor placement based on the
// character count of the number
NSString *stringPrecedingReplacement = [textView.text substringToIndex:range.location];
NSString *currentLine = [NSString stringWithFormat:@"%lu", [stringPrecedingReplacement componentsSeparatedByString:@"\n"].count + 1];
// If the replacement string either contains a new line
// character or is a backspace, proceed with the following
// block...
if ([text rangeOfString:@"\n"].location != NSNotFound || range.length == 1) {
// Combine the new text with the old
NSString *combinedText = [textView.text stringByReplacingCharactersInRange:range withString:text];
// Seperate the combinedText into lines
NSMutableArray *lines = [[combinedText componentsSeparatedByString:@"\n"] mutableCopy];
// To handle the backspace condition
if (range.length == 1) {
// If the user deletes the number at the beginning of the line,
// also delete the newline character proceeding it
// Check to see if the user's deleting a number and
// if so, keep moving backwards digit by digit to see if the
// string's preceeded by a newline too.
if ([textView.text characterAtIndex:range.location] >= '0' && [textView.text characterAtIndex:range.location] <= '9') {
NSUInteger index = 1;
char c = [textView.text characterAtIndex:range.location];
while (c >= '0' && c <= '9') {
c = [textView.text characterAtIndex:range.location - index];
// If a newline is found directly preceding
// the number, delete the number and move back
// to the preceding line.
if (c == '\n') {
combinedText = [textView.text stringByReplacingCharactersInRange:NSMakeRange(range.location - index, range.length + index) withString:text];
lines = [[combinedText componentsSeparatedByString:@"\n"] mutableCopy];
// Set this variable so the cursor knows to back
// up one line
goBackOneLine = YES;
break;
}
index ++;
}
}
// If the user attempts to delete the number 1
// on the first line...
if (range.location == 1) {
NSString *firstRow = [lines objectAtIndex:0];
// If there's text left in the current row, don't
// remove the number 1
if (firstRow.length > 3) {
return NO;
}
// Else if there's no text left in text view other than
// the 1, don't let the user delete it
else if (lines.count == 1) {
return NO;
}
// Else if there's no text in the first row, but there's text
// in the next, move the next row up
else if (lines.count > 1) {
[lines removeObjectAtIndex:0];
}
}
}
// Using a loop, remove the numbers at the start of the lines
// and store the new strings in the linesWithoutLeadingNumbers array
NSMutableArray *linesWithoutLeadingNumbers = [[NSMutableArray alloc] init];
// Go through each line
for (NSString *string in lines) {
// Use the following string to make updates
NSString *stringWithoutLeadingNumbers = [string copy];
// Go through each character
for (int i = 0; i < (int)string.length ; i++) {
char c = [string characterAtIndex:i];
// If the character's a number, remove it
if (c >= '0' && c <= '9') {
stringWithoutLeadingNumbers = [stringWithoutLeadingNumbers stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:@""];
} else {
// And break from the for loop since the number
// and subsequent space have been removed
break;
}
}
// Remove the white space before and after the string to
// clean it up a bit
stringWithoutLeadingNumbers = [stringWithoutLeadingNumbers stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
[linesWithoutLeadingNumbers addObject:stringWithoutLeadingNumbers];
}
// Using a loop, add the numbers to the start of the lines
NSMutableArray *linesWithUpdatedNumbers = [[NSMutableArray alloc] init];
for (int i = 0 ; i < linesWithoutLeadingNumbers.count ; i ++) {
NSString *updatedString = [linesWithoutLeadingNumbers objectAtIndex:i];
NSString *lineNumberString = [NSString stringWithFormat:@"%d ", i + 1];
updatedString = [lineNumberString stringByAppendingString:updatedString];
[linesWithUpdatedNumbers addObject:updatedString];
}
// Then combine the array back into a string by re-adding the
// new lines
NSString *combinedString = @"";
for (int i = 0 ; i < linesWithUpdatedNumbers.count ; i ++) {
combinedString = [combinedString stringByAppendingString:[linesWithUpdatedNumbers objectAtIndex:i]];
if (i < linesWithUpdatedNumbers.count - 1) {
combinedString = [combinedString stringByAppendingString:@"\n"];
}
}
// Set the cursor appropriately.
NSRange cursor;
if ([text isEqualToString:@"\n"]) {
cursor = NSMakeRange(range.location + currentLine.length + 2, 0);
} else if (goBackOneLine) {
cursor = NSMakeRange(range.location - 1, 0);
} else {
cursor = NSMakeRange(range.location, 0);
}
textView.selectedRange = cursor;
// And update the text view
[textView setText:combinedString];
return NO;
}
return YES;
}
Upvotes: 7