Swift IOS Custom Action in Number Detection - ios

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.

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)
}

How does replacingCharacters work in a delegate?

I'm new to Swift and I'm learning about delegates:
class ZipCodeTextFieldDelegate: NSObject, UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
var newText = textField.text! as NSString
newText = newText.replacingCharacters(in: range, with: string) as NSString // this line
return newText.length <= 5
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true;
}
}
This is basically a delegate to limit the character count to within 5. But, I'm not quite sure what role this method replacingCharacters is playing in this function. Apple documentation shows that it
Returns a new string in which the characters in a specified range of
the receiver are replaced by a given string.
which seems pretty straight forward. It replaces some range of characters with a replacement of your choice. I can see that textField has a parameter called range and replacementString, but what range is being provided and what are they being replaced with?
The main ViewController that the delegate is being used in doesn't provide either of them. It simply instantiate the delegate and applies it to the relevant textField
let zipCodeDelegate = ZipCodeTextFieldDelegate()
// MARK: Outlets
#IBOutlet weak var textField: UITextField!
// MARK: Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
self.textField.delegate = self.zipCodeDelegate
}
First, you should understand the purpose of shouldChangeCharactersIn. Whenever the user tries to change the text of a text field in some way, the text field asks its delegates whether it "should change the characters", i.e. whether it should allow the user to do the change that they are trying to do.
Note that the user can change the textfield's text in a lot of ways, not just by "entering a single character". Here's a list of possible ways (you might be able to think of others):
entering a single character, or pasting some text
deleting a single character
selecting a range of characters, and deleting it
selecting a range of characters, and entering a single character, or pasting some text. This will replace the selected range with the single character, or pasted text
Notice that all those ways are just special cases of Way 4. In all of these ways, you are just replacing a range in the textfield's text with some new text. Let me reword the first 3 ways:
replacing a zero-length range at some position with a single character, or some pasted text
replacing a one-length range of the textfield with an empty string
replacing some range of the textfield with an empty string
When the user tries to change the textfield's text in any way, the textfield will make a call like similar to this:
delegate?.textField(self, shouldChangeCharactersIn: someRange, replacementString: someString)
You should now understand what the parameters someRange and someString are for. No matter how the user changes the text, it can always be modelled as "replacing a range of text (someRange) with some other text (someString)".
So what does
newText.replacingCharacters(in: range, with: string)
do?
It simply does the replacement mentioned before. Recall that this delegate method is the textfield asking you, "should this change (modelled as a range and a replacement string) be allowed?" And to determine that, you try to do the change first. If after the change, the text field's text has a length of at most 5, then it is allowed.
The line tries to do the change.
Of course, you are not changing the textfield's text when doing this change, which is why you assign it to a variable called newText.

Crash when adding string from regex capture to attributed string

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])

How to convert smileys to emoticon?

Is there a way to convert ":)" to 😊 like : detection and conversion, I have a UITextView in a chat application which should convert the smiley to the respective emoticon.
Get the offical list: emoji
Just an example: yourTextView.text = "your smiley: \u{1f642}"
If you want to convert emoticons to emoji on the fly either you need to specify it by yourself and analyze the input string or using a 3rd-party lib e.g. pods for converting and watching input string through text change events e.g.: docs
You can use the logic in this package npm, you find also a map of smile and the respective emoji:
https://www.npmjs.com/package/smile2emoji
I have created this simple class based on the npm package suggested by #emish89 https://www.npmjs.com/package/smile2emoji.
https://gist.github.com/lorenzOliveto/f20a89e9f68276cae21497a177ad8a4c
Swift 5
You should implement delegate textViewDidChange for your UITextView and find all need substrings in its text then replace them inside with textStorage property:
extension ViewController : UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
guard let text = textView.text else { return }
var index = text.startIndex
while let range = text.range(of: ":)", range: index..<text.endIndex) {
index = range.upperBound
textView.textStorage.replaceCharacters(in: NSRange(range, in: text), with: "😀")
}
}
}
It works while editing or paste text.

Multiple lines UILabel with multiple buttons and fonts

I'm making a UITableViewCell for some activity feed objects, to give you an idea they're going to be like the Facebook posts where you have multiple links in one post.
In my case there are going to be two links to other UIViewController for each post and one plain UILabel that connects the two and explains the connection (such as "X has commented on Y's post") where you could tap both X and Y for some actions to happen.
As right now, I just made 3 separate UILabels. The problem with that is that I'm not sure how to handle names that are too long in multiple lines.
Meaning if for example instead of X, the post was "XXXXXXXXXXXXXXXXX has commented on Y's post", then "has commented on Y's post" would need to go on another line.
As right now I just link the 3 UILabels with constraints such that they're next to each other but that wouldn't work when they're too long.
If you have any idea on how could I approach this issue, it would be really appreciated if you could let me know.
Thanks in advance.
There are too many labels, I think you can use this extension:
extension NSMutableAttributedString {
public func setAsLink(textToFind:String, linkURL:String) -> Bool {
let foundRange = self.mutableString.rangeOfString(textToFind)
if foundRange.location != NSNotFound {
self.addAttribute(NSLinkAttributeName, value: linkURL, range: foundRange)
return true
}
return false
}
}
Then you can do:
let labelFont = UIFont(name: "HelveticaNeue-Bold", size: 18)
let attributes :[String:AnyObject] = [NSFontAttributeName : labelFont!]
let attrString = NSAttributedString(string:"foo www.google.com", attributes: attributes)
let urlPath: String = "http://www.google.com"
let url: NSURL = NSURL(string: urlPath)!
attrString.setAsLink("www.google.com", linkURL:url)
myLabel.attributedText = attrString
UPDATE: (after your comments)
If you need to intercept urllink you can transform your label to textView (UITextView), set it the delegate and handle the shouldInteractWithURL method:
class ViewController: UIViewController, UITextViewDelegate {
... // on your code do:
myTextView.delegate = self
...
func textView(textView: UITextView!, shouldInteractWithURL URL: NSURL!, inRange characterRange: NSRange) -> Bool {
if URL.scheme == "http://www.google.com" {
//do whatever you want
launchMyMethodForThisUrl()
}
}
Set number of line of label to zero like,
label.numberOfLines = 0 // it will increase line when needed
And use attribute string to for different fonts and size or links etc.
and then set that string to label as,
label.attributedText = attributedString
Update :
If you want some action event on particular text then you should use UITextview instead of label. it will be more easier by implementing shouldInteractWithURL delegate of it.
Another good solution is use thirdparty library like TTTAttributedLabel. It is exact what you want i think. have a look once.
Update 2 :
refer this link or this link for your desired output
Hope this will help :)

Resources