Find CGPoint Location of substring in TextView - ios

I'm looking to create a SpriteKit Node positioned at the location of a substring inside a UITextView. How would I retrieve the CGPoint location so I can position the SKNode there?
let textFont = [NSFontAttributeName: UIFont(name: "GillSansMT", size: 30.0) ?? UIFont.systemFontOfSize(18.0)]
attrString1 = NSMutableAttributedString(string: "My name is Dug.", attributes: textFont)
textShown1 = CustomTextView(frame: CGRectMake(CGRectGetMidX(self.frame), 175 + (90 * paragraphNumber), CGRectGetWidth(self.frame) - 80, CGRectGetHeight(self.frame)-400))
textShown1.attributedText = attrString1
self.view?.addSubview(textShown1)

You can use firstRectForRange(_:) method on UITextView
let textFont = [NSFontAttributeName: UIFont(name: "GillSansMT", size: 30.0) ?? UIFont.systemFontOfSize(18.0)]
let attrString1 = NSMutableAttributedString(string: "My name is Dug.", attributes: textFont)
// range of substring to search
let str1 = attrString1.string as NSString
let range = str1.rangeOfString("name", options: nil, range: NSMakeRange(0, str1.length))
// prepare the textview
let textView = UITextView(frame:CGRectMake(0,0,200,200))
textView.attributedText = attrString1
// you should ensure layout
textView.layoutManager.ensureLayoutForTextContainer(textView.textContainer)
// text position of the range.location
let start = textView.positionFromPosition(textView.beginningOfDocument, offset: range.location)!
// text position of the end of the range
let end = textView.positionFromPosition(start, offset: range.length)!
// text range of the range
let tRange = textView.textRangeFromPosition(start, toPosition: end)
// here it is!
let rect = textView.firstRectForRange(tRange)

Related

One string with multiple paragraph styles

