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

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.

Related

How to keep some text non editable and some editable in textview swift?

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

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

Custom images in UITextField like Venmo app

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.

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

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.

Resources