Read/see More at the end of the label - ios

I am trying to create a read more button at the end of my label. I want it to display 3 lines by default. I am coding in swift not objective c. Only when the user clicks the read more part of the label, should the label expand. It should look and work exactly like it does on instagram except on Instagram, it is in a tableview cell. My label and read more button will be in a scrollview. I have managed to get the expanding and contracting part working by adjusting the number of lines property of the label.
if descriptionLabel.numberOfLines == 0{
descriptionLabel.numberOfLines = 3
}else {
descriptionLabel.numberOfLines = 0
}
descriptionLabel.lineBreakMode = NSLineBreakMode.byWordWrapping
I am having problems with putting a "...more" at the end of the label and cutting the text off at the right place. I have looked at other people's responses to similar questions but nothing seems to work properly.
I can put a button over the last line of text so making the see more part of the label clickable also isn't the problem. The problem I am having is truncating the text at the right place and placing the see more text at the right place so that it displays.
I also want the read more button to only appear when it is necessary. I don't want to it appear when there are only 1-3 lines of text. This is also something I am having issues with.
I can't use this https://github.com/apploft/ExpandableLabel because it does not support scrollviews just tableviews.
the swift solution here didn't work: Add "...Read More" to the end of UILabel. It crashed the app.
Finally, the read more button should be in line with the last line of text and at the end of it. It would be an added benefit it this worked in a tableview cell as well!

I found ReadMoreTextView in Github, which is based on UITextView. The key method in this library is the following:
private func characterIndexBeforeTrim(range rangeThatFits: NSRange) -> Int {
if let text = attributedReadMoreText {
let readMoreBoundingRect = attributedReadMoreText(text: text, boundingRectThatFits: textContainer.size)
let lastCharacterRect = layoutManager.boundingRectForCharacterRange(range: NSMakeRange(NSMaxRange(rangeThatFits)-1, 1), inTextContainer: textContainer)
var point = lastCharacterRect.origin
point.x = textContainer.size.width - ceil(readMoreBoundingRect.size.width)
let glyphIndex = layoutManager.glyphIndex(for: point, in: textContainer, fractionOfDistanceThroughGlyph: nil)
let characterIndex = layoutManager.characterIndexForGlyph(at: glyphIndex)
return characterIndex - 1
} else {
return NSMaxRange(rangeThatFits) - readMoreText!.length
}
}
To display text like "xxxx...Read More", the library
Get how many characters could be display in the UITextView: Use NSLayoutManager.characterRange(forGlyphRange:, actualGlyphRange:)
Get the position of the last visible character and the width of "...Read More": Use NSLayoutManager.boundingRect(forGlyphRange glyphRange: NSRange, in container: NSTextContainer)
Get the character index before trimming: Use NSLayoutManager.characterIndexForGlyph(at glyphIndex: Int)
Replace text which should be trimmed with "...Read More": UITextStorage.replaceCharacters(in range: NSRange, with attrString: NSAttributedString)

Please check :
func addSeeMore(str: String, maxLength: Int) -> NSAttributedString {
var attributedString = NSAttributedString()
let index: String.Index = str.characters.index(str.startIndex, offsetBy: maxLength)
let editedText = String(str.prefix(upTo: index)) + "... See More"
attributedString = NSAttributedString(string: editedText)
return attributedString
}
You can use like :
let str = "Lorem Ipsum is simply dummy text of the printing and typesetting industry."
descriptionLabel.attributedText = addSeeMore(str: str, maxLength: 20)
// Output : Lorem Ipsum is simpl... See More

Related

Swift 3 : How do you wrap content in a tableviewcell with multiple labels?

I've been trying to figure this out for a while. I have set constraints for each label with background color set. I set each label's line break to word wrap, but that still doesn't work. What I'm looking for is a label wrap like word wrap whether or not that exists. Thanks.
Here is a slightly different approach. You can customize the appearance of each tagged word with an attributed string. This has some limitations but depending on your requirements it could be a good fit for you. The below code is an example pointing you in the correct direction, however you still might need to write additional code for correctly wrapping the spaces or recognizing touch events.
let tags = ["Outdoors", "Working", "Learning"].map { " \($0) " }
let text = tags.joined(separator: " ")
let ranges = tags.compactMap { text.range(of: $0) }
let attributedText = NSMutableAttributedString(string: text)
for range in ranges {
attributedText.addAttributes([.backgroundColor: UIColor.green], range: NSRange(range, in: text))
}
textView.attributedText = attributedText

Scroll down to, **and a little bit past**, a desired spot in a UITextView

What I have:
Working code that scrolls to and highlights the next instance of a search term in a UITextView that contains a moderately lengthy document.
What I want:
Instead of scrolling to the desired term so that it is at the very bottom of the TextView, I'd like to scroll slightly PAST the term so that it is better visible in the view.
Where I'm stuck:
I'm weak on how Swift generally & Swift 4 in particular work with Range, and NSRange, so while I suspect that the answer will include capturing some amount of the document beyond the search term itself (say, the lesser of the next 200 characters or the next 5 line breaks), I'm not clear how to accomplish that. Or if there is a better approach to scroll the view a bit more.
Code #1 -- does the highlighting and finds "this" instance of the search term:
func attributedTextHighlighting(instanceNbr: Int, searchTerm: String, inRawHaystackText: String) -> NSMutableAttributedString {
let desiredFont = UIFont(name: "Menlo-Regular", size: self.currentDisplayFontSize)
let fontAttribute : [NSAttributedStringKey : Any] = [NSAttributedStringKey.font: desiredFont as Any]
let attributed = NSMutableAttributedString(string: inRawHaystackText, attributes: fontAttribute)
if !searchTerm.isEmpty {
do {
let foo = NSRegularExpression.escapedPattern(for: searchTerm)
let regex = try NSRegularExpression(pattern: foo, options: .caseInsensitive)
var countingMatches = 0
for match in regex.matches(in: inRawHaystackText, range: NSRange(location: 0, length:
inRawHaystackText.utf16.count)) as [NSTextCheckingResult] {
if countingMatches == instanceNbr {
// highlight this term green
attributed.addAttribute(NSAttributedStringKey.backgroundColor, value: UIColor.green, range: match.range)
theGreenMatchingRange = match.range
} else {
// highlight this term yellow
attributed.addAttribute(NSAttributedStringKey.backgroundColor, value: UIColor.yellow, range: match.range)
}
// and either way, increment the countingMatches
countingMatches += 1
}
} catch {
fatalError("Bad RegEx! \(error)")
}
} else {
print("ofSearchTerm.isEmpty = true")
}
return attributed
}
This code finds the search term in the source text & generates attributed text that highlights the term green (for the current "find") or yellow (for all others). This code works OK.
Code #2 -- scrolls to the next instance of the search term
func scrollToNextMatchButtonTapped() {
print("scrollToNextMatchButtonTapped")
currentlyHighlightedInstance += 1
if currentlyHighlightedInstance == numberOfInstances {
currentlyHighlightedInstance = 0 // since numberOfInstances is 1-based but currentlyHighlightedInstance is 0-based
}
reloadAttribTextWithHighlighting() // calls the above code, so the green highlight moves to the next found instance
textDisplay.scrollRangeToVisible(theGreenMatchingRange) // would really like to scroll to here plus a few lines (not past the end of the document, obviously)
The last line there works (theGreenMatchingRange is the global variable that's set in Code #1 above to be the range of "this" instance of the search term), but it scrolls so that the green-highlighted term is at the very bottom of the window.
What do I need to add or do differently so that the green term isn't at the very bottom?

iOS - image attachment for the text view changes attributes

I've a UITextView described as follows with the given attributes:
lazy var inputTextView: UITextView = {
let tv = UITextView()
tv.backgroundColor = .white
tv.textContainerInset = UIEdgeInsetsMake(12, 12, 12, 12) // Posicionamento do texto
let spacing = NSMutableParagraphStyle()
spacing.lineSpacing = 4
let attr = [NSParagraphStyleAttributeName : spacing, NSFontAttributeName: UIFont.systemFont(ofSize: 16), NSForegroundColorAttributeName: UIColor.blue]
tv.typingAttributes = attr
return tv
}()
Everything works as expected until I attach an image to the UITextView.
The image gets inserted in the desired position but after its inserted it overrides my textView attributes.
The text becomes small and in a different color than the attributes I've implemented in its declaration.
I'm attaching the image as follows:
let att = NSTextAttachment()
att.image = image
let attrString = NSAttributedString(attachment: att)
self.inputTextView.textStorage.insert(attrString, at: self.currentCursorLocation)
What's causing this issue?
I've even tried to reenforce its attributes whenever I insert an UIImage to its content.
I've tried the following when adding the image:
let att = NSTextAttachment()
att.image = image
let attrString = NSAttributedString(attachment: att)
self.inputTextView.textStorage.insert(attrString, at: self.currentCursorLocation)
let spacing = NSMutableParagraphStyle()
spacing.lineSpacing = 4
let attr = [NSParagraphStyleAttributeName : spacing, NSFontAttributeName: UIFont.systemFont(ofSize: 16), NSForegroundColorAttributeName: UIColor.blue]
self.inputTextView.typingAttributes = attr
And it still doesn't change its attributes.
Whats causing this issue? Any tip?
Thanks
Edit
As suggested here's how I'm setting the cursor position
func textViewDidChange(_ textView: UITextView) {
currentCursorLocation = textView.selectedRange.location
}
I do this to insert the image at the current location of the text blinking cursor
[Edit: Unfortunately this does not solve Ivan's problem - I leave the answer because it is interesting detail for those who do not understand Unicode character encoding].
String range specification is non-intuitive due to the subtleties of Unicode. I expect your issue is that the cursor position at which you are inserting your image is not where you think it is relative to the text and you are inserting the image at a Unicode scalar position that is not between Unicode code points, such that you are corrupting a unicode code. To understand why this can happen, see this Apple article.
Strings in Swift 2
I would suggest using the following notation when specifying string ranges (taken from this Stack Overflow answer: NSAttributedString and emojis: issue with positions and lengths).
// Convert to NSRange by computing the integer distances:
let nsRange = NSRange(location: text.utf16.distance(from: text.utf16.startIndex, to: from16),
length: text.utf16.distance(from: from16, to: to16))
However without seeing how you set your cursor position, it is not possible for me to be sure this is the source of your problem. [Update: thanks for updating the question to show the cursor position - we got there in the end but for others, note, after setting the cursor position this way (which would have been fine), he was incrementing it by 1, which meant the issue I have referred to about Unicode scalars versus code points was in fact the issue].

Get color changed words of attributed string

I have a UITextView that allows to select words of the text by tapping it. If it is tapped the word is highlighted in color by changing the NSForegroundColor attribute.
Tapping it again deselects it by changing the color back to the textcolor.
Now I would need to know all selected words in the UITextView.
The first idea was to remove everything that is a special character and split the text at space. Then check if the color attribute is equals to the selected/highlighted color of each seperate word.
But attributed string doesn't allow to split at a character or remove components. Neither does NSAttributedString.
Second idea was to save the ranges of the highlighted parts in an array and iterate over it to get the highlighted parts. But this seems a bit too complicated for me, especially as I need the correct order of the words as they appear which is not guaranteed with an array, add/remove on each tap
(For example let's say the text is: "This is a test"
Tap this -> index 0
Tap test -> index 1
Tap this -> test becomes index 0
Tap this -> this becomes index 1
then the order is not good anymore.
I already figured out how to get the color of an attributed string. That's not the problem.
How can I iterate over the attributed string and figure out the words that have changed color or what is the best way to solve this problem?
Thank you!
Greetings
You can iterate over the attributed string looking for the color attribute.
The follow code demonstrates how:
// This generates a test attributed string.
// You actually want the attributedText property of your text view
let str = NSMutableAttributedString(string: "This is a test of the following code")
str.addAttributes([NSForegroundColorAttributeName:UIColor.red], range: NSMakeRange(0, 4))
str.addAttributes([NSForegroundColorAttributeName:UIColor.red], range: NSMakeRange(8, 1))
str.addAttributes([NSForegroundColorAttributeName:UIColor.red], range: NSMakeRange(15, 2))
print(str)
The above prints:
This{
NSColor = "UIExtendedSRGBColorSpace 1 0 0 1";
} is {
}a{
NSColor = "UIExtendedSRGBColorSpace 1 0 0 1";
} test {
}of{
NSColor = "UIExtendedSRGBColorSpace 1 0 0 1";
} the following code{
}
This code processes the attributed string. Any range of text formatted with a foreground color will be put into the words array.
var words = [String]()
str.enumerateAttribute(NSForegroundColorAttributeName, in: NSMakeRange(0, str.length), options: []) { (value, range, stop) in
if value != nil {
let word = str.attributedSubstring(from: range).string
words.append(word)
}
}
print(words)
This prints:
["This", "a", "of"]
I can suggest you to create some kind of storage for selected ranges, then based on this range you can customize look of this words, not the other way. It will allow you to access selected words every time without checking attributes of whole text.
While I agree with Piotr that you should store the Ranges, to answer your question:
attributedString.enumerateAttributes(in: NSMakeRange(0, attributedString.length), options: []) { attributes, range, _ in
if let color = attributes[NSForegroundColorAttributeName] as? UIColor,
color == YOUR_HIGHLIGHT_COLOR {
let nString = attributedString.string as NSString
let word = nString.substring(with: range)
// Do what you want with the word
}
}

UITextField - Remove Ellipses on Text Overflow

I have a UItextfieldthat holds a person's middle name. I only want it to display the first initial, which it does, but i want it to hold their entire name. It's only large enough to show the one initial, but it adds that ellipses (...) after the letter.Is it possible to remove those when a uitextfield overflows? I haven't found anything online regarding someone with the same issue.
Thankyou for your help
I do see this truncation when the text field resigns first responder. I fixed this by setting the lineBreakMode in the NSParagraphStyle attribute to .byClipping. I happened to be using a subclass of UITextField so I overrode resignFirstResponder() to do this. My textField starts out empty so there is no attributedString to start with in viewDidLoad.
override func resignFirstResponder() -> Bool {
guard let newAttributedText = (attributedText?.mutableCopy() as? NSMutableAttributedString) else {
return super.resignFirstResponder()
}
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byClipping
newAttributedText.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: attributedText?.length ?? 0))
attributedText = newAttributedText
return super.resignFirstResponder()
}
This might not work if you set the text in code. In that case, you might want to set the lineBreak mode in a common function that you call from both the override of resignFirstResponder() and after setting the text in code. You could make a set(text: String?) function and call the common function from there.
A UITextField shouldn't be truncating the text (because you can usually scroll/select that UI element).
A UILabel will truncate by default, you can set it to clip instead.

Resources