I have a PDF image that I appended to a String. I used NSTextAttachment and NSAttributedString to get it done. I add them to a textView and the result is Hello with an image of the World underneath of it.
The problem is when I set the bounds for the PDF image on the textAttachment the World image is distorted. It's stretched long and wide.
How can I set a contentMode on the textAttachment object to redraw the image correctly using .aspectRatio?
number #4 is where I set the bounds
// #1. Define dict attribute for string
let bold17 = [NSFontAttributeName: UIFont.boldSystemFont(ofSize: 17)]
// #2. Create "hello" string and add the dict attribute to it
let helloStr = NSAttributedString(string: "Hello\n\n", attributes: bold17)
// #3. Create NSTextAttachment
let textAttachment = NSTextAttachment()
// #4. Add image to the textAttachment then set it's bounds
textAttachment.image = UIImage(named: "world_PDF")
textAttachment.bounds = CGRect(x: 0, y: 0, width: 200, height: 200)
// #5. Set image as NSAttributedString
let worldImage = NSAttributedString(attachment: textAttachment)
// #6. Create NSMutableString to
let mutableAttributedString = NSMutableAttributedString()
// #7. Append the "hello" string and the "world" image to each other using the mutableAttributedString object
mutableAttributedString.append(helloStr)
mutableAttributedString.append(worldImage)
// #8. Set the mutableAttributedString to the textView then center it
textView.attributedText = mutableAttributedString
textView.textAlignment = .center
I followed #Maciej Swic's answer
Resize NSTextAttachment Image
For some reason I couldn't extend the NSTextAttachment class so I added it to the bottom of the class I was using it in. I removed the bounds property that I used in my question and used his function instead. It's on #4, the second line:
class MyController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
// #1. Define dict attribute for string
let bold17 = [NSFontAttributeName: UIFont.boldSystemFont(ofSize: 17)]
// #2. Create "hello" string and add the dict attribute to it
let helloStr = NSAttributedString(string: "Hello\n\n", attributes: bold17)
// #3. Create NSTextAttachment
let textAttachment = NSTextAttachment()
// #4. Add image to the textAttachment then set it's bounds
textAttachment.image = UIImage(named: "world_PDF")
textAttachment.setImageHeight(height: 200) // <----HIS ANSWER HERE
// #5. Set image as NSAttributedString
let worldImage = NSAttributedString(attachment: textAttachment)
// #6. Create NSMutableString to
let mutableAttributedString = NSMutableAttributedString()
// #7. Append the "hello" string and the "world" image to each other using the mutableAttributedString object
mutableAttributedString.append(helloStr)
mutableAttributedString.append(worldImage)
// #8. Set the mutableAttributedString to the textView then center it
textView.attributedText = mutableAttributedString
textView.textAlignment = .center
}
}
extension NSTextAttachment {
func setImageHeight(height: CGFloat) {
guard let image = image else { return }
let ratio = image.size.width / image.size.height
bounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: ratio * height, height: height)
}
}
Related
I have big problem. I want add few UIView to text (UITextView, UILabel)
Each view contains ImageView with corner radius and text.
I want have result like this:
sample from Android
I tried:
Add image with text by NSMutableAttributedString. In this case I can't add corner radius. And all images are from external serwers so it's problem with add to text.
I tried this library: SubviewAttachingTextView. In this case when I added multiple items all items were stacked on top of each other.
Finaly I used WKWebView and I inject HTML with CSS to WebView. But in this solution I have problem with fit content to frame size and is very slow. (for me is the worst solution)
Does anyone have an idea how to develop? Maybe there are some mechanisms in SwiftUI?
You can create a Custom class for a UIView and add subviews that you need inside that, and you can call that class when ever you want, in SwiftUI you can add this very easily by implementing a Label element inside the stack.
I solved my problem. I used NSMutableAttributedString and extensions on UIImage (download images and making circular avatars)
extension UIImage {
convenience init?(withContentsOfUrl url: URL) throws {
let imageData = try Data(contentsOf: url)
self.init(data: imageData)
}
public func withRoundedCorners(radius: CGFloat? = nil) -> UIImage? {
let maxRadius = min(size.width, size.height) / 2
let cornerRadius: CGFloat
if let radius = radius, radius > 0 && radius <= maxRadius {
cornerRadius = radius
} else {
cornerRadius = maxRadius
}
UIGraphicsBeginImageContextWithOptions(size, false, scale)
let rect = CGRect(origin: .zero, size: size)
UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).addClip()
draw(in: rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
and my sample code:
class NewNotificationsViewController: UIViewController, UITextViewDelegate {
#IBOutlet weak var testText: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
self.testText.delegate = self
let url = "image-url";
let font = UIFont.systemFont(ofSize: 22)
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: UIColor.orange,
.link: "http://test.pl"
]
let myString = NSMutableAttributedString(string: "Text at the beginning ", attributes: attributes)
let imageAttachment = NSTextAttachment()
do {
imageAttachment.image = try UIImage.init(withContentsOfUrl: URL(string: url)!)
} catch {
imageAttachment.image = UIImage(named: "avatar_k")
}
imageAttachment.image = imageAttachment.image?.withRoundedCorners()
imageAttachment.bounds = CGRect(x: 0, y: -8, width: 32, height: 32)
let imageString = NSAttributedString(attachment: imageAttachment)
myString.append(imageString)
myString.append(NSAttributedString(string: " THE END!!!", attributes: attributes))
self.testText.attributedText = myString
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
print("DEBUG", URL.absoluteString)
return false
}
}
I have a class called "rectangle" to make custom UILabels. I override "draw" in the rectangle class. When I instantiate the label, I want the FIRST line of text to show up in bolded font. I know how to solve this by manually getting the range for each string... however, I have more than 300 strings to do. The strings are currently in an array, formatted like so: "Happy \n Birthday". How can I make the word "Happy" bold?
var messageText = "Happy \n Birthday"
let rectanglePath = UIBezierPath(rect: rectangleRect)
context.saveGState()
UIColor.white.setFill()
rectanglePath.fill()
context.restoreGState()
darkPurple.setStroke()
rectanglePath.lineWidth = 0.5
rectanglePath.lineCapStyle = .square
rectanglePath.lineJoinStyle = .round
rectanglePath.stroke()
let rectangleStyle = NSMutableParagraphStyle()
rectangleStyle.alignment = .center
let rectangleFontAttributes = [
.font: UIFont.myCustomFont(true),
.foregroundColor: UIColor.black,
.paragraphStyle: rectangleStyle,
] as [NSAttributedString.Key: Any]
let rectangleTextHeight: CGFloat = messageText.boundingRect(with: CGSize(width: rectangleRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: rectangleFontAttributes, context: nil).height
context.saveGState()
context.clip(to: rectangleRect)
messageText.draw(in: CGRect(x: rectangleRect.minX, y: rectangleRect.minY + (rectangleRect.height - rectangleTextHeight) / 2, width: rectangleRect.width, height: rectangleTextHeight), withAttributes: rectangleFontAttributes)
context.restoreGState()
You can find the first by separating the string by newline:
let firstLine = "Happy \n Birthday".split(separator: "\n").first
This will give you the first line of the string. (long text multi lining doesn't count) then you can find the range using this and apply the bold effect.
How this works:
You need to set the label the way that accepts multiline:
Find the range of first line
Convert it to nsRange
Apply attributes to the range
Here is a fully working example:
import UIKit
import PlaygroundSupport
extension StringProtocol where Index == String.Index {
func nsRange(from range: Range<Index>) -> NSRange {
return NSRange(range, in: self)
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let label = UILabel()
label.numberOfLines = 0
label.text = "Happy \n Birthday"
label.textColor = .black
let text = "Happy \n Birthday"
let attributedString = NSMutableAttributedString(string: text)
let firstLine = text.split(separator: "\n").first!
let range = text.range(of: firstLine)!
attributedString.addAttributes([.font : UIFont.boldSystemFont(ofSize: 14)], range: text.nsRange(from: range))
label.attributedText = attributedString
label.sizeToFit()
view.addSubview(label)
self.view = view
}
}
PlaygroundPage.current.liveView = MyViewController()
I'm adding an icon to a UILabel using NSTextAttachment inside an NSMutableAttributedString like this:
//Setting up icon
let moneyIcon = NSTextAttachment()
moneyIcon.image = UIImage(named: "MoneyIcon")
let moneyIconString = NSAttributedString(attachment: moneyIcon)
//Setting up text
let balanceString = NSMutableAttributedString(string: " 1,702,200")
balanceString.insert(moneyIconString, at: 0)
//Adding string to label
self.attributedText = balanceString
self.sizeToFit()
But for some reason the icon isn't vertically aligned
Does anybody know how can I align it?
Thank you!
use bounds property of NSTextAttachment.
//Setting up icon
let moneyIcon = NSTextAttachment()
moneyIcon.image = UIImage(named: "MoneyIcon")
let imageSize = moneyIcon.image!.size
moneyIcon.bounds = CGRect(x: CGFloat(0), y: (font.capHeight - imageSize.height) / 2, width: imageSize.width, height: imageSize.height)
let moneyIconString = NSAttributedString(attachment: moneyIcon)
//Setting up text
let balanceString = NSMutableAttributedString(string: " 1,702,200")
balanceString.insert(moneyIconString, at: 0)
//Adding string to label
self.attributedText = balanceString
self.sizeToFit()
This answer, which is about vertically centering two differently sized fonts in a single NSAttributedString, mentions using the baseline offset to calculate the center of the string.
You can use the same approach when using an image:
Subtract the font size from the image's height and divide it by 2.
Subtract the font's descender from the value (since font size isn't the same as the ascent of your font). The font that you are particularly using (Baloo-Regular) has a descender value that differs from the standard and it should be divided by 2. Other fonts (including San Fransisco) don't need that fix or require a different divisor.
This code covers most cases, if your font behaves differently, you should check out the guide for managing texts in Text Kit.
// *Setting up icon*
let moneyIcon = NSTextAttachment()
// If you're sure a value is not and will never be nil, you can use "!".
// Otherwise, avoid it.
let moneyImage = UIImage(named: "MoneyIcon")!
moneyIcon.image = moneyImage
let moneyIconString = NSAttributedString(attachment: moneyIcon)
// *Setting up NSAttributedString attributes*
let balanceFontSize: CGFloat = 16
let balanceFont = UIFont(name: "Baloo", size: balanceFontSize)!
let balanceBaselineOffset: CGFloat = {
let dividend = moneyImage.size.height - balanceFontSize
return dividend / 2 - balanceFont.descender / 2
}()
let balanceAttr: [NSAttributedString.Key: Any] = [
.font: balanceFont,
.baselineOffset: balanceBaselineOffset
]
// *Setting up text*
let balanceString = NSMutableAttributedString(
string: " 1,702,200",
attributes: balanceAttr
)
balanceString.insert(moneyIconString, at: 0)
This question already has answers here:
How to add image and text in UITextView in IOS?
(10 answers)
Closed 6 years ago.
I'm making a note taking app but I ran into trouble.
I want to put a image in UITextView. I used NSAttributedString to put the image in UITextView but when I put image from the imagepicker, the image size is too big like this
I want to make it like the apple notes app
How can I figure that?
let images = selectedImage
let attachment = NSTextAttachment()
attachment.image = images
let attString = NSAttributedString(attachment: attachment)
textView.textStorage.insertAttributedString(attString, atIndex: textView.selectedRange.location)
This might be helpful to you
let textView = UITextView(frame: CGRectMake(50, 50, 200, 300))
let attributedString = NSMutableAttributedString(string: "before after")
let textAttachment = NSTextAttachment()
textAttachment.image = UIImage(named: "sample_image.jpg")!
let oldWidth = textAttachment.image!.size.width;
let scaleFactor = oldWidth / (textView.frame.size.width - 10); //for the padding inside the textView
textAttachment.image = UIImage(CGImage: textAttachment.image!.CGImage, scale: scaleFactor, orientation: .Up)
var attrStringWithImage = NSAttributedString(attachment: textAttachment)
attributedString.replaceCharactersInRange(NSMakeRange(6, 1), withAttributedString: attrStringWithImage)
textView.attributedText = attributedString;
self.view.addSubview(textView)
I have image and string in label togher in navigation bar
When I use coins, say use all 50 of them it should be 0 with image coins but its not
but when I click to another viewcontroller and back to same viewcontroller with coins the image is back
here is code:
let coinLabel = UILabel()
override func viewWillAppear(animated: Bool) {
// Set icon in label in navigation bar ////
let image = UIImage(named: "coins.jpeg")
let newSize = CGSize(width: 15, height: 15)
//Resize image
UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0)
image?.drawInRect(CGRectMake(0, 0, newSize.width, newSize.height))
let imageResized = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
//Create attachment text with image
let attachment = NSTextAttachment()
attachment.image = imageResized
let attachmentString = NSAttributedString(attachment: attachment)
let myString = NSMutableAttributedString(string: "\(coins)")
myString.appendAttributedString(attachmentString)
coinLabel.attributedText = myString
coinLabel.sizeToFit()
// Set defined label to navigation bar right corner
let leftItem = UIBarButtonItem(customView: coinLabel)
self.navigationItem.rightBarButtonItem = leftItem
coinLabel.font = coinLabel.font.fontWithSize(15)
}
I think the problem is with reloading... or something. Any Idea??
Don't sure that it's a reason but at least you need to set font before calling sizeToFit()
coinLabel.font = coinLabel.font.fontWithSize(15)
coinLabel.sizeToFit()
Also you missed call to super.viewWillAppear(animated)