I have a UIButton that holds an NSAttributedString that contains an icon (which is an NSAttributedString itself) followed by a text.
It looks something like this:
I want to make it look like this when the device is configured for an RTL language (e.g. Arabic, Hebrew):
The string is built like this:
var iconText = NSAttributedString(fontName: fontName, fontSize: fontPointSize, fontValue: fontValue, color: color ?? iconColor)
let iconTextRange = NSRange(location: 0, length: iconText.count)
let iconAttrs: [NSAttributedString.Key: Any] = [.font: icon.font(pointSize: pointSize),
.foregroundColor: iconColor]
if !text.isEmpty {
iconText = "\(iconText) \(text)"
}
let attributeString = NSMutableAttributedString(string: iconText, attributes: textAttrs)
attributeString.addAttributes(iconAttrs, range: iconTextRange)
return attributeString
As you can see, first the icon is created using a font, then it's concatenated to the text.
In other parts of the app, I managed to make NSAttributedString's RTL-compliant with this little piece of code:
public extension NSAttributedString {
/// Returns an identical attributed string that'll adjust its direction based on the device's configured language.
func rightToLeftAdjusted() -> NSAttributedString {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.baseWritingDirection = .natural
let attrs: [NSAttributedString.Key: Any] = [.paragraphStyle: paragraphStyle]
let range = NSRange(location: 0, length: length)
let copy = NSMutableAttributedString(attributedString: self)
copy.addAttributes(attrs, range: range)
return copy
}
}
Unfortunately, in this specific case, it doesn't seem to work, the star ALWAYS stays to the left of the text.
Are there any other ways of achieving this?
let preferredLanguage = NSLocale.preferredLanguages[0]
if preferredLanguage == "en" {
button.semanticContentAttribute = .forceLeftToRight
}else if preferredLanguage == "ar" {
button.semanticContentAttribute = .forceRightToLeft
}
The source code is at the below repository link:
https://github.com/ahmetbostanciklioglu/ButtonAlignmentLeftToRightAndRightToLeft.git
Thanks to everyone who helped on the matter.
The solution I got to was, instead of appending the icon as a font, I converted it to a UIImage, then to an NSTextAttachment.
That way, the image flipped to the right side of the text when the app is running in RTL mode.
func addDecorator(icon: DrawbleIcon, pointSize: CGFloat, to text: String, textAttrs: [NSAttributedString.Key: Any]) -> NSAttributedString {
let attributedString = NSMutableAttributedString()
appendIcon(to: attributedString, icon: icon, pointSize: pointSize)
// Append text
if !text.isEmpty {
attributedString.append(NSAttributedString(string: text, attributes: textAttrs))
}
// Adjust text to right-to-left devices
let writingDirection: NSWritingDirection = UIApplication.isRightToLeft ? .rightToLeft : .leftToRight
let leftToRightAttrs: [NSAttributedString.Key: Any] = [.writingDirection: [NSNumber(value: writingDirection.rawValue)]]
attributedString.addAttributes(leftToRightAttrs, range: NSRange(location: 0, length: attributedString.length))
return attributedString
}
private func appendIcon(to attributedString: NSMutableAttributedString, icon: DrawbleIcon, pointSize: CGFloat) {
let iconColor = icon.foreColor
let iconAttrs: [NSAttributedString.Key: Any] = [.font: icon.font(pointSize: pointSize),
.foregroundColor: iconColor]
if let iconText = icon.attributedString(fontPointSize: pointSize, color: nil),
let font = iconText.attributes(at: 0, effectiveRange: nil)[.font] as? UIFont,
let iconImage = NSAttributedString(string: iconText.string, attributes: iconAttrs).image() {
let textAttachment = NSTextAttachment(image: iconImage)
let imageSize = iconImage.size
// Fix image height - without this, the image is higher than the text.
textAttachment.bounds = CGRect(x: CGFloat(0), y: (font.capHeight - imageSize.height) / 2, width: imageSize.width, height: imageSize.height)
attributedString.append(NSAttributedString(attachment: textAttachment))
attributedString.append(NSAttributedString(string: " "))
}
}
private extension String {
/// Generates a `UIImage` instance from this string using a specified
/// attributes and size.
///
/// - Parameters:
/// - attributes: to draw this string with. Default is `nil`.
/// - size: of the image to return.
/// - Returns: a `UIImage` instance from this string using a specified
/// attributes and size, or `nil` if the operation fails.
func image(withAttributes attributes: [NSAttributedString.Key: Any]? = nil, size: CGSize? = nil) -> UIImage? {
let size = size ?? (self as NSString).size(withAttributes: attributes)
return UIGraphicsImageRenderer(size: size).image { _ in
(self as NSString).draw(in: CGRect(origin: .zero, size: size),
withAttributes: attributes)
}
}
}
private extension NSAttributedString {
func image(size: CGSize? = nil) -> UIImage? {
string.image(withAttributes: attributes(at: 0, effectiveRange: nil), size: size)
}
}
Hi I wanna add image round dot to some UILabel in my app.
I have code for adding the image. But I don't understand how I could put the image on the start of the UILabel rather than in the end of the label.
Any suggestion on this? Below is the code I use for it:
What should I add to place image on start of UILabel? I thought imageBehindText :false would fix it but it didn't.
extension UILabel {
/**
This function adding image with text on label.
- parameter text: The text to add
- parameter image: The image to add
- parameter imageBehindText: A boolean value that indicate if the imaga is behind text or not
- parameter keepPreviousText: A boolean value that indicate if the function keep the actual text or not
*/
func addTextWithImage(text: String, image: UIImage, imageBehindText: Bool, keepPreviousText: Bool) {
let lAttachment = NSTextAttachment()
lAttachment.image = image
// 1pt = 1.32px
let lFontSize = round(self.font.pointSize * 1.20) // rounded dot should be smaller than font
let lRatio = image.size.width / image.size.height
lAttachment.bounds = CGRect(x: 0, y: ((self.font.capHeight - lFontSize) / 2).rounded(), width: lRatio * lFontSize, height: lFontSize)
let lAttachmentString = NSAttributedString(attachment: lAttachment)
if imageBehindText {
let lStrLabelText: NSMutableAttributedString
if keepPreviousText, let lCurrentAttributedString = self.attributedText {
lStrLabelText = NSMutableAttributedString(attributedString: lCurrentAttributedString)
lStrLabelText.append(NSMutableAttributedString(string: text))
} else {
lStrLabelText = NSMutableAttributedString(string: text)
}
lStrLabelText.append(lAttachmentString)
self.attributedText = lStrLabelText
} else {
let lStrLabelText: NSMutableAttributedString
if keepPreviousText, let lCurrentAttributedString = self.attributedText {
lStrLabelText = NSMutableAttributedString(attributedString: lCurrentAttributedString)
lStrLabelText.append(NSMutableAttributedString(attributedString: lAttachmentString))
lStrLabelText.append(NSMutableAttributedString(string: text))
} else {
lStrLabelText = NSMutableAttributedString(attributedString: lAttachmentString)
lStrLabelText.append(NSMutableAttributedString(string: text))
}
self.attributedText = lStrLabelText
}
}
I got it to work. The problem was that I was setting the text in the storyboard(.xib). So this extension didn't change the image to the front even if the bool-val were false.
Simply set the text from the function-call and the 'false' value will trigger the image to be set in the start of the uilabel.
Example1 (what I did wrong):
// This is what I tried first!
label.addTextWithImage(text: "",
image: UIImage(named: embededIcon)!,
imageBehindText: false, // note! This is false.
keepPreviousText: true) // this was the problem!
Example2 (what got it to work!):
label.addTextWithImage(text: "putYourLabelTextHere!", // You have to put text here, even if it's already in storyboard.
image: UIImage(named: embededIcon)!,
imageBehindText: false,
keepPreviousText: false) // false, so the image will be set before text!
lazy var attachment: NSTextAttachment = {
let attachment = NSTextAttachment()
attachment.image = UIImage(named: "")
return attachment
}()
it will work for both remote and local image
if let imageUrl = imageUrl, imageUrl.count > 0, let url = URL(string: imageUrl) {
let imageAttachmentString = NSAttributedString(attachment: attachment)
attachment.bounds = CGRect(x: 0, y: (titleLabel.font.capHeight - 16) / 2, width: 16, height: 16)
BaseAPI().downloadImage(with: url, placeHolder: "") { [weak self] image, error, success, url in
if let image = image, let self = self {
DispatchQueue.main.async {
let finalString = NSMutableAttributedString(string: "")
let titleString = NSMutableAttributedString(string: self.title)
let space = NSMutableAttributedString(string: " ")
let imageAttachment = self.attachment
imageAttachment.image = image
finalString.append(imageAttachmentString)
finalString.append(space)
finalString.append(titleString)
self.titleLabel.attributedText = finalString
}
}
}
} else {
titleLabel.text = title
}
I would like to mask text of a UILabel to achieve the following result
In Swift, You can do it like this:
var attributedString = NSMutableAttributedString(string: "Your String")
let textAttachment = NSTextAttachment()
textAttachment.image = UIImage(named: "Your Image Name")
let attrStringWithImage = NSAttributedString(attachment: textAttachment)
attributedString.insert(attrStringWithImage, at: 0)
label.attributedText = attributedString
This will work for you .
extension UILabel
{
func addImage(imageName: String)
{
let attachment:NSTextAttachment = NSTextAttachment()
attachment.image = UIImage(named: imageName)
let attachmentString:NSAttributedString = NSAttributedString(attachment: attachment)
let myString:NSMutableAttributedString = NSMutableAttributedString(string: self.text!)
myString.appendAttributedString(attachmentString)
self.attributedText = myString
}
}
Another version of the code that allow adding the icon before or after the label.
extension UILabel
{
func addImage(imageName: String, afterLabel bolAfterLabel: Bool = false)
{
let attachment: NSTextAttachment = NSTextAttachment()
attachment.image = UIImage(named: imageName)
let attachmentString: NSAttributedString = NSAttributedString(attachment: attachment)
if (bolAfterLabel)
{
let strLabelText: NSMutableAttributedString = NSMutableAttributedString(string: self.text!)
strLabelText.appendAttributedString(attachmentString)
self.attributedText = strLabelText
}
else
{
let strLabelText: NSAttributedString = NSAttributedString(string: self.text!)
let mutableAttachmentString: NSMutableAttributedString = NSMutableAttributedString(attributedString: attachmentString)
mutableAttachmentString.appendAttributedString(strLabelText)
self.attributedText = mutableAttachmentString
}
}
//you can remove the image by calling this function
func removeImage()
{
let text = self.text
self.attributedText = nil
self.text = text
}
}
For this use a UITextView instead of UILabel.
let imgRectBezier = UIBezierPath(rect: imgView.frame)
txtView.textContainer.exclusionPaths = [imgRectBezier]
Using this, the text will be excluded from the frame area added in the exclusion paths. You can even exclude multiple frames.
extension UILabel {
func setAttributedTextWithElements(objectsForLabel: [AnyObject]) {
let attributedString = NSMutableAttributedString()
for object in objectsForLabel {
if let string = object as? String {
attributedString.append(NSAttributedString.init(string: string))
} else if let imageForLabel = object as? UIImage {
let attachment = NSTextAttachment()
attachment.image = imageForLabel
let attributedImageString = NSAttributedString.init(attachment: attachment)
attributedString.append(attributedImageString)
let imageWidth = (attachment.image?.size.width)!
let imageHeight = (attachment.image?.size.height)!
attachment.bounds = CGRect(x: 0, y: font.descender, width:imageWidth, height:imageHeight)
}
}
attributedText = attributedString
}
Basically the text is not centred any more. If comment out the code for the image the label text is perfectly centred.
I'm passing an image and a string "20" to this method in the array.
I want to add both text and image in UITextView. The textview should be expanded according to the length of the text and image. In short what I want to do is that when I capture an image from camera or pick from gallery then it should display in UITextView and I should also be able to add some text with that image similar to Facebook.I am also attaching an image that how the UITextView will look like.
This is absolutely possible now, using
+ (NSAttributedString *)attributedStringWithAttachment:(NSTextAttachment *)attachment
See Apple docs here
And this example taken from this other answer:
UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(0,0,140,140)];
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:#"before after"];
NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init];
textAttachment.image = [UIImage imageNamed:#"sample_image.jpg"];
CGFloat oldWidth = textAttachment.image.size.width;
//I'm subtracting 10px to make the image display nicely, accounting
//for the padding inside the textView
CGFloat scaleFactor = oldWidth / (textView.frame.size.width - 10);
textAttachment.image = [UIImage imageWithCGImage:textAttachment.image.CGImage scale:scaleFactor orientation:UIImageOrientationUp];
NSAttributedString *attrStringWithImage = [NSAttributedString attributedStringWithAttachment:textAttachment];
[attributedString replaceCharactersInRange:NSMakeRange(6, 1) withAttributedString:attrStringWithImage];
textView.attributedText = attributedString;
Using the above code will get you an image with text inside a UITextView on iOS 7+. You can/show style the attributed text as you want it and probably set the width of the image to make sure it fits within your textView (as well as setting your own aspect ratio/scale preference)
Here's a quick test image:
Thank you for your code, it actually worked. I make a code in the Swift, so I would like to share Swift version of your code. I checked this code works too.
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;
//I'm subtracting 10px to make the image display nicely, accounting
//for the padding inside the textView
let scaleFactor = oldWidth / (textView.frame.size.width - 10);
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)
Code For Swift 3.0
var attributedString :NSMutableAttributedString!
attributedString = NSMutableAttributedString(attributedString:txtBody.attributedText)
let textAttachment = NSTextAttachment()
textAttachment.image = image
let oldWidth = textAttachment.image!.size.width;
//I'm subtracting 10px to make the image display nicely, accounting
//for the padding inside the textView
let scaleFactor = oldWidth / (txtBody.frame.size.width - 10);
textAttachment.image = UIImage(cgImage: textAttachment.image!.cgImage!, scale: scaleFactor, orientation: .up)
let attrStringWithImage = NSAttributedString(attachment: textAttachment)
attributedString.append(attrStringWithImage)
txtBody.attributedText = attributedString;
if you just want place the image in the end, you can use
//create your UIImage
let image = UIImage(named: change_arr[indexPath.row]);
//create and NSTextAttachment and add your image to it.
let attachment = NSTextAttachment()
attachment.image = image
//put your NSTextAttachment into and attributedString
let attString = NSAttributedString(attachment: attachment)
//add this attributed string to the current position.
textView.textStorage.insertAttributedString(attString, atIndex: textView.selectedRange.location)
Check This answer
if you want to get the image from the camera, you can try my code below: (Swift 3.0)
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
let image = info[UIImagePickerControllerOriginalImage] as! UIImage
//create and NSTextAttachment and add your image to it.
let attachment = NSTextAttachment()
attachment.image = image
//calculate new size. (-20 because I want to have a litle space on the right of picture)
let newImageWidth = (textView.bounds.size.width - 20 )
let scale = newImageWidth/image.size.width
let newImageHeight = image.size.height * scale
//resize this
attachment.bounds = CGRect.init(x: 0, y: 0, width: newImageWidth, height: newImageHeight)
//put your NSTextAttachment into and attributedString
let attString = NSAttributedString(attachment: attachment)
//add this attributed string to the current position.
textView.textStorage.insert(attString, at: textView.selectedRange.location)
picker.dismiss(animated: true, completion: nil)
}
Lots of people are making this a lot more complicated than it needs to be. Firstly, add your image to the catalogue at the right size:
Then, create the NSAttributedString in code:
// Creates the logo image
let twitterLogoImage = NSTextAttachment()
twitterLogoImage.image = UIImage(named: "TwitterLogo")
let twitterLogo = NSAttributedString(attachment: twitterLogoImage)
Then add the NSAttributedString to what you want:
// Create the mutable attributed string
let text = NSMutableAttributedString(string: "")
/* Code above, creating the logo */
/* More attributed strings */
// Add the logo to the whole text
text.append(twitterLogo)
textView.attributedText = text
Or:
textView.attributedText = twitterLogo
let htmlString = "<html><body><h1>This is the title</h1><p>This is the first paragraph.</p><img src=\"https://miro.medium.com/max/9216/1*QzxcfBpKn5oNM09-vxG_Tw.jpeg\" width=\"360\" height=\"240\"><p>This is the second paragraph.</p><p>This is the third paragraph.</p><p>This is the fourth paragraph.</p><p>This is the last paragraph.</p></body></html>"
Use this string extension:
extension String {
func convertToAttributedFromHTML() -> NSAttributedString? {
var attributedText: NSAttributedString?
let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue]
if let data = data(using: .unicode, allowLossyConversion: true), let attrStr = try? NSAttributedString(data: data, options: options, documentAttributes: nil) {
attributedText = attrStr
}
return attributedText
}
}
Then set the textView:
textView.attributedText = htmlString.convertToAttributedFromHTML()
If you want to add a simple image in textview from Gallery or Camera as an attachment then this method should be used:
func insertImage(_ image:UIImage) {
let attachment = NSTextAttachment()
attachment.image = image
attachment.setImageHeight(height: 200)
let attString = NSAttributedString(attachment: attachment)
/// at is current cursor position
self.descriptionTextView.textStorage.insert(attString, at: self.descriptionTextView.selectedRange.location)
descriptionTextView.font = UIFont(name: UIFont.avenirNextRegular, size: 17)
descriptionTextView.textColor = .white
}
If you want to add an image from a link than you need to do this, there can be multiple links in string, so will be achive using these methods.
Checks URLS from exisiting string so that we can download an image and show as attachment
func checkForUrls(text: String) -> [NSTextCheckingResult] {
let types: NSTextCheckingResult.CheckingType = .link
do {
let detector = try NSDataDetector(types: types.rawValue)
let matches = detector.matches(in: text, options: .reportCompletion, range: NSRange(location: 0, length: text.count))
return matches
// return matches.compactMap({$0.url})
} catch let error {
debugPrint(error.localizedDescription)
}
return []
}
// Recursive funtion call next after successfull downloaded
func convertToAttachment() {
if imageURLResultsFromStr.count > 0 {
imageTOAttributedText(imageURLResultsFromStr.first?.url, imageURLResultsFromStr.first?.range)
}
}
**// MARK: Server URL to Image conversion to show**
func imageTOAttributedText(_ url:URL?,_ range:NSRange?) {
guard let url = url, let range = range else { return }
let imgView = UIImageView()
imgView.kf.setImage(with: url, completionHandler: { result in
switch result {
case .success(var data):
let attachment = NSTextAttachment()
data.image.accessibilityIdentifier = self.recentlyUploadedImage
attachment.image = data.image
// attachment.fileType = self.recentlyUploadedImage
attachment.setImageHeight(height: 200)
/// This will help to remove existing url from server which we have sent as url
/// Start
let mutStr = self.descriptionTextView.attributedText.mutableCopy() as! NSMutableAttributedString
let range = (mutStr.string as NSString).range(of: "\n\(url.absoluteString)\n")
mutStr.deleteCharacters(in: range)
self.descriptionTextView.attributedText = mutStr
//End
/// Add image as attachment downloaded from url
let attString = NSAttributedString(attachment: attachment)
self.descriptionTextView.textStorage.insert(attString, at: range.location)
/// Recursivly calls to check how many urls we have in string to avoid wrong location insertion
/// We need to re-calculate new string from server after removing url string and add image as attachment
self.imageURLResultsFromStr.remove(at: 0)
self.imageURLResultsFromStr = self.checkForUrls(text: self.descriptionTextView.text)
self.convertToAttachment()
case .failure(let error):
print(error)
}
})
}
// Now call this function and initialise it from server string, Call it from viewdidload or from api response
func initialise() {
self.descriptionTextView.text = ..string from server
self.imageURLResultsFromStr = self.checkForUrls(text:string from server)
convertToAttachment()
}
var imageURLResultsFromStr:[NSTextCheckingResult] = []
NSURL *aURL = [NSURL URLWithString:[[NSString stringWithFormat:#"%#%#",Image_BASE_URL,str] stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]];
//UIImage *aImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:aURL]];
//[aImage drawInRect:CGRectMake(0, 0, 20, 20)];
__block NSTextAttachment *imageAttachment = [NSTextAttachment new];
imageAttachment.bounds = CGRectMake(0, -5, 20, 20);
NSAttributedString *stringWithImage = [NSAttributedString attributedStringWithAttachment:imageAttachment];
[deCodedString replaceCharactersInRange:NSMakeRange(deCodedString.length, 0) withAttributedString:stringWithImage];
incomingMessage.messageAttributedString = deCodedString;
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
imageAttachment.image = [UIImage imageNamed:#"profile_main_placeholder"];
[downloader downloadImageWithURL:aURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
if (image && finished) {
[image drawInRect:CGRectMake(0, 0, 20, 20)];
imageAttachment.image = image;
dispatch_async(dispatch_get_main_queue(), ^(void)
{
[self.tbl_Conversation reloadRowsAtIndexPaths:[self.tbl_Conversation indexPathsForVisibleRows]
withRowAnimation:UITableViewRowAnimationNone];
[self.tbl_Conversation reloadData];
});
// NSAttributedString *stringWithImage = [NSAttributedString attributedStringWithAttachment:imageAttachment];
// [deCodedString replaceCharactersInRange:NSMakeRange(deCodedString.length, 0) withAttributedString:stringWithImage];
// incomingMessage.messageAttributedString = deCodedString;
}
}];
Please, try use placeholderTextView to simple input with icon placeholder support.
#IBOutlet weak var tvMessage: PlaceholderTextView!
let icon: NSTextAttachment = NSTextAttachment()
icon.image = UIImage(named: "paper-plane")
let iconString = NSMutableAttributedString(attributedString: NSAttributedString(attachment: icon))
tvMessage.icon = icon
let textColor = UIColor.gray
let lightFont = UIFont(name: "Helvetica-Light", size: tvMessage.font!.pointSize)
let italicFont = UIFont(name: "Helvetica-LightOblique", size: tvMessage.font!.pointSize)
let message = NSAttributedString(string: " " + "Personal Message", attributes: [ NSFontAttributeName: lightFont!, NSForegroundColorAttributeName: textColor])
iconString.append(message)
let option = NSAttributedString(string: " " + "Optional", attributes: [ NSFontAttributeName: italicFont!, NSForegroundColorAttributeName: textColor])
iconString.append(option)
tvMessage.attributedPlaceHolder = iconString
tvMessage.layoutSubviews()
You can refer to how MLLabel work.
Main ideal is NSTextAttachment
Create ImageAttachment extends NSTextAttachment -> override - (nullable UIImage *)imageForBounds:(CGRect)imageBounds textContainer:(nullable NSTextContainer *)textContainer characterIndex:(NSUInteger)charIndex to return image size like you want.
Create NSAttributedString with [NSAttributedString attributedStringWithAttachment:ImageAttachment]
Create NSMutableAttributedString and append attributed string of ImageAttachment using - (void)replaceCharactersInRange:(NSRange)range withAttributedString:(NSAttributedString *)attrString;
Result: You have NSMutableAttributedString contain your image and set it to textView.attributedText
Sample: HERE