每个新行添加一个编号列表UITextView

时间:2014-12-15 00:14:18

标签: ios objective-c uitextview

每当用户输入换行符时,我都会尝试在每行的开头添加一个数字。我希望数字按顺序排列(如在有序列表中),但是使用我当前的代码,如果用户不在末尾添加新行,而是在{{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。

修改

enter image description here

当用户在第1行后添加新行时,我想要更新所有行。希望这更清楚。

2 个答案:

答案 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.放在第一行。此外,用户可以输入无序列表以及有序列表。最后,如果用户点击回车键,我希望清除一个空的项目符号行。

屏幕截图进行解释:

enter image description here

和代码:

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
}