I have a UITextView where I want to allow the user to insert photos from the photo library and PDFs from a document picker.
It is quite easy to embed a photo into the text view attributed string:
let attachment = NSTextAttachment(data: image.jpegData(1.0), ofType: kUTTypeJPEG as String)
attachment.bounds = CGRect(origin: CGPoint.zero, size: CGSize(width: 40, height: 40))
let attachmentString = NSAttributedString(attachment: attachment)
let text = NSMutableAttributedString(attributedString: textView.attributedText)
text.append(attachmentString)
textView.attributedText = text
This works perfectly for a photo: the image displays in the UITextView and the attachment is retrievable from the attributedText:
textView.attributedText.enumerateAttribute(.attachment, in: NSMakeRange(0, attributedText.length), options: []) { (item, range, ptr) in
if let attachment = item as? NSTextAttachment {
files.append(FileAttachment(data: attachment.contents, type: attachment.fileType))
}
The problem comes when trying to attach a PDF.
If I follow the exact same pattern with a PDF, replacing the data with PDF data and file type with kUTTypePDF, the attachment is created but does not display anything in the UITextView.
I should simply be able to set an image on the NSTextAttachment as a representation of the contents but this resets the attachment data.
let data = Data(contentsOf: pdfURL)
let attachment = NSTextAttachment(data: data, ofType: kUTTypePDF as String)
attachment.image = UIImage(named: "pdf-document-icon")
attachment.bounds = CGRect(origin: CGPoint.zero, size: CGSize(width: 40, height: 40))
let attachmentString = NSAttributedString(attachment: attachment)
let text = NSMutableAttributedString(attributedString: textView.attributedText)
text.append(attachmentString)
textView.attributedText = text
Now, the PDF attachment displays in the text view as a nice icon, but later when I enumerate the attachments the contents and file type are nil. i.e. setting an image on NSTextAttachment resets contents and fileType
I need to both set content and separate image representing the content on NSTextAttachment.
The best solution I have come up with so far is to subclass NSTextAttachment:
class TextAttachmentWithThumbnail: NSTextAttachment {
private var thumbnail: UIImage?
override var image: UIImage? {
get { return thumbnail }
set { thumbnail = newValue }
}
}
So that setting an image no longer resets contents and fileType
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've tried so many different things based on what I found online and still can't do this. There are similar questions that I have tried to piece together but it's still not working. Here is what I have so far:
class CustomUITextView: UITextView {
override func paste(sender: AnyObject?) {
let data: NSData? = UIPasteboard.generalPasteboard().dataForPasteboardType("public.png")
if data != nil {
let attributedString = self.attributedText.mutableCopy() as! NSMutableAttributedString
let textAttachment = NSTextAttachment()
textAttachment.image = UIImage(data: data!)
let attrStringWithImage = NSAttributedString(attachment: textAttachment)
attributedString.replaceCharactersInRange(self.selectedRange, withAttributedString: attrStringWithImage)
self.attributedText = attributedString
} else {
let pasteBoard: UIPasteboard = UIPasteboard.generalPasteboard()
let text = NSAttributedString(string: pasteBoard.string!)
let attributedString = self.attributedText.mutableCopy() as! NSMutableAttributedString
attributedString.replaceCharactersInRange(self.selectedRange, withAttributedString: text)
self.attributedText = attributedString
}
}
}
Also, my IBOutlet for the textView is in another file and is of this custom type I created:
#IBOutlet var textView: CustomUITextView!
I created a subclass of UITextView and assigned it to my textview. I then override the paste function to allow for images since I read that UITextField does not support image pasting by default. This is currently working for the text but not the image ---> if I do a long press gesture to show the Menu, the "Paste" button never shows up if there is an image in the PasteBoard; it only shows if there is text.
Ultimately, I want to paste PNG, JPEGS, and images from URLS in my textview. Someone please help !!
I am building an iOS app where I have to add support for emojis within UILabel, so basically, whenever I receive a string containing either of these:
[kick-off]
[yellow-card]
[red-card]
[introduce]
[substitute]
[attention]
[free-kick]
[penalty]
[offside]
[extra-time]
[throw-in]
[corner]
[goal-post]
[bar]
[cheers]
[goal]
I have to replace these tags with a corresponding emoji. I have custom images for these emojis:
https://cdn-waf-beta.global.ssl.fastly.net/0.55.12/static/images/WAF_live_icons_sprite.png
Any idea how could I pull that off using Swift?
You need to create an NSTextAttachment and append it to an NSAttributedString.
Example:
let stringAttributes = [
// insert any attributes here
NSFontAttributeName : UIFont.systemFontOfSize(14)
,NSForegroundColorAttributeName : UIColor.blackColor()
]
let attributedString = NSMutableAttributedString(string: "the string before your image ", attributes: stringAttributes)
// Image needs to be NSData
let imageData:NSData = UIImagePNGRepresentation(yourImage)!
// Attachment type is very important
let attachment = NSTextAttachment(data: imageData, ofType: "public.png")
let attachmentString = NSAttributedString(attachment: attachment)
attributedString.appendAttributedString(attachmentString)
print(attributedString)
You can find the appropriate uti (Uniform Type Identifier) here
Here is the latest (swift v5) syntax with a few additions:
func strgWithImage(yourImage: String, preImage: String, postImage: String) -> NSMutableAttributedString {
// Call function with attributed text label:
// testLabel.attributedText = strgWithImage(yourImage: "doneX.png", preImage: "preS", postImage: "postS")
let stringAttributes = [
// insert any attributes here
NSAttributedString.Key.font : UIFont.systemFont(ofSize: 14)
,NSAttributedString.Key.foregroundColor : UIColor.black
]
let attributedString = NSMutableAttributedString(string: preImage, attributes: stringAttributes)
let postString = NSMutableAttributedString(string: postImage, attributes: stringAttributes)
let attachment = NSTextAttachment()
attachment.image = UIImage(named:yourImage) // loaded in assets
let imageOffsetY: CGFloat = -15.0
attachment.bounds = CGRect(x: 0, y: imageOffsetY, width: attachment.image!.size.width, height: attachment.image!.size.height)
let attachmentString = NSAttributedString(attachment: attachment)
attributedString.append(attachmentString)
attributedString.append(postString)
return attributedString
}
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