每当用户输入换行符时,我都会尝试在每行的开头添加一个数字。我希望数字按顺序排列(如在有序列表中),但是使用我当前的代码,如果用户不在末尾添加新行,而是在{{1}的中间添加行},它将从底部停止的位置继续计算 - 意味着UITextView
我做了增量并且没有考虑到用户没有在最后创建新行。
NSUInteger
我刚发布的代码添加了一个编号列表,每个新行增加1。现在,如果您尝试在中间添加新行,而不是在最后,它将从最后一个行号增加1,它不会从前一行号增加。例如,如果用户向- (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;
}
添加6行,则用户转到第3行并添加新行,它将在第3行之后显示#7,因为每次用户创建新行时UITextView
增加1。
修改
当用户在第1行后添加新行时,我想要更新所有行。希望这更清楚。
答案 0 :(得分:7)
这种情况实际上比我预期的更复杂,因为在用户输入时动态创建编号列表,需要一个处理与删除,插入,光标位置等相关的各种场景的代码。但是在心里我的答案,这个代码基本上是通过将文本视图字符串分成" line"由" \ n"分隔的组件,从每行中删除当前尾随数字,按顺序重新添加适当的数字,然后重新组合字符串以返回到文本视图。
- (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;
}
答案 1 :(得分:1)
尽管要求有所不同,但仍将在此处发布一些我自己的代码。
基本上,我还需要自动创建和索引项目符号点,但是项目符号并不是唯一要创建的内容,因此不应将1.放在第一行。此外,用户可以输入无序列表以及有序列表。最后,如果用户点击回车键,我希望清除一个空的项目符号行。
屏幕截图进行解释:
和代码:
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
}