Custom images in UITextField like Venmo app - ios

I am wondering how Venmo places custom emoticons into their textfield.
When you copy these images and paste them elsewhere, they show up as ":sunset:", ":concert:", etc.
So my guess is the textField delegate checks for any text that matches that pattern (i.e. ":concert:") and replaces it with a tiny image.
So I am wondering how you can place your own little UIImageView within a textField alongside other text.
Edit: This could also be a UITextView now that I think about it

The text input in the screenshot is almost definitely a custom subclass of UITextView, and here I'll present one way to achieve the desired result with just that.
Here's a short demonstration, copying text containing a custom image from one UITextView to another:
First we'll need to subclass NSTextAttachment to have a textual representation of the image at hand, which we'll later use when copying.
class TextAttachment: NSTextAttachment {
var representation: String?
}
Now when we create an attributed string containing the image, we'll add the desired textual representation of the image to the attachment:
let attachment = TextAttachment()
attachment.image = UIImage(named: "1f197")
attachment.representation = ":anything-here:"
Next, we'll subclass UITextView and override the copy(_:) method declared in UIResponderStandardEditActions which UITextView implements.
class TextView: UITextView {
override func copy(_ sender: Any?) {
let selectedString = self.attributedText.attributedSubstring(from: self.selectedRange)
let enumeratableRange = NSRange(location: 0, length: selectedString.length)
let result = NSMutableAttributedString(attributedString: selectedString)
selectedString.enumerateAttribute(NSAttachmentAttributeName, in: enumeratableRange, options: []) { (value, range, _) in
if let attachment = value as? TextAttachment, let representation = attachment.representation {
result.replaceCharacters(in: range, with: representation)
}
}
UIPasteboard.general.string = result.string
}
}
We could also override a few other methods, such as cut(_:) and paste(_:), but that's outside the scope of the question.
Finally, let's add some attributed text to an instance of the custom text view to see how it performs in action:
var textView: TextView // Create an instance however.
let mutableString = NSMutableAttributedString()
mutableString.append(NSAttributedString(string: "Text with "))
mutableString.append(NSAttributedString(attachment: attachment))
mutableString.append(NSAttributedString(string: " text attachment."))
self.textView.attributedText = mutableString
Obviously it would be more intuitive to convert text/emoji/whatever into attachments on the fly while the user is typing.

Related

iOS: insert attributed string at cursor for UITextView?

UITextView lets you insert plain text at the cursor with the insertText function. Is there a clean way to do so with attributed text?
Is the only approach to split the attributedText property into two parts -- pre-cursor and post-cursor -- then append the new attributed string to the pre-cursor attributed text, followed by appending the post-cursor attributed text?
Insert into a mutable copy of the text view’s attributed text by calling https://developer.apple.com/documentation/foundation/nsmutableattributedstring/1414947-insert.
As advised by #matt, here's a Swift 4.x function:
fileprivate func insertAtTextViewCursor(attributedString: NSAttributedString) {
// Exit if no selected text range
guard let selectedRange = textView.selectedTextRange else {
return
}
// If here, insert <attributedString> at cursor
let cursorIndex = textView.offset(from: textView.beginningOfDocument, to: selectedRange.start)
let mutableAttributedText = NSMutableAttributedString(attributedString: textView.attributedText)
mutableAttributedText.insert(attributedString, at: cursorIndex)
textView.attributedText = mutableAttributedText
}

Coloring text in UITextView with NSAttributedString is really slow

I'm making a simple code viewer / editor on top of a UITextView and so I want to color some of the keywords (vars, functions, etc...) so it's easy to view like in an IDE. I'm using NSAttributedString to do this and coloring in range using the functions apply(...) in a loop (see below). However, when there are a lot of words to color it starts becoming really slow and jamming the keyboard (not so much on the simulator but its really slow on an actual device). I thought I could use threading to solve this but when I run the apply function in DispatchQueue.global().async {...} it doesn't color anything at all. Usually if there's some UI call that needs to run in the main thread it will print out the error / crash and so I can find where to add DispatchQueue.main.sync {...} and I've tried in various places and it still doesnt work. Any suggestions on how I might resolve this?
Call update
func textViewDidChange(_ textView: UITextView) {
updateLineText()
}
Update function
var wordToColor = [String:UIColor]()
func updateLineText() {
var newText = NSMutableAttributedString(string: content)
// some values are added to wordToColor here dynamically. This is quite fast and can be done asynchronously.
// when this is run asynchronously it doesn't color at all...
for word in wordToColor.keys {
newText = apply(string: newText, word: word)
}
textView.attributedText = newText
}
Apply functions
func apply (string: NSMutableAttributedString, word: String) -> NSMutableAttributedString {
let range = (string.string as NSString).range(of: word)
return apply(string: string, word: word, range: range, last: range)
}
func apply (string: NSMutableAttributedString, word: String, range: NSRange, last: NSRange) -> NSMutableAttributedString {
if range.location != NSNotFound {
if (rangeCheck(range: range)) {
string.addAttribute(NSAttributedStringKey.foregroundColor, value: wordToColor[word], range: range)
if (range.lowerBound != 0) {
let index0 = content.index(content.startIndex, offsetBy: range.lowerBound-1)
if (content[index0] == ".") {
string.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.purple, range: range)
}
}
}
let start = last.location + last.length
let end = string.string.count - start
let stringRange = NSRange(location: start, length: end)
let newRange = (string.string as NSString).range(of: word, options: [], range: stringRange)
apply(string: string, word: word, range: newRange, last: range)
}
return string
}
This will be more of some analysis and some suggestions rather than a full code implementation.
Your current code completely rescans the all of the text and reapplies all of the attributes for each and every character the user types into the text view. Clearly this is very inefficient.
One possible improvement would be to implement the shouldChangeTextInRange delegate. Then you can start with the existing attributed string and then process only the range being changed. You might need to process a bit of the text on either side but this would be much more efficient than reprocessing the whole thing.
You could combine the two perhaps. If the current text is less than some appropriate size, do a full scan. Once it reaches a critical size, do the partial update.
Another consideration is to do all scanning and creation of the attribute string in the background but make it interruptible. Each text update your cancel and current processing and start again. Don't actually update the text view with the newly calculated attributed text until the user stops typing long enough for your processing to complete.
But I would make use of Instruments and profile the code. See what it taking the most time. Is it find the words? Is it creating the attributed string? Is it constantly setting the attributedText property of the text view?
You might also consider going deeper into Core Text. Perhaps UITextView just isn't well suited to your task.
I have a logger functionality in which i log all the service calls that i have made and can search for a particular string in that logs. I display the text in the textfield and highlight a text when searched for. I use below func with Regex and its not slow. Hope it helps you.
func searchText(searchString: String) {
guard let baseString = loggerTextView.text else {
return
}
let attributed = NSMutableAttributedString(string: baseString)
do {
let regex = try! NSRegularExpression(pattern: searchString,options: .caseInsensitive)
for match in regex.matches(in: baseString, options: NSRegularExpression.MatchingOptions(), range: NSRange(location: 0, length: baseString.count)) as [NSTextCheckingResult] {
attributed.addAttribute(NSBackgroundColorAttributeName, value: UIColor.yellow, range: match.range)
}
attributed.addAttribute(NSFontAttributeName, value: UIFont.regularFont(ofSize: 14.0), range: NSRange(location: 0, length: attributed.string.count))
self.loggerTextView.attributedText = attributed
}
}

Get selected text in a UITextView

I have a UITextView, and I want to allow the user to highlight a portion of text and copy it with a button instead of using the default Apple method. The problem is that I can't get the text within the selected range.
Here's what I have:
#IBAction func copyButton(_ sender: Any) {
let selectedRange: UITextRange? = textView.selectedTextRange
selectedText = textView.textInRange(selectedRange)
UIPasteboard.general.string = selectedText
}
But I'm getting
UITextView has no member textInRange
and I'm not sure what I should be using instead.
What is happening is that UITextView method textInRange have been renamed to text(in: Range) since Swift 3. Btw you forgot to add the let keyword in your sentence:
if let range = textView.selectedTextRange {
UIPasteboard.general.string = textView.text(in: range)
}

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.

How do you copy an image and paste it into a UITextView?

I've tried so many different things based on what I found online and still can't do this. There are similar questions that I have tried to piece together but it's still not working. Here is what I have so far:
class CustomUITextView: UITextView {
override func paste(sender: AnyObject?) {
let data: NSData? = UIPasteboard.generalPasteboard().dataForPasteboardType("public.png")
if data != nil {
let attributedString = self.attributedText.mutableCopy() as! NSMutableAttributedString
let textAttachment = NSTextAttachment()
textAttachment.image = UIImage(data: data!)
let attrStringWithImage = NSAttributedString(attachment: textAttachment)
attributedString.replaceCharactersInRange(self.selectedRange, withAttributedString: attrStringWithImage)
self.attributedText = attributedString
} else {
let pasteBoard: UIPasteboard = UIPasteboard.generalPasteboard()
let text = NSAttributedString(string: pasteBoard.string!)
let attributedString = self.attributedText.mutableCopy() as! NSMutableAttributedString
attributedString.replaceCharactersInRange(self.selectedRange, withAttributedString: text)
self.attributedText = attributedString
}
}
}
Also, my IBOutlet for the textView is in another file and is of this custom type I created:
#IBOutlet var textView: CustomUITextView!
I created a subclass of UITextView and assigned it to my textview. I then override the paste function to allow for images since I read that UITextField does not support image pasting by default. This is currently working for the text but not the image ---> if I do a long press gesture to show the Menu, the "Paste" button never shows up if there is an image in the PasteBoard; it only shows if there is text.
Ultimately, I want to paste PNG, JPEGS, and images from URLS in my textview. Someone please help !!

Resources