Crash when adding string from regex capture to attributed string - ios

I'm trying to create a link in an attributed string using:
matches = regex.matches(in: strings, options: [], range: NSRange(strings.startIndex..., in: strings))
for match in matches {
rangeBetweenQuotes = match.range(at: 1)
let swiftRange = Range(rangeBetweenQuotes, in: strings)!
let link:String = String(strings[swiftRange])
attributedString.addAttribute(.link, value: link, range: rangeBetweenQuotes)
}
I know the foregoing works if I simply add a font attribute rather than a link. So my regex works. However, when adding a link attribute I run into problems. When I compile the code as written above and run the app, I tap the link and get an error: Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0. It appears in the app delegate class.
As far as I can tell, last two lines of code are throwing the error.
let link:String = String(strings[swiftRange])
attributedString.addAttribute(.link, value: link, range: rangeBetweenQuotes)
To debug, I placed a breakpoint in between the last two lines of code above. I can see that the variable link contains the correct string. The string is also found in the value parameter of addAttribute.
The error is thrown during runtime when the link is tapped. I know this is the case because I can replace or assign link with a string literal, i.e. "test", and the link works fine, and I am able to use
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
to assign the "test" string literal to an outlet.
Using the debugger, I came across the following which I found in a drill-down menu within the link variable in the value parameter of addAttribute.
(BridgeObject) _object = extracting data from value failed
This suggests there is something wrong with the variable. No?
I tried using URL(string:"") to convert the link variable of type String to a URL, which did not work either.

I believe the answer concerns whether the URL type in the following function is compatible with whatever type is passed into it.
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool
Th code below works without throwing an error. Take a look at URL(fileURLWithPath: )
... let swiftRange = Range(rangeBetweenQuotes, in: strings)!
let link:String = String(strings[swiftRange])
let link2 = URL(fileURLWithPath: link)
attributedString.addAttribute(.link, value: link2, range: rangeBetweenQuotes)
The crash I kept getting did not originate from an error in executing the function addAttribute parameter value which takes Any object type. In debugging the error, I found that the parameter value in addAttribute contained the string value I passed into it. As mentioned above, the issue originated in the function:
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool
which takes a URL. While I attempted to convert the string type link to a URL using
URL(string:"")
the conversion did not work. I kept getting a null value which of course threw an error when tapping on the link. However, I can safely pass a variable into the parameter shouldInteractWith URL:URL when the string type variable
is converted to a URL using:
URL(fileURLWithPath: link)
I still do not understand why shouldInteractWith URL:URL accepts a string literal while String(string[swiftRange]), supra, does not work. Any thoughts?
EDIT ... further explanation
I know the answer to why URL type accepts one string literal while not another. A valid URL type cannot have a space in a string. URL(fileURLWithPath:) works because it fills white space with %20.
I hope this helps someone down the road.

One reason could be the conversion NSRange to Range<String.Index> and vice versa. You are strongly discouraged from using string.count and the NSString detour.
There are convenience APIs to convert the types safely
matches = regex.matches(in: string, range: NSRange(string.startIndex..., in: string))
and
let swiftRange = Range(rangeBetweenQuotes, in: string)!
let link = String(string[swiftRange])

Related

How can I disable the return key in a UITextView based on the number of empty rows in the UITextView?

Both TikTok & Instagram (iOS) have a mechanism built into their edit profile biography code which enables the user to use the return key and create separation lines in user's profile biographies. However, after a certain number of lines returned with no text in the lines, they prevent the user from using the return key again.
How can one do this?
I understand how to prevent the return key from being used if the present line the cursor is on is empty, using the following:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
guard text.rangeOfCharacter(from: CharacterSet.newlines) == nil else {
return false
}
return true
Moreover, I need help figuring out how to detect, for example, that 4 lines are empty, and stating that if 4 lines are empty, preventing the user from using the return key.
This may need fine tuning for edge cases but this would be my starting point for sure. The idea is to check the last 4 inputs into the text view with the current input and decide what to do with it. This particular code would prevent the user from creating a fourth consecutive empty line.
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text == "\n",
textView.text.hasSuffix("\n\n\n\n") {
return false
}
return true
}
I am not completely sure I have understood your question correctly. But let me try to help.
A UITextView has a text property that you can read. Then, if a user enters a new character/inserts new text (make sure to test this with copy-pasting text), you could check whether the last three characters of the text are newlines or not. If so, and the user is trying to add another newline, you know to return false.
This would look like this:
let lastThreeCharacters = textView.text.suffix(3)
let lastThreeAreNewlines = (lastThreeCharacters.count == 3) && lastThreeCharacters.allSatisfy( {$0.isNewline} ) // Returns true if the last 3 characters are newlines
You'd need to implement some additional checks. Is the character that's going to be inserted a newline? If the user pastes text, will the last 4 characters be newlines?
Another method would be to make use of another method of the UITextViewDelegate. You could also implement textViewDidChange(_:), which is called after a user has changed the text. Then, check if (and where) the text contains four new lines and replace them with empty characters.
This would look something like this (taken from here):
func textViewDidChange(_ textView: UITextView) {
// Avoid new lines also at the beginning
textView.text = textView.text.replacingOccurrences(of: "^\n", with: "", options: .regularExpression)
// Avoids 4 or more new lines after some text
textView.text = textView.text.replacingOccurrences(of: "\n{4,}", with: "\n\n\n", options: .regularExpression)
}

Swift IOS Custom Action in Number Detection

In our App, we have an outbound VOIP phone dialer.
I would like to detect numbers within a string and add a custom action so that instead of the default behaviour to open the native dialer, it will go to our own dialler within the App and pre-populate the selected number.
My first thoughts were to get the body of the message into a string array of words and check each word to see if it is a number and then create an attributed string. But I'm struggling to understand how I can get an attributed string back into a string and what I would even specify to open the said screen with the number pre-populated.
I am aware of NSDataDetector which can list the numbers found in the string but I am stuck with how to replace those particular parts with a clickable action and return it back as a string.
If anyone has had similar experiences with this then any help would be much appreciated?
NOTE : The body of this message is being show in a UILabel control.
Update
this is what I had so far...
func AddNumberLink() -> String {
let body = self
let wordsInBody = body.components(separatedBy: .whitespaces)
for i in 0..<wordsInBody.count {
var word = wordsInBody[i]
if word.isTelephoneNumeric {
let attributedString = NSMutableAttributedString(string:word)
attributedString.addAttribute(NSAttributedStringKey.link, value:"https://www.google.com",range: NSRange(location: 0, length: word.count))
attributedString.addAttribute(NSAttributedStringKey.foregroundColor,value: UIColor.red, range: NSRange(location:0, length: word.count))
word = attributedString.string
}
}
return body
}
NSDataDetector gives you the ranges of the numbers. Now, create a (mutable) attributed string and for each range, add a link to the corresponding number URL (NSAttributedString.Key.link: urlString).
Then, display the text in a UITextView and add a delegate.
In the delegate, implement func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool
In this function, you can decide what action is appropriate.
Thank you all for your suggestions it has made me approach the problem in a different manner to my code above.
Like #Lutz and #Larme suggested above, by overriding the default behaviour when the UITextView is tapped I can gather the number via the UITextViewDelegate.
So the complete solution is :
Add number detector attribute to UITextView
Implement UITextViewDelegate func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool
Add custom logic to handle the tap accordingly.

Changing font of strings separated by spaces

I'm trying to make the words split by spaces green in a UITextField, kind of like the way it works when you compose of a new iMessage. I commented out the part of my code that's giving me a runtime error. Please let me know if you have any ideas:
func textChanged(sender : UITextField) {
var myMutableString = NSMutableAttributedString()
let arr = sender.text!.componentsSeparatedByString(" ")
var c = 0
for i in arr {
/*
myMutableString.addAttribute(NSForegroundColorAttributeName, value: UIColor.greenColor(), range: NSRange(location:c,length:i.characters.count))
sender.attributedText = myMutableString
*/
print(c,i.characters.count)
c += i.characters.count + 1
}
}
Your code has at least two parts needed to be fixed.
var myMutableString = NSMutableAttributedString()
This line creates an empty NSMutableAttributedString. Any access to the content may cause runtime error.
The other is i.characters.count. You should not use Character based locations and counts, when the APIs you want use is based on the behaviour of NSString. Use UTF-16 based count.
And one more, this is not critical, but you should use sort of meaningful names for variables.
So, all included:
func textChanged(sender: UITextField) {
let text = sender.text ?? ""
let myMutableString = NSMutableAttributedString(string: text)
let components = text.componentsSeparatedByString(" ")
var currentPosition = 0
for component in components {
myMutableString.addAttribute(NSForegroundColorAttributeName, value: UIColor.greenColor(), range: NSRange(location: currentPosition,length: component.utf16.count))
sender.attributedText = myMutableString
print(currentPosition, component.utf16.count)
currentPosition += component.utf16.count + 1
}
}
But whether this works as you expect or not depends on when this method is called.
You create an empty attributed string but never install any text into it.
The addAttribute call apples attributes to text in a string. If you try to apply attributes to a range that does not contain text, you will crash.
You need to install the content of the unattributed string into the attributed string, then apply attributes.
Note that you should probably move the line
sender.attributedText = myMutableString
Outside of your for loop. There is no good reason to install the attributed string to the text field repeatedly as you add color attributes to each word.
Note this bit from the Xcode docs on addAttribute:
Raises... an NSRangeException if any part of aRange lies beyond the
end of the receiver’s characters.
If you are getting an NSRangeException that would be a clue as to what is wrong with your current code. Pay careful attention to the error messages you get. They usually offer important clues as to what's going wrong.

Understanding `var status = (string: statusVal as NSString)` variable declaration in Swift

I found the following code in a project that compiles and executes successfully. But I am unable to understand how it works. I tried Googling it using a variety of search phrases but couldn't find an explanation.
let statusVal = "Somestring"
var status = (string: statusVal as NSString)
Can someone clarify what is going on in the second line ?
According to what little knowledge I have in Swift , the second line should be something like
var status = NSString(string: statusVal as NSString)
which of course compiles as well.
While,it is just a tuple with one element
var status = (abcdefg:"abc")
The part abcdefg is description, and "abc" is value.
If a tuple has only one element,it used the type of the element.So the type of status is String
More document about tuple
https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/TheBasics.html
What is really happening is var status = statusVal as NSString, the string: part is just giving the variable an association, which is ignored when the code is executed. You can put any word you want in place of string: and the code will still compile.

Swift 1.2 NSTextStorage removeAttribute with Range<String.Index>

I have a subclass of NSTextStorage and I'm trying to remove the foreground color of a paragraph the following way:
var paragraphRange = self.string.paragraphRangeForRange(
advance(self.string.startIndex, theRange.location)..advance(self.string.startIndex, theRange.location + theRange.length))
self.removeAttribute(NSForegroundColorAttributeName, range: paragraphRange)
However, I get the following error Cannot invoke 'removeAttribute' with an argument list of type '(String, range: (Range<String.Index>))'
Help Please. I think TextKit on Swift is a mess. Some methods receive/return NSRange but String works with Range<String.Index> making it a hell to work with.
The problem here is that the NSString returned by self.string
is automatically bridged to a Swift String. A possible solution is
to convert it back to NSString explicitly:
func removeColorForRange(theRange : NSRange) {
let paragraphRange = (self.string as NSString).paragraphRangeForRange(theRange)
self.removeAttribute(NSForegroundColorAttributeName, range: paragraphRange)
}
Note also that the range operator .. has been replaced by ..<
in newer Swift versions (to avoid confusion with ... and to
emphasize that the upper bound is not included).

Resources