NotationMaster
NotationMaster

Reputation: 410

How can I prevent a user from adding too many line breaks in a UITextView in Swift 5?

I have a UITextView to let users type-in comments for some audio file they have just recorded.
I would like to limit the amount of the "\n" (newline characters) they can use to 5 (i.e., the comment should be up to 5 lines long). If they try going to the sixth line I would like to show an alert with a sensible message and, upon pressing the OK button in the relative action, I would like to let the user be able to edit their text.

Delegation is already set up and implementation of the optional func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool is what I am doing now.
The logic I put inside shows the alert correctly but then, upon clicking OK and trying to delete some characters, the method is called again and I get the alert.
I know this is due to the counter still being stuck at 5 but resetting it to 0 allows then for 9 lines or more, so it is not a solution.

Here is the code that I tried and that is not working as intended:

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    if text == characterToCheck /* \n, declared globally */ {
        characterCounter += 1 // this was also declared as a global property
    }

    if characterCounter > 4 {
        let newlineAC = UIAlertController(title: "Too many linebreaks", message: "Please go back and make your comment fit into a maximum of 5 lines", preferredStyle: .alert)
        newlineAC.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] (_) in
            let currentText = textView.text ?? ""
            guard let currentTextRange = Range(range, in: currentText) else { return }

            self?.comments.text = currentText.replacingOccurrences(of: "\n", with: "@ ", range: currentTextRange)
        })
        present(newlineAC, animated: true)

        return false
    } else {
        return true
    }
}  

No error message is thrown because the code does exactly what I ask him but I'm clearly asking it in the wrong way. What can I do?

Upvotes: 0

Views: 1573

Answers (2)

Matt Bart
Matt Bart

Reputation: 939

The logic you are describing is happening because you are always incrementing the counter, but never decrementing it. You should be decrementing the counter, if you incremented the counter, but never ended up adding the new line to the TextView.

...
present(newlineAC, animated: true)
characterCounter-=1
return false
...

Why you should decrement:

By returning false in this function, the TextView will not add the new changes to the TextView. Because they are not added, it shouldn’t be counted in characterCount.

Selection Delete

You must also take into consideration when a user deletes a whole selection of text at once.

let text = NSString(string: textView.text)
for char in text.substring(with: range) {
    print(char)
if char == characterToCheck {
        //if the text that is going to be removed has the character
        //remove it from the count      
        characterCounter-=1
    }
}

Make sure if the user if deleting, the function will return true so that the text actually gets deleted.

Paste (Selection Insert)

If a user pastes, a bunch of text we need to check the line breaks there.

for char in replacementString {
    if char == characterToCheck {
      characterCounter+=1
  } 
}

All Together Now

This takes everything into account. Updates a little bit of logic with a new variable as well.

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    var localCount = characterCount
    //check for deletion
    let NStext = NSString(string: textView.text)
    for char in NStext.substring(with: range) {
        print(char)
        if char == characterToCheck {
            //if the text that is going to be removed has the character
            //remove it from the count      
            localCount-=1
        }
    }

    //check for insertion
    //this will also replace the simple check in the beginning
    for char in replacementString {
            if char == characterToCheck {
            //if any of the character being inserted is the one
            //will be taken into account here
                localCount+=1
            } 
        }

    if localCount > 4 {
        let newlineAC = UIAlertController(title: "Too many linebreaks", message: "Please go back and make your comment fit into a maximum of 5 lines", preferredStyle: .alert)
        newlineAC.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] (_) in
            let currentText = textView.text ?? ""
            guard let currentTextRange = Range(range, in: currentText) else { return }

            self?.comments.text = currentText.replacingOccurrences(of: "\n", with: "@ ", range: currentTextRange)
        })
        present(newlineAC, animated: true)
        return false
    } else {
        characterCounter = localCount
        //only updates if has an OK # of newlines
        return true
    }
}  

Upvotes: 0

Rey Bruno
Rey Bruno

Reputation: 363

Here is my solution:

UPDATED WITH Matt Bart feedback

 func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    if text == String(characterToCheck) /* \n, declared globally */ {
        characterCounter += 1 // this was also declared as a global property
    }

    if text == "" {
        let characters = Array(textView.text)
        if characters.count >= range.location {
            let deletedCharacter = characters[range.location]
            if  deletedCharacter == characterToCheck {
                characterCounter -= 1
            }
        }
    }

    if characterCounter > 4 {
        let newlineAC = UIAlertController(title: "Too many linebreaks", message: "Please go back and make your comment fit into a maximum of 5 lines", preferredStyle: .alert)
        newlineAC.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] (_) in
            let currentText = textView.text ?? ""
            guard let currentTextRange = Range(range, in: currentText) else { return }

            self?.comments.text = currentText.replacingOccurrences(of: "\n", with: "@ ", range: currentTextRange)
        })
        present(newlineAC, animated: true, completion: { [weak self] in
            self?.characterCounter -= 1
        })

        return false
    } else {
        return true
    }
}

}

Also, declare characterToCheck as a Character instead of String

characterCounter must start at 0

Upvotes: 2

Related Questions