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)
}
Related
I have one textview i am want to keep some text non-editable and other text editable.
FOr ex. My textview has text "My name is: Sandesh Sardar". so my name is should non editable and other text should editable. is there any method to do this in Swift 5?
Is this possible using textrange. means if I have big non editable text?
also is there any better way if i have multipe such textview?
OR how can we stop editing after certain range in textview?
Use this piece of code to get your result. It means textView would not return true while editing, if the existing text is My name is:. For example, if you want to stop after user has entered around 30 characters make a check inside like below.
extension ViewController: UITextViewDelegate {
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if let textViewText = textView.text {
if textViewText == "My name is:" && text == "" {
return false
}
//For text range
if let textRange = Range(range, in: textViewText) {
let updatedText = textViewText.replacingCharacters(in: textRange,
with: text)
return updatedText.count < 30 + 11 //11 characters for *My name is:*
}
}
return true
}
}
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
}
Let's say I have the following UITextView object:
var textView = UITextView()
textView.text = "Hello World!"
Now let's say I don't want to allow the user to delete the "W" character while editing it. How could I know which character is before the cursor (or selected by it)?
I'm looking for something that would work like this:
if textView.characterBeforeCursor() != "W" {
textView.deleteBackward()
}
or... (when the user selects the "W" character):
if textView.selectedTextContains("W") == false {
textView.deleteBackward()
}
What approach should I use to accomplish this?
Here's an idea, not fully tested, but seems to work... Just grab the character about to be acted upon and block backspace if its the target... Also with regard to selection of text, if the selection contains the target at all, we block new text.
import UIKit
class ViewController: UIViewController, UITextViewDelegate {
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
self.textView.delegate = self
// Do any additional setup after loading the view, typically from a nib.
}
func characterBeforeCursor() -> String? {
// get the cursor position
if let cursorRange = textView.selectedTextRange {
// get the position one character before the cursor start position
if let newPosition = textView.position(from: cursorRange.start, offset: -1) {
let range = textView.textRange(from: newPosition, to: cursorRange.start)
return textView.text(in: range!)
}
}
return nil
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (characterBeforeCursor() == "W") {
let char = text.cString(using: String.Encoding.utf8)!
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
return false
}
return true
}
else {
if let range = textView.selectedTextRange {
let selectedText = textView.text(in: range)
if (selectedText!.contains("W")) {
return false
}
}
return true
}
}
}
This should do it:
let forbiddenLetter = "W"
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
guard let txt = textView.text, let txtRange = Range(range, in: txt) else {
return false
}
let subString: Substring = txt[txtRange]
return !subString.contains(forbiddenLetter)
}
In the code above let txt = textView.text is just for simplicity, we could keep force-unwrapping textView.text! since the .text property is designed never returns nil for a non-nil UITextView.
By let txtRange = Range(range, in: txt) we get a variable of type Range<String.Index> instead of the vanilla NSRange that range is. By doing so we can get the Substring of txt that the textView is about to change.
Finally, the result of checking whether or not the subString contains the forbiddenLetter, is returned.
This snippet would prevent deleting W by using:
Backspace key ⌫
Deleting selection
Pasting over selection
Autocorrect (from the popup)
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.
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.