I want to have one string with different paragraphs styles. The goal is to customize the paragraph/line spacing for different parts of the string. I researched and found this answer but since I added multiple new line characters, not sure how to implement.
Design
This is my goal in terms of layout:
Code
This is the code I have which makes it look like the left image above. Please see the comments Not working in the code. Notice how the spacing is set for the main string, but the other strings can't then set their own custom spacing:
struct BookModel: Codable {
let main: String
let detail: String
}
func createAttributedString(for model: BookModel) -> NSMutableAttributedString {
let fullString = NSMutableAttributedString()
let mainString = NSMutableAttributedString(string: model.main)
let mainStringParagraphStyle = NSMutableParagraphStyle()
mainStringParagraphStyle.alignment = .center
mainStringParagraphStyle.lineSpacing = 10
mainStringParagraphStyle.paragraphSpacing = 30
let mainStringAttributes: [NSAttributedString.Key: Any] = [.paragraphStyle: mainStringParagraphStyle]
let spacingAfterQuote = NSMutableAttributedString(string: "\n")
let lineImageAttachment = NSTextAttachment(image: #imageLiteral(resourceName: "line-image"))
let lineImageString = NSMutableAttributedString(attachment: lineImageAttachment)
let lineParagraphStyle = NSMutableParagraphStyle()
lineParagraphStyle.alignment = .left
lineParagraphStyle.lineSpacing = 0 // Not working - instead of 0 it is 30 from `mainStringParagraphStyle`
lineParagraphStyle.paragraphSpacing = 0 // Not working - instead of 0 it is 30 from `mainStringParagraphStyle`
let lineAttributes: [NSAttributedString.Key: Any] = [.paragraphStyle: lineParagraphStyle]
let spacingAfterSeparator = NSMutableAttributedString(string: "\n")
let spacingAfterSeparatorParagraphStyle = NSMutableParagraphStyle()
spacingAfterSeparatorParagraphStyle.alignment = .left
spacingAfterSeparatorParagraphStyle.lineSpacing = 0 // Not working - instead of 0 it is 30 from `mainStringParagraphStyle`
spacingAfterSeparatorParagraphStyle.paragraphSpacing = 5 // Not working - instead of 5 it is 30 from `mainStringParagraphStyle`
let spacingAfterSeparatorAttributes: [NSAttributedString.Key: Any] = [.paragraphStyle: spacingAfterSeparatorParagraphStyle]
let detailString = NSMutableAttributedString(string: model.detail)
let detailStringAttributes: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 20)]
fullString.append(mainString)
fullString.append(spacingAfterQuote)
fullString.append(lineImageString)
fullString.append(spacingAfterSeparator)
fullString.append(detailString)
fullString.addAttributes(mainStringAttributes, range: fullString.mutableString.range(of: model.main))
fullString.addAttributes(lineAttributes, range: fullString.mutableString.range(of: lineImageString.string))
fullString.addAttributes(spacingAfterSeparatorAttributes, range: fullString.mutableString.range(of: spacingAfterSeparator.string))
fullString.addAttributes(detailStringAttributes, range: fullString.mutableString.range(of: model.detail))
return fullString
}
Any thoughts on how to achieve the image on the right?
Question Update 1
The code below is working! There is only one slight problem. When I add lineSpacing, there is extra space at the end of the last line in main string. Notice that I have this set to zero: mainStringParagraphStyle.paragraphSpacing = 0, but there is still space at the end because mainStringParagraphStyle.lineSpacing = 60.
The reason I ask this is to have more fine grain control of spacing. For example, have a perfect number between the line image and main string. Any thoughts on this?
I put code and picture below:
Code:
func createAttributedString(for model: BookModel) -> NSMutableAttributedString {
let fullString = NSMutableAttributedString()
let mainStringParagraphStyle = NSMutableParagraphStyle()
mainStringParagraphStyle.alignment = .center
mainStringParagraphStyle.paragraphSpacing = 0 // The space after the end of the paragraph
mainStringParagraphStyle.lineSpacing = 60 // NOTE: This controls the spacing after the last line instead of just `paragraphSpacing`
let mainString = NSAttributedString(string: "\(model.main)\n",
attributes: [.paragraphStyle: mainStringParagraphStyle, .font: UIFont.systemFont(ofSize: 24)])
let lineImageStringParagraphStyle = NSMutableParagraphStyle()
lineImageStringParagraphStyle.alignment = .center
let lineImageAttachment = NSTextAttachment(image: #imageLiteral(resourceName: "line-view"))
let lineImageString = NSMutableAttributedString(attachment: lineImageAttachment)
lineImageString.addAttribute(.paragraphStyle, value: lineImageStringParagraphStyle, range: NSRange(location: 0, length: lineImageString.length))
let detailStringParagraphStyle = NSMutableParagraphStyle()
detailStringParagraphStyle.alignment = .center
detailStringParagraphStyle.paragraphSpacingBefore = 5 // The distance between the paragraph’s top and the beginning of its text content
detailStringParagraphStyle.lineSpacing = 0
let detailString = NSAttributedString(string: "\n\(model.detail)",
attributes: [.paragraphStyle: detailStringParagraphStyle, .font: UIFont.systemFont(ofSize: 12)])
fullString.append(mainString)
fullString.append(lineImageString)
fullString.append(detailString)
return fullString
}
Updated answer:
Here's a new example. I set the spacing at the top and at the bottom of the paragraph with the image. This allows line breaks to be used in model.main and model.detail if needed. Also, instead of lineSpacing, I used lineHeightMultiple. This parameter affects the indentation between lines without affecting the last line:
func createAttributedString(for model: BookModel) -> NSAttributedString {
let fullString = NSMutableAttributedString()
let mainStringParagraphStyle = NSMutableParagraphStyle()
mainStringParagraphStyle.alignment = .center
mainStringParagraphStyle.lineHeightMultiple = 2 // Note that this is a multiplier, not a value in points
let mainString = NSAttributedString(string: "\(model.main)\n", attributes: [.paragraphStyle: mainStringParagraphStyle, .font: UIFont.systemFont(ofSize: 24)])
let lineImageStringParagraphStyle = NSMutableParagraphStyle()
lineImageStringParagraphStyle.alignment = .center
lineImageStringParagraphStyle.paragraphSpacingBefore = 10 // The space before image
lineImageStringParagraphStyle.paragraphSpacing = 20 // The space after image
let lineImageAttachment = NSTextAttachment(image: #imageLiteral(resourceName: "line-image"))
let lineImageString = NSMutableAttributedString(attachment: lineImageAttachment)
lineImageString.addAttribute(.paragraphStyle, value: lineImageStringParagraphStyle, range: NSRange(location: 0, length: lineImageString.length))
let detailStringParagraphStyle = NSMutableParagraphStyle()
detailStringParagraphStyle.alignment = .center
let detailString = NSAttributedString(string: "\n\(model.detail)", attributes: [.paragraphStyle: detailStringParagraphStyle, .font: UIFont.systemFont(ofSize: 12)])
fullString.append(mainString)
fullString.append(lineImageString)
fullString.append(detailString)
return fullString
}
Also have a look at my library StringEx. It allows you to create a NSAttributedString from the template and apply styles without having to write a ton of code:
import StringEx
...
func createAttributedString(for model: BookModel) -> NSAttributedString {
let pattern = "<main />\n<image />\n<detail />"
let ex = pattern.ex
ex[.tag("main")]
.insert(model.main)
.style([
.aligment(.center),
.lineHeightMultiple(2),
.font(.systemFont(ofSize: 24))
])
let lineImageAttachment = NSTextAttachment(image: #imageLiteral(resourceName: "line-image"))
let lineImageString = NSAttributedString(attachment: lineImageAttachment)
ex[.tag("image")]
.insert(lineImageString)
.style([
.aligment(.center),
.paragraphSpacingBefore(10),
.paragraphSpacing(20)
])
ex[.tag("detail")]
.insert(model.detail)
.style([
.aligment(.center),
.font(.systemFont(ofSize: 12))
])
return ex.attributedString
}
Old answer:
I think you can just set the spacing at the end of the first paragraph (main string) and the spacing at the beginning of the last paragraph (detail string):
func createAttributedString(for model: BookModel) -> NSMutableAttributedString {
let fullString = NSMutableAttributedString()
let mainStringParagraphStyle = NSMutableParagraphStyle()
mainStringParagraphStyle.alignment = .center
mainStringParagraphStyle.paragraphSpacing = 30 // The space after the end of the paragraph
let mainString = NSAttributedString(string: "\(model.main)\n", attributes: [.paragraphStyle: mainStringParagraphStyle])
let lineImageStringParagraphStyle = NSMutableParagraphStyle()
lineImageStringParagraphStyle.alignment = .center
let lineImageAttachment = NSTextAttachment(image: #imageLiteral(resourceName: "line-image"))
let lineImageString = NSMutableAttributedString(attachment: lineImageAttachment)
lineImageString.addAttribute(.paragraphStyle, value: lineImageStringParagraphStyle, range: NSRange(location: 0, length: lineImageString.length))
let detailStringParagraphStyle = NSMutableParagraphStyle()
detailStringParagraphStyle.alignment = .center
detailStringParagraphStyle.paragraphSpacingBefore = 5 // The distance between the paragraph’s top and the beginning of its text content
let detailString = NSAttributedString(string: "\n\(model.detail)", attributes: [.paragraphStyle: detailStringParagraphStyle])
fullString.append(mainString)
fullString.append(lineImageString)
fullString.append(detailString)
return fullString
}

How to Align Text with NSAttributedString in swift

I am looking for align Text in NSAttributedString. It should be on same indent(List of string on same Column).
Output:
I have used below code
func setAttributedText() {
let image1Attachment = NSTextAttachment()
image1Attachment.image = UIImage(named: "checkgreen.png")
image1Attachment.bounds = CGRect(x: 0,
y: (contentLabel.font.capHeight - image1Attachment.image!.size.height).rounded() / 2, width: image1Attachment.image!.size.width,
height: image1Attachment.image!.size.height)
var attributes = [NSAttributedString.Key: Any]()
attributes[.font] = UIFont.preferredFont(forTextStyle: .body)
attributes[.foregroundColor] = UIColor.black
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.headIndent = 50
attributes[.paragraphStyle] = paragraphStyle
let line0 = NSAttributedString(string: "Dummy text is text that is used in the publishing industry or by web designers to occupy the space which will later be filled with 'real' content. \n")
let line1 = NSAttributedString(string: "Your account will be charged for renewal within 24-hours prior to the end of the current subscription perio\n")
// let line2 = NSAttributedString(string: "You can manage your subscriptions and turn off auto-renewal by going to your Account Settings on th")
let checkgreenImageAttribute = NSMutableAttributedString(attributedString: NSAttributedString(attachment: image1Attachment))
let finalString = NSMutableAttributedString(attributedString: checkgreenImageAttribute)
finalString.append(line0)
finalString.append(checkgreenImageAttribute)
finalString.append(line1)
// finalString.append(checkgreenImageAttribute)
// finalString.append(line2)
contentLabel.attributedText = finalString
}
NOTE: i don't want to use bullet points
In an attributed string, a NSTextAttachment becomes a character in the string.
So, apply your attributes to the entire string after you've "assembled" it:
let checkgreenImageAttribute = NSMutableAttributedString(attributedString: NSAttributedString(attachment: image1Attachment))
let finalString = NSMutableAttributedString(attributedString: checkgreenImageAttribute)
finalString.append(line0)
finalString.append(checkgreenImageAttribute)
finalString.append(line1)
// apply paragraph attributes here
finalString.addAttributes(attributes, range: NSRange(location: 0, length: finalString.length))

How to remove bottom padding of UILabel with attributedText inside UIStackView

I want to remove the bottom padding of a UILabel with attributedText inside a UIStackview.
I found this solution How to remove the extra padding below an one line UILabel. This works with normal text but not with attributed text.
let textLabel = UILabel()
textLabel.translatesAutoresizingMaskIntoConstraints = false
textLabel.text = "What is a chemical property and how can you observe it?"
textLabel.numberOfLines = 0
textLabel.lineBreakMode = .byWordWrapping
textLabel.backgroundColor = .lightGray
mainStackView.addArrangedSubview(textLabel)
let textLabel2 = UILabel()
textLabel2.translatesAutoresizingMaskIntoConstraints = false
let html = "<html lang=\"en\"><head><meta charset=\"UTF-8\"></head><body><div style=\"font-size:36;\"><p>What is a <em>chemical property</em> and how can you observe it?</p></div></body></html>"
let data = Data(html.utf8)
if let attributedString = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) {
let a = NSMutableAttributedString.init(attributedString: attributedString)
let range = (a.string as NSString).range(of: a.string)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .left
paragraphStyle.firstLineHeadIndent = 0.0
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.black,
.paragraphStyle: paragraphStyle
]
a.addAttributes(attributes, range: range)
textLabel2.attributedText = a
}
textLabel2.numberOfLines = 0
textLabel2.lineBreakMode = .byWordWrapping
textLabel2.backgroundColor = .yellow
mainStackView.addArrangedSubview(textLabel2)
let textLabel3 = UILabel()
textLabel3.translatesAutoresizingMaskIntoConstraints = false
textLabel3.text = "What is a chemical property and how can you observe it?"
textLabel3.numberOfLines = 0
textLabel3.lineBreakMode = .byWordWrapping
textLabel3.backgroundColor = .lightGray
mainStackView.addArrangedSubview(textLabel3)
A working sample project with this code can be found here: https://github.com/Quobject/testUIlabelInStackviewpadding
The "bottom spacing" is not "spacing" ... your converted <p>...</p> html block adds a newline character at the end of the text.
You can use this extension (found here):
extension NSMutableAttributedString {
func trimmedAttributedString() -> NSAttributedString {
let invertedSet = CharacterSet.whitespacesAndNewlines.inverted
let startRange = string.rangeOfCharacter(from: invertedSet)
let endRange = string.rangeOfCharacter(from: invertedSet, options: .backwards)
guard let startLocation = startRange?.upperBound, let endLocation = endRange?.lowerBound else {
return NSAttributedString(string: string)
}
let location = string.distance(from: string.startIndex, to: startLocation) - 1
let length = string.distance(from: startLocation, to: endLocation) + 2
let range = NSRange(location: location, length: length)
return attributedSubstring(from: range)
}
}
and change this line:
textLabel2.attributedText = a
to:
textLabel2.attributedText = a.trimmedAttributedString()
Result (applying that change to your GitHub repo):

How to change "\t" length in NSAttributedString?

I want to change default width of tab in UILabel using attributed string. How can I achieve that? I assume that I should add attribute NSMutableParagraphStyle, but I don't know which property is responsible for tab length.
Let's use this code for example:
let text = "test\ttest"
let attributedText = NSMutableAttributedString(string: text)
let paragraphStyle = NSMutableParagraphStyle()
let textRange = NSRange(location: 0, length: text.length)
attributedText.addAttribute(NSAttributedStringKey.paragraphStyle, value: paragraphStyle, range: textRange)
According to Apple Developer Documentation, var tabStops: [NSTextTab]! is an array of NSTextTab objects representing the receiver’s tab stops. You can access tabs and change their location as follows:
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: newTabLength, options: [:])]
label.attributedText = NSAttributedString(string: text, attributes: [NSParagraphStyleAttributeName: paragraphStyle])
To change the length of the tabstops via NSMutableParagraphStyle you have to create a new array of NSTextTab instances and assign it to the tabStops array
let text = "test\ttest\ttest"
let attributedText = NSMutableAttributedString(string: text)
let paragraphStyle = NSMutableParagraphStyle()
let tabInterval : CGFloat = 40.0
var tabs = [NSTextTab]()
for i in 1...10 { tabs.append(NSTextTab(textAlignment: .left, location: tabInterval * CGFloat(i))) }
paragraphStyle.tabStops = tabs
let textRange = NSRange(location: 0, length: text.count)
attributedText.addAttribute(NSAttributedStringKey.paragraphStyle, value: paragraphStyle, range: textRange)
you can try replacing the \t with number of space you want
var text = "test\ttest"
text = text.replacingOccurrences(of: "\\t", with: " ")
let attributedText = NSMutableAttributedString(string: text)
let paragraphStyle = NSMutableParagraphStyle()
let textRange = NSRange(location: 0, length: text.length)
attributedText.addAttribute(NSAttributedStringKey.paragraphStyle, value: paragraphStyle, range: textRange)

Multiple range on NSMutableAttributedString

i'm trying to set bold font two different string range in a string. I know how to do it range by range but is it possible to do it one time with multiple range. I don't like to repeat code.
Here's my code
if let rankString = trophy.fullRank.value {
var secondRankString = "some string"
var string = SRUtils.LocalizedString("unlocked.description", comment: "")
string = String(format: string, trophy.trophiesNumber.value!, rankString)
let atStr = NSMutableAttributedString(string: string)
//Here i need to add rankString and other range
let textRange = (string as NSString).range(of: rankString)
if let font = UIFont(name: "HelveticaNeue-Bold", size: 16) {
atStr.addAttribute(NSFontAttributeName, value: font, range: textRange)
self.trophyDescription.value = atStr
}
}
If for example:
var string = "Hello do you need a cat"
var rankString = "cat"
Result with my code: "Hello do you need a cat"
What i need it's :
var mytring = "Hello do you need a cat"
var rankString = "cat"
var secondRankString = "need"
Excepted result : "Hello do you need a cat"
So how i can, in swift 3, add multiple range to apply, without multiple declaration of variable... is it possible ?
two or more, use a for — Edsger W. Dijkstra
This code
import UIKit
let string = "Hello do you need a cat"
let attributedString = NSMutableAttributedString(string: string)
if let font = UIFont(name: "HelveticaNeue", size: 16) {
attributedString.addAttribute(NSFontAttributeName, value: font, range: NSRange(location:0, length: string.characters.count))
}
let highlightedWords = ["cat", "need"]
for highlightedWord in highlightedWords {
let textRange = (string as NSString).range(of: highlightedWord)
if let font = UIFont(name: "HelveticaNeue-Bold", size: 16) {
attributedString.addAttribute(NSFontAttributeName, value: font, range: textRange)
}
}
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
label.attributedText = attributedString
label.sizeToFit()
results in
i was just searching an other way to do it without for or multiple declaration
A for loop can always be expressed as a more generell while loop. But I find for loops easier to understand.
var highlightedWords = ["cat", "need"]
repeat {
if let highlightedWord = highlightedWords.popLast() {
let textRange = (string as NSString).range(of: highlightedWord)
if let font = UIFont(name: "HelveticaNeue-Bold", size: 16) {
attributedString.addAttribute(NSFontAttributeName, value: font, range: textRange)
}
}
} while highlightedWords.count > 0

Resources