How to add image and text in UITextView in IOS? - ios
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
Related
How to place image inside UILabel on start of UILabel text?
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 }
How to mask text of a UILabel or a UITextView?
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.
adding image to NSAttributedString makes the text not centre
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.
How to insert custom emoji in UILabel programmatically, in Swift?
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 }
How to get text / String from nth line of UILabel?
Is there an easy way to get (or simply display) the text from a given line in a UILabel? My UILabel is correctly displaying my text and laying it out beautifully but occasionally I need to be able to just show certain lines but obviously I need to know how UILabel has positioned everything to do this. I know this could easily be done with a substring but I'd need to know the start and end point of the line. Alternatively I could scroll the UILabel if there was some kind of offset to the UILabel's frame and hide the rest of the content I didn't want to see. I've not been able to uncover anything that shows how this could be done easily. Anyone got any good ideas? Thanks iphaaw
I have better way to find it. You can get this with the help of CoreText.framework. 1.Add CoreText.framework. 2.Import #import <CoreText/CoreText.h>. Then use below method: - (NSArray *)getLinesArrayOfStringInLabel:(UILabel *)label { NSString *text = [label text]; UIFont *font = [label font]; CGRect rect = [label frame]; CTFontRef myFont = CTFontCreateWithName((__bridge CFStringRef)([font fontName]), [font pointSize], NULL); NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:text]; [attStr addAttribute:(NSString *)kCTFontAttributeName value:(__bridge id)myFont range:NSMakeRange(0, attStr.length)]; CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attStr); CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, CGRectMake(0,0,rect.size.width,100000)); CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, NULL); NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame); NSMutableArray *linesArray = [[NSMutableArray alloc]init]; for (id line in lines) { CTLineRef lineRef = (__bridge CTLineRef )line; CFRange lineRange = CTLineGetStringRange(lineRef); NSRange range = NSMakeRange(lineRange.location, lineRange.length); NSString *lineString = [text substringWithRange:range]; [linesArray addObject:lineString]; } return (NSArray *)linesArray; } Call this method :- NSArray *linesArray = [self getLinesArrayOfStringInLabel:yourLabel]; Now you can use linesArray. SWIFT 4 VERSION func getLinesArrayOfString(in label: UILabel) -> [String] { /// An empty string's array var linesArray = [String]() guard let text = label.text, let font = label.font else {return linesArray} let rect = label.frame let myFont = CTFontCreateWithFontDescriptor(font.fontDescriptor, 0, nil) let attStr = NSMutableAttributedString(string: text) attStr.addAttribute(kCTFontAttributeName as NSAttributedString.Key, value: myFont, range: NSRange(location: 0, length: attStr.length)) let frameSetter: CTFramesetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString) let path: CGMutablePath = CGMutablePath() path.addRect(CGRect(x: 0, y: 0, width: rect.size.width, height: 100000), transform: .identity) let frame: CTFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil) guard let lines = CTFrameGetLines(frame) as? [Any] else {return linesArray} for line in lines { let lineRef = line as! CTLine let lineRange: CFRange = CTLineGetStringRange(lineRef) let range = NSRange(location: lineRange.location, length: lineRange.length) let lineString: String = (text as NSString).substring(with: range) linesArray.append(lineString) } return linesArray } Use: let lines: [String] = getLinesArrayOfString(in: label)
Swift 3 func getLinesArrayFromLabel(label:UILabel) -> [String] { let text:NSString = label.text! as NSString // TODO: Make safe? let font:UIFont = label.font let rect:CGRect = label.frame let myFont:CTFont = CTFontCreateWithName(font.fontName as CFString, font.pointSize, nil) let attStr:NSMutableAttributedString = NSMutableAttributedString(string: text as String) attStr.addAttribute(String(kCTFontAttributeName), value:myFont, range: NSMakeRange(0, attStr.length)) let frameSetter:CTFramesetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString) let path:CGMutablePath = CGMutablePath() path.addRect(CGRect(x:0, y:0, width:rect.size.width, height:100000)) let frame:CTFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil) let lines = CTFrameGetLines(frame) as NSArray var linesArray = [String]() for line in lines { let lineRange = CTLineGetStringRange(line as! CTLine) let range:NSRange = NSMakeRange(lineRange.location, lineRange.length) let lineString = text.substring(with: range) linesArray.append(lineString as String) } return linesArray } Swift 2 (Xcode 7) version (tested, and re-edited from the Swift 1 answer) func getLinesArrayOfStringInLabel(label:UILabel) -> [String] { let text:NSString = label.text! // TODO: Make safe? let font:UIFont = label.font let rect:CGRect = label.frame let myFont:CTFontRef = CTFontCreateWithName(font.fontName, font.pointSize, nil) let attStr:NSMutableAttributedString = NSMutableAttributedString(string: text as String) attStr.addAttribute(String(kCTFontAttributeName), value:myFont, range: NSMakeRange(0, attStr.length)) let frameSetter:CTFramesetterRef = CTFramesetterCreateWithAttributedString(attStr as CFAttributedStringRef) let path:CGMutablePathRef = CGPathCreateMutable() CGPathAddRect(path, nil, CGRectMake(0, 0, rect.size.width, 100000)) let frame:CTFrameRef = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil) let lines = CTFrameGetLines(frame) as NSArray var linesArray = [String]() for line in lines { let lineRange = CTLineGetStringRange(line as! CTLine) let range:NSRange = NSMakeRange(lineRange.location, lineRange.length) let lineString = text.substringWithRange(range) linesArray.append(lineString as String) } return linesArray }
Answer with Proper release !!!! -(NSArray *)getLinesArrayOfStringInLabel:(UILabel *)label { NSString *text = [label text]; UIFont *font = [label font]; CGRect rect = [label frame]; CTFontRef myFont = CTFontCreateWithName(( CFStringRef)([font fontName]), [font pointSize], NULL); NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:text]; [attStr addAttribute:(NSString *)kCTFontAttributeName value:( id)myFont range:NSMakeRange(0, attStr.length)]; CFRelease(myFont); CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString(( CFAttributedStringRef)attStr); CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, CGRectMake(0,0,rect.size.width,100000)); CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, NULL); NSArray *lines = ( NSArray *)CTFrameGetLines(frame); NSMutableArray *linesArray = [[NSMutableArray alloc]init]; for (id line in lines) { CTLineRef lineRef = ( CTLineRef )line; CFRange lineRange = CTLineGetStringRange(lineRef); NSRange range = NSMakeRange(lineRange.location, lineRange.length); NSString *lineString = [text substringWithRange:range]; CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attStr, lineRange, kCTKernAttributeName, (CFTypeRef)([NSNumber numberWithFloat:0.0])); CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attStr, lineRange, kCTKernAttributeName, (CFTypeRef)([NSNumber numberWithInt:0.0])); //NSLog(#"''''''''''''''''''%#",lineString); [linesArray addObject:lineString]; } [attStr release]; CGPathRelease(path); CFRelease( frame ); CFRelease(frameSetter); return (NSArray *)linesArray; }
Very important change regarding iOS 11+ Starting with iOS 11, Apple intentionally changed the behaviour of their word-wrapping feature for UILabel which effects detecting the String contents of individual lines in a multiline UILabel. By design, the word-wrapping of the UILabel now avoids orphaned text (single words in a new line), as discussed here: word wrapping in iOS 11 Because of that, the way CTFrameGetLines(frame) returns the CTLine array of all lines in the label no longer works correctly if the new word-wrapping that avoids orphaned text takes effect in a particular line. To the contrary, it results in parts of the String that by the new word wrapping design would belong to the next line instead end up in the line in focus. A tested fix for this problem can be found in my altered version of #TheTiger's answer, which makes use of calculating the actual content size of the UILabel using sizeThatFits(size:), before using that size to create the rect / path written in Swift 4: extension UILabel { /// creates an array containing one entry for each line of text the label has var lines: [String]? { guard let text = text, let font = font else { return nil } let attStr = NSMutableAttributedString(string: text) attStr.addAttribute(NSAttributedString.Key.font, value: font, range: NSRange(location: 0, length: attStr.length)) let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString) let path = CGMutablePath() // size needs to be adjusted, because frame might change because of intelligent word wrapping of iOS let size = sizeThatFits(CGSize(width: self.frame.width, height: .greatestFiniteMagnitude)) path.addRect(CGRect(x: 0, y: 0, width: size.width, height: size.height), transform: .identity) let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attStr.length), path, nil) guard let lines = CTFrameGetLines(frame) as? [Any] else { return nil } var linesArray: [String] = [] for line in lines { let lineRef = line as! CTLine let lineRange = CTLineGetStringRange(lineRef) let range = NSRange(location: lineRange.location, length: lineRange.length) let lineString = (text as NSString).substring(with: range) linesArray.append(lineString) } return linesArray } } This UILabel extension returns the contents of the label as a String array with one entry per line exactly as presented to the eye of the user.
I don't think there's a native way for doing this (like a "takethenline" method). I can figure out a tricky solution but I'm not sure is the best one. You could split your label into an array of words. Then you could loop the array and check the text height until that word like this: NSString *texttocheck; float old_height = 0; int linenumber = 0; for (x=0; x<[wordarray lenght]; x++) { texttocheck = [NSString stringWithFormat:#"%# %#", texttocheck, [wordarray objectAtIndex:x]]; float height = [text sizeWithFont:textLabel.font constrainedToSize:CGSizeMake(textLabel.bounds.size.width,99999) lineBreakMode:UILineBreakModeWordWrap].height; if (old_height < height) { linenumber++; } } If height changes, it means there's a line break before the word. I can't check if the syntax is written correctly now, so you have to check it yourself.
This is the Swift 3 version for getting all the lines in the label. (#fredpi has a similar answer but it's only for the first line) extension UILabel { func getArrayOfLinesInLabel() -> [String] { let text = NSString(string: self.text ?? "-- -- -- --") let font = self.font ?? // Your default font here let rect = self.frame let myFont = CTFontCreateWithName(font.fontName as CFString?, font.pointSize, nil) let attStr = NSMutableAttributedString(string: text as String) attStr.addAttribute(String(kCTFontAttributeName), value:myFont, range: NSRange(location: 0, length: attStr.length)) let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString) let path = CGPath(rect: CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height), transform: nil) let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil) guard let lines = CTFrameGetLines(frame) as? [CTLine] else { return [] } var linesArray = [String]() for line in lines { let lineRange = CTLineGetStringRange(line) let range = NSRange(location: lineRange.location, length: lineRange.length) let lineString = text.substring(with: range) linesArray.append(lineString as String) } return linesArray } }
Swift 3 – Xcode 8.1 I've put together code from the previous answers to create a Swift 3, Xcode 8.1-compatible extension to UILabel returning the first line of the label. import CoreText extension UILabel { /// Returns the String displayed in the first line of the UILabel or "" if text or font is missing var firstLineString: String { guard let text = self.text else { return "" } guard let font = self.font else { return "" } let rect = self.frame let attStr = NSMutableAttributedString(string: text) attStr.addAttribute(String(kCTFontAttributeName), value: CTFontCreateWithName(font.fontName as CFString, font.pointSize, nil), range: NSMakeRange(0, attStr.length)) let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString) let path = CGMutablePath() path.addRect(CGRect(x: 0, y: 0, width: rect.size.width + 7, height: 100)) let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil) guard let line = (CTFrameGetLines(frame) as! [CTLine]).first else { return "" } let lineString = text[text.startIndex...text.index(text.startIndex, offsetBy: CTLineGetStringRange(line).length-2)] return lineString } } To use it, simple call firstLineString on your UILabel instance like this: let firstLine = myLabel.firstLineString
The accepted answer is very good. I refactored two places: changed 10000 to CGFloat.greatestFiniteMagnitude Added it to an extension of UILabel I also want to mention, if you create the label by setting the frame it works fine. If you use autolayout then dont forgot to call youLabel.layoutIfNeeded() to get correct frame size. Here is the code: extension UILabel { var stringLines: [String] { guard let text = text, let font = font else { return [] } let ctFont = CTFontCreateWithName(font.fontName as CFString, font.pointSize, nil) let attStr = NSMutableAttributedString(string: text) attStr.addAttribute(kCTFontAttributeName as NSAttributedString.Key, value: ctFont, range: NSRange(location: 0, length: attStr.length)) let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString) let path = CGMutablePath() path.addRect(CGRect(x: 0, y: 0, width: self.frame.size.width, height: CGFloat.greatestFiniteMagnitude), transform: .identity) let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil) guard let lines = CTFrameGetLines(frame) as? [Any] else { return [] } return lines.map { line in let lineRef = line as! CTLine let lineRange: CFRange = CTLineGetStringRange(lineRef) let range = NSRange(location: lineRange.location, length: lineRange.length) return (text as NSString).substring(with: range) } } }
If all your characters are displayed in the same size, i.e. they're enclosed in a box of common size, you can exploit that. (This seems to be the case with Japanese characters, for example.) Otherwise you can query the size of each character in the display font and calculate what the line would have to be. The only worry then is that your calculation might disagree with what Apple's doing behind the scenes - in which case, I recommend you go to the trouble of overriding the text frame drawing. Look up Core Text in the documents for this. (I may have been doing this wrong, but I didn't find Apple's method as given in the docs was very accurate, so I did something else myself.)
Sorry, my reputation is too low to place a comment. This is a comment to https://stackoverflow.com/a/53783203/2439941 from Philipp Jahoda. Your code snippet worked flawless, until we enabled Dynamic Type on the UILabel. When we set the text size to the largest value in the iOS Settings app, it started to miss characters in the last line of the returned array. Or even missing the last line completely with a significant amount of text. We managed to resolve this by using a different way to get frame: let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString) let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: self.frame.width, height: .greatestFiniteMagnitude)) let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attStr.length), path.cgPath, nil) guard let lines = CTFrameGetLines(frame) as? [Any] else { return nil } Now it works correctly for any Dynamic Type size. The complete function is then: extension UILabel { /// creates an array containing one entry for each line of text the label has var lines: [String]? { guard let text = text, let font = font else { return nil } let attStr = NSMutableAttributedString(string: text) attStr.addAttribute(NSAttributedString.Key.font, value: font, range: NSRange(location: 0, length: attStr.length)) let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString) let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: self.frame.width, height: .greatestFiniteMagnitude)) let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attStr.length), path.cgPath, nil) guard let lines = CTFrameGetLines(frame) as? [Any] else { return nil } var linesArray: [String] = [] for line in lines { let lineRef = line as! CTLine let lineRange = CTLineGetStringRange(lineRef) let range = NSRange(location: lineRange.location, length: lineRange.length) let lineString = (text as NSString).substring(with: range) linesArray.append(lineString) } return linesArray } }