Reputation: 410
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
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
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