Detecting empty lines and absence of text in a UITextView in Swift - ios

I have created a UITextView programmatically , and added a placeholder like so:
//textView
textView.frame = CGRectMake(viewSize.width * 0.05, contentView.frame.height - viewSize.width * 0.05, viewSize.width - viewSize.width * 0.05 * 2, -viewSize.height * 0.220833)
self.textView.delegate = self
self.textView.text = "Agregue una descripción..."
self.textView.textColor = UIColor.lightGrayColor()
self.textView.autocorrectionType = UITextAutocorrectionType.No
self.textView.layer.cornerRadius = 6
self.view.addSubview(textView)
and in the delegate functions:
func textViewDidBeginEditing(textView: UITextView) {
if textView.textColor == UIColor.lightGrayColor() {
textView.text = ""
textView.textColor = UIColor.blackColor()
}
}
func textViewDidEndEditing(textView: UITextView) {
if textView.text.isEmpty {
textView.text = "Agregue una descripción..."
textView.textColor = UIColor.lightGrayColor()
}
}
I have also tried to check string text to nil:
if textView.text == nil
And like this:
if textView.text == ""
None did detected the empty lines, and even the nil comparison was not detecting the absence of text after adding and deleting some chars.
So when the user enters text there is no problem, and when the user deletes all the text the placeholder comes back, but if you add a line or more without any text, the comparison to empty is not true, i need a way to detect that i have no text and also the lines added do not have text, because the newly introduced in Swift .isEmpty is not detecting empty lines added, nor any of the other comparison did.
How can i detect empty lines in my String?

Ask yourself whether the text is composed of only characters from the NSCharacterSet whitespaceAndNewlineCharacterSet (or whatever characters constitute emptiness in your mind - perhaps what you are after is newlineCharacterSet). The easiest way is to take its invertedSet and call rangeOfCharacterFromSet - if the result is {NSNotFound, 0} (Swift nil), this text is composed only of whitespace characters.
extension String {
var isBlank : Bool {
let s = self
let cset = NSCharacterSet.newlineCharacterSet().invertedSet
let r = s.rangeOfCharacterFromSet(cset)
let ok = s.isEmpty || r == nil
return ok
}
}
"howdy".isBlank // false
"".isBlank // true
"\n\n".isBlank // true

"quick and dirty" version of the code matt posted, without extending String:
func textViewDidEndEditing(textView: UITextView) {
let myAwesomeDescription = textView.text.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSe‌​t())
if myAwesomeDescription == "" {
//user did not modify empty box or text only consisted of space/newline characters, reset placeholder text and placeholder textColor
textView.text = "Agregue una descripción..."
textView.textColor = UIColor.lightGrayColor()
} else {
//textView holds a valid string, do stuff
}
}
What does myAwesomeDescription do? We refer to a great NSHipster article for that:
Stripping Whitespace
NSString -stringByTrimmingCharactersInSet: is a
method you should know by heart. It's most often passed NSCharacterSet
+whitespaceCharacterSet or +whitespaceAndNewlineCharacterSet in order to remove the leading and trailing whitespace of string input.
It's important to note that this method only strips the first and last
contiguous sequences of characters in the specified set.
That being said, this not only helps us dealing with Strings that have unnecessary leading or following spaces/newlines, but also with "empty" which in most definitions will mean that the String is just spaces/newlines only.

Related

How to detect if a user typed an ellipsis in a UITextField?

How can you check if you string has an ellipsis in swift? I have noticed some odd behavior when using the swift string functions and learned that ellipsis are to blame. When a user enters ... as part of the string in a UITextField and then I try to locate a string after that the character count is always off by 2. This is because the string functions are treating ... as 3 characters when locating the index of the string I am searching for, but the character count functions are treating it as 1 character. The solution is pretty simple... when I have an elliptical in the string then adjust the "found" index of the string by 2. The issue is I don't know how to search "does this string have an ellipsis" because this didn't find it:
if heading.contains("...") {
print ("found the ...!")
}
I suspect there is a special way to search for an ellipsis but haven't been able to find out what it is. This is my "find the last space after substringing out the first 30 characters" function that works for strings that don't have an ellipsis:
func breakOutHeadingFromString(fullString: String, charBreakPoint: Int) -> (heading: String, remainingText: String, breakChar: String) {
var heading = fullString
var remainingText = ""
var breakChar = ""
// If the Full string has less characters than break point then just return full blurb as heading and blank out 2
if fullString.count > charBreakPoint {
// Get N characters out of total char count (hardcoded to 30 can this be dynamic?)
var prefix = String(fullString.prefix(charBreakPoint))
// Find the last space in heading so you can continue message there
let lastSpace = prefix.lastIndex(of: " ") ?? prefix.endIndex
var breakPoint = lastSpace
breakChar = "|"
// If there is a /n clip there
if let newLine = prefix.firstIndex(of: "\n") {
prefix = String(prefix[..<newLine])
breakPoint = newLine
breakChar = "\n"
}
// Use the Break Point to split the message in 2
let breakPointInt: Int = fullString.distance(from: fullString.startIndex, to: breakPoint)
// if the string has a eliptical ... then the break point is off by 2 because it 1 char in but 3 in
heading = String(fullString.prefix(breakPointInt))
remainingText = String(fullString.suffix(fullString.count - breakPointInt - 1))
}
return (heading,remainingText,breakChar)
}
The ellipsis is 1 unicode character, not 3 so it is counted as 1 character and below is what I think is happening in your situation.
This did not find it
if heading.contains("...") {
print ("found the ...!")
}
Because these are 3 periods (3 characters) and different from the ellipsis character (1 character)
Try highlighting with a mouse what you are comparing (...) with the actual ellipsis character (…)
In the first instance, you can highlight each of the dots individually using your mouse and in the second scenario you will not be able to select each individual dot.
Here is some test:
var ellipsisString = "This is … a"
var threeDotString = "This is ... a"
print("Ellipsis character count: \(ellipsisString.count)")
print("Three dot character count: \(threeDotString.count)")
// The output:
Ellipsis character count: 11
Three dot character count: 13
As you can see, with the proper ellipsis character, it counts it as only 1 character
Now using your contains function with the ellipsis string:
var ellipsisString = "This is … a"
print(ellipsisString.contains("…"))
print(ellipsisString.contains("..."))
// The output
true
false
You can see contains("…") succeeds with the real ellipsis character but contains("...") fails with the three dots you used.
Finally, let's say I wanted to add the string nice after the ellipsis character in the ellipsis string This is … a - your strategy will not work of adding 2 to the index if a proper ellipsis character was used
Here is what I do to achieve this:
var ellipsisString = "This is … a"
// Find the index of the ellipsis character
if let ellipsisIndex = ellipsisString.firstIndex(of: "…")
{
// Convert the String index to a numeric value
let numericIndex: Int
= ellipsisString.distance(from: ellipsisString.startIndex,
to: ellipsisIndex)
// Ellipsis located at the 8th index which is right
print(numericIndex)
// Set the index where we want to add the new text
// The index after the ellipsis character so offset by 1
let insertIndex = ellipsisString.index(ellipsisIndex,
offsetBy: 1)
// Insert the text " nice" into the string after the
// ellipsis character
ellipsisString.insert(contentsOf: " nice",
at: insertIndex)
}
// Print the output after the addition
print(ellipsisString)
// Output
This is … nice a
This gives the desired output you wish which is finding the position of the ellipsis character accurately and then using that position to do what you want, in my case, adding a text after the ellipsis character.
Hope this clears some things up for you
Update With Example
Here is a small update to detect an ellipsis or a period added by the user in a UITextField in real time. The ellipsis will be highlighted in yellow
1. Set Up with a UITextField
class EllipsisViewController: UIViewController
{
let textField = UITextField()
override func viewDidLoad()
{
super.viewDidLoad()
// Basic set up
view.backgroundColor = .white
title = "Ellipses text"
configureTextField()
}
private func configureTextField()
{
// UITextField with auto layout
textField.layer.borderWidth = 2.0
textField.delegate = self
textField.layer.borderColor = UIColor.blue.cgColor
textField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(textField)
textField.leadingAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor,
constant: 16)
.isActive = true
textField.topAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
.isActive = true
textField.trailingAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor,
constant: -16)
.isActive = true
textField.heightAnchor
.constraint(equalToConstant: 70)
.isActive = true
}
}
Detect ellipsis in a string using UITextFieldDelegate and searching character by character
extension EllipsisViewController: UITextFieldDelegate
{
func textFieldDidChangeSelection(_ textField: UITextField)
{
highlightEllipsisAndPeriods()
}
private func highlightEllipsisAndPeriods()
{
if let text = textField.text
{
let attributedText = NSMutableAttributedString(string: text)
let ellipsisCharacter: String.Element = "…"
for (index, character) in text.enumerated()
{
let attributedKey = NSAttributedString.Key.backgroundColor
// Highlight ellipsis in yellow
if character == ellipsisCharacter
{
attributedText.addAttribute(attributedKey,
value: UIColor.yellow,
range: NSRange(location: index,
length: 1))
}
// Highlight periods in green
if character == "."
{
attributedText.addAttribute(attributedKey,
value: UIColor.green,
range: NSRange(location: index,
length: 1))
}
}
textField.attributedText = attributedText
}
}
}
This gives you the following result
If you have a single pattern you want to find like an ellipsis, you have the option to use NSRegularExpression as well
That would give the same result as above, here is the function:
extension EllipsisViewController: UITextFieldDelegate
{
func textFieldDidChangeSelection(_ textField: UITextField)
{
//highlightEllipsisAndPeriods()
highlightEllipsisUsingRegex()
}
private func highlightEllipsisUsingRegex()
{
if let text = textField.text
{
let attributedText = NSMutableAttributedString(string: text)
let ellipsisString = "…"
do
{
let regex = try NSRegularExpression(pattern: ellipsisString,
options: .caseInsensitive)
// Loop through all the ellipsis found and highlight
// in yellow
for match in regex.matches(in: text,
options: NSRegularExpression.MatchingOptions(),
range: NSRange(location: 0,
length: text.count))
as [NSTextCheckingResult]
{
attributedText.addAttribute(NSAttributedString.Key.backgroundColor,
value: UIColor.yellow,
range: match.range)
}
textField.attributedText = attributedText
}
catch
{
// handle errors
print("error")
}
}
}
}
This is the result using the Regex method:

Strange behavior while calling UITextViewDelegate ShouldTextChangeIn

I have a UITableViewCell where i have a comments UITextView to write comments. While writing down the comments i check out for #hashtags and #mentions to check their availability on the server side of the app and display them in a tableView.
The issue is that once the delegate method is called i cannot reset it to check in other if statements. For instance the " " space character "If statement is never called if i started my text by a #hashtag" so i cannot know if it is the end of a #hashtag or a #mention. And also #hashtag and " " if statements are never called if i started my text with #mention
The ranging and everything is working perfectly fine but when i type space it is not reseting to start a new #hashtag or #mention
Do i need to reset the range or what do i have to do?
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if textView == postCommentTextView {
if textView.text == "#" {
recordingHash = true
recordingMention = false
startParseHash = range.location
} else if textView.text == "#" {
recordingHash = false
recordingMention = true
startParseMention = range.location
} else if textView.text == " " {
recordingHash = false
recordingMention = false
createdHashesArray.append(createdHashString)
createdHashString = ""
}
if recordingHash != nil {
if recordingHash == true {
var value = String()
print("COUNT: \(textView.text.count)")
if startParseHash <= (textView.text.count - startParseHash) {
let createdRange = NSMakeRange(startParseHash, textView.text.count - startParseHash)
value = textView.text(in: createdRange.toTextRange(textView)!)! //as! NSString
createdHashString = "\(value)"
// print("hash:\(createdHashString)")
}
print("hash:\(createdHashString)")
if textView.text.count == 0 {
recordingHash = false
recordingMention = false
// createdHashesArray.append(createdHashString)
createdHashString = ""
}
}
}
return true
}
When you check for the character you should be using text variable not textView.text. text gives you the current character (or whole text that was pasted in) textview.text gives you the whole text

How to break selection by paragraphs (Medium like) in an iOS App?

How to separate paragraphs inside a UITextView into completely isolated text clusters such as when doing selection you can only select words inside that paragraph?
In this case you could only select text "You obliged. "
I´m experimenting with selection cancelation when outside the paragraph, doing the required maths to define paragraph scope, but no luck so far.
The idea is to locate extension of current paragraph from cursor
position when starting to select the text. Then allow only the
intersection between ranges of the paragraph and the one corresponding to the selection.
This is the solution as of Swift 3
class RichTextView: UITextView {...}
extension RichTextView: UITextViewDelegate {
func textViewDidChangeSelection(_ textView: UITextView) {
let range = textView.selectedRange
if range.length > 0 {
if let maxRange =
textView.attributedText.getParagraphRangeContaining(cursorPosition: range.location){
selectedRange = NSIntersectionRange(maxRange, range)
}
}
}
}
extension NSAttributedString {
func getParagraphRangeContaining(cursorPosition: Int) -> NSRange? {
let cursorPosition = cursorPosition - 1
let nsText = self.string as NSString
let textRange = NSMakeRange(0, nsText.length)
var resultRange : NSRange?
nsText.enumerateSubstrings(in: textRange, options: .byParagraphs, using: {
(substring, substringRange, _, _) in
if (NSLocationInRange(cursorPosition , substringRange)) {
resultRange = substringRange
return
}
})
return resultRange
}
}
If I understand your question correctly, I would try creating a UITextView for each paragraph and positioning them correctly. Create a new one when the user presses enter (and make sure to preserve the text after their cursor), and join the contents of the two adjacent views if they press delete with their cursor positioned at the beginning of the second one.
That way, selection would work in each view, but the user could not select across two views at once.

Adding color to a word in string using NSMutableAttributedString

I am trying to add color to 2 words in a string. This is the code I am using:
var HighScore:Int = 0
var CurrentScore:Int = 0
let stringOne = "You have managed to score \(CurrentScore). Current record is \(self.HighScore). Play again and give it another try!"
let stringTwo = "\(CurrentScore)"
let stringThree = "\(HighScore)"
let range1 = (stringOne as NSString).range(of: stringTwo)
let range2 = (stringOne as NSString).range(of: stringThree)
let attributedText = NSMutableAttributedString.init(string: stringOne)
attributedText.addAttribute(NSForegroundColorAttributeName, value: UIColor.init(netHex: 0x00b4ff) , range: range1)
attributedText.addAttribute(NSForegroundColorAttributeName, value: UIColor.init(netHex: 0x00b4ff) , range: range2)
gameOverDescriptionLabel.attributedText = attributedText
The problem I have is that if CurrentScore and HighScore is the same(ex: 2 & 2) the color on the range2 still stays white, but if they are not equal(2 & 1 or 1 & 2) both gets the color I have choosen.
Any suggestions?
Add this to the top or bottom of your .swift file:
extension NSMutableAttributedString {
func bold(_ text:String) -> NSMutableAttributedString {
let attrs:[String:AnyObject] = [NSForegroundColorAttributeName: UIColor.init(netHex: 0x00b4ff)]
let boldString = NSMutableAttributedString(string:"\(text)", attributes:attrs)
self.append(boldString)
return self
}
func normal(_ text:String)->NSMutableAttributedString {
let normal = NSAttributedString(string: text)
self.append(normal)
return self
}
}
To code below is the usage, you can edit it how you'd like, but I have made it so you can easy just copy&paste it to your project:
let formattedString = NSMutableAttributedString()
formattedString
.normal("You have managed to score ")
.bold("\(CurrentScore)")
.normal(". Current record is ")
.bold("\(HighScore)")
.normal(". Play again and give it another try!")
gameOverDescriptionLabel.attributedText = formattedString
If the current score and high score are the same string, searching for the latter finds the former (because the search starts at the beginning).
There are lots of other, better ways to do this.
Perform the second search in a range starting after the result of the first search (range(of:) has a range: parameter, but you are not using it)
Instead of looking for the range of the high score, search for the surrounding boilerplate ("You have managed to score" and so on) and figure out where the numbers must be.
Use NSScanner or regular expressions to find the numerical expressions embedded in the string.
My favorite: mark each of the numbers with an "invisible" attribute and search for that attribute so that you can find the numbers reliably (example here).
A solution without searching for the range would be to create 2 separate NSMutableAttributedString for current score and high score, and then append everything together.
let currentScoreString = NSMutableAttributedString(...)
let highscoreString = NSMutableAttributedString(...)
let finalString = NSMutableAttributedString(string: "You have managed to score ").append(currentScoreString)....
//MARK: forgroundColor
open var foregroundColor: UIColor? {
didSet {
textAttributes[NSForegroundColorAttributeName] = foregroundColor
self.attributedText = NSAttributedString(string: self.text, attributes: textAttributes)
}
}
//MARK: backgroundColor
open var textBackgroundColor: UIColor? {
didSet {
textAttributes[NSBackgroundColorAttributeName] = textBackgroundColor
self.attributedText = NSAttributedString(string: self.text, attributes: textAttributes)
}
}

Detecting special character tap in UITextView [Swift]

I'm trying to build a UITextView in iOS8 that recognizes taps on specific words, specifically words preceded by the "#" and "# symbols"
I first tried the following method in a subclass of UITextView:
var point = tapGesture.locationInView(self)
var position = closestPositionToPoint(point)
let range = tokenizer.rangeEnclosingPosition(position, withGranularity: .Word, inDirection: 1)
let word = textInRange(range)
println(word!)
However, clicking on a word in a text view prints the word, but will leave out the "#" and "#", I believe this to be from the .Word granularity not recognizing special characters. I came up with a work around that uses attributed text to identify the prefixed special character.
var point = tapGesture.locationInView(self)
var position = closestPositionToPoint(point)
let range = tokenizer.rangeEnclosingPosition(position, withGranularity: .Word, inDirection: 1)
if range != nil {
let location = offsetFromPosition(beginningOfDocument, toPosition: range!.start)
let length = offsetFromPosition(range!.start, toPosition: range!.end)
let attrRange = NSMakeRange(location, length)
let attrText = attributedText.attributedSubstringFromRange(attrRange)
let word = attributedText.attributedSubstringFromRange(attrRange)
let isHashtag: AnyObject? = word.attribute("Hashtag", atIndex: 0, longestEffectiveRange: nil, inRange: NSMakeRange(0, word.length))
let isAtMention: AnyObject? = word.attribute("Mention", atIndex: 0, longestEffectiveRange: nil, inRange: NSMakeRange(0, word.length))
if isHashtag != nil {
println("#\(word.string)")
} else if isAtMention != nil {
println("#\(word.string)")
}
}
And this works pretty well, but tapping on the special character will not print out the word. Does anyone have possible solution to this problem? Is there a different way to identify tapped words without using rangeEnclosingPosition?

Resources