I'm trying to add a dotted underline style to an NSAttributedString (on iOS7+/TextKit). Of course, I tried the built-in NSUnderlinePatternDot:
NSString *string = self.label.text;
NSRange underlineRange = [string rangeOfString:#"consetetur sadipscing elitr"];
NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:string];
[attString addAttributes:#{NSUnderlineStyleAttributeName: #(NSUnderlineStyleSingle | NSUnderlinePatternDot)} range:underlineRange];
self.label.attributedText = attString;
However, the style produced by this is actually rather dashed than dotted:
Am I missing something obvious here (NSUnderlinePatternReallyDotted? ;) ) or is there perhaps a way to customize the line-dot-pattern?
According to Eiko&Dave's answer, i made an example like this
i've try to finish that by using UILabel rather than UITextView, but could not find a solution after searching from stackoverflow or other websites. So, i used UITextView to do that.
let storage = NSTextStorage()
let layout = UnderlineLayout()
storage.addLayoutManager(layout)
let container = NSTextContainer()
container.widthTracksTextView = true
container.lineFragmentPadding = 0
container.maximumNumberOfLines = 2
container.lineBreakMode = .byTruncatingTail
layout.addTextContainer(container)
let textView = UITextView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width-40, height: 50), textContainer: container)
textView.isUserInteractionEnabled = true
textView.isEditable = false
textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
textView.text = "1sadasdasdasdasdsadasdfdsf"
textView.backgroundColor = UIColor.white
let rg = NSString(string: textView.text!).range(of: textView.text!)
let attributes = [NSAttributedString.Key.underlineStyle.rawValue: 0x11,
NSAttributedString.Key.underlineColor: UIColor.blue.withAlphaComponent(0.2),
NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17),
NSAttributedString.Key.baselineOffset:10] as! [NSAttributedString.Key : Any]
storage.addAttributes(attributes, range: rg)
view.addSubview(textView)
override LayoutManage Method
class UnderlineLayout: NSLayoutManager {
override func drawUnderline(forGlyphRange glyphRange: NSRange, underlineType underlineVal: NSUnderlineStyle, baselineOffset: CGFloat, lineFragmentRect lineRect: CGRect, lineFragmentGlyphRange lineGlyphRange: NSRange, containerOrigin: CGPoint) {
if let container = textContainer(forGlyphAt: glyphRange.location, effectiveRange: nil) {
let boundingRect = self.boundingRect(forGlyphRange: glyphRange, in: container)
let offsetRect = boundingRect.offsetBy(dx: containerOrigin.x, dy: containerOrigin.y)
let left = offsetRect.minX
let bottom = offsetRect.maxY-15
let width = offsetRect.width
let path = UIBezierPath()
path.lineWidth = 3
path.move(to: CGPoint(x: left, y: bottom))
path.addLine(to: CGPoint(x: left + width, y: bottom))
path.stroke()
}
}
in my solution, i've to expand lineSpacing also keep a customize underline by using NSAttributedString property NSMutableParagraphStyle().lineSpacing, but it seems that didn't work, but NSAttributedString.Key.baselineOffset is worked. hope can
I've spent a little bit of time playing around with Text Kit to get this and other similar scenarios to work. The actual Text Kit solution for this problem is to subclass NSLayoutManager and override drawUnderline(forGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:)
This is the method that gets called to actually draw the underline that is requested by the attributes in the NSTextStorage. Here's a description of the parameters that get passed in:
glyphRange the index of the first glyph and the number of following glyphs to be underlined
underlineType the NSUnderlineStyle value of the NSUnderlineAttributeName attribute
baselineOffset the distance between the bottom of the bounding box and the baseline offset for the glyphs in the provided range
lineFragmentRect the rectangle that encloses the entire line that contains glyphRange
lineFragmentGlyphRange the glyph range that makes up the entire line that contains glyphRange
containerOrigin the offset of the container within the text view to which it belongs
Steps to draw underline:
Find the NSTextContainer that contains glyphRange using NSLayoutManager.textContainer(forGlyphAt:effectiveRange:).
Get the bounding rectangle for glyphRange using NSLayoutManager.boundingRect(forGlyphRange:in:)
Offset the bounding rectangle by the text container origin using CGRect.offsetBy(dx:dy:)
Draw your custom underline somewhere between the bottom of the offset bounding rectangle and the baseline (as determined by baselineOffset)
I know this is a 2 year old story but maybe it will help someone facing the same problem, like I did last week.
My workaround was to subclass UITextView, add a subview (the dotted lines container) and use the textView’s layoutManager method enumerateLineFragmentsForGlyphRange to get the number of lines and their frames.
Knowing the line’s frame I calculated the y where I wanted the dots. So I created a view (lineView in which I drawn dots), set clipsToBounds to YES, the width to that of the textView and then
added as a subview in the linesContainer at that y.
After that I set the lineView’s width to that returned by enumerateLineFragmentsForGlyphRange.
For multiple rows the same approach: update frames, add new lines or remove according to what enumerateLineFragmentsForGlyphRange returns.
This method is called in textViewDidChange after each text update.
Here is the code to get the array of lines and their frames
NSLayoutManager *layoutManager = [self layoutManager];
[layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(0, [layoutManager numberOfGlyphs])
usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {
[linesFramesArray addObject:NSStringFromCGRect(usedRect)];
}
];
I need to achieve something exactly like NSLineBreakByTruncatingHead for UITextField as shown here. Let's assume the original text is:
This is the long text that cannot be shown inside a UITextField
I need it like:
...cannot be shown inside a UITextField
but currently I am getting something like:
This is the long text that cannot...
simply the truncation at the beginning. The lineBreakMode property is not given for UITextField. How can I achieve it?
I took the solution here and modified it to truncate the head of a string instead of the tail. Know that it only shows the ellipsis when the field is not being edited.
NOTE: This solution is for iOS 7+ only. To use in iOS 6, use sizeWithFont: instead of sizeWithAttributes: in the NSString+TruncateToWidth.m file.
EDIT: Added support for iOS 6
NSString+TruncateToWidth.h
#interface NSString (TruncateToWidth)
- (NSString*)stringByTruncatingToWidth:(CGFloat)width withFont:(UIFont *)font;
#end
NSString+TruncateToWidth.m
#import "NSString+TruncateToWidth.h"
#define ellipsis #"…"
#implementation NSString (TruncateToWidth)
- (NSString*)stringByTruncatingToWidth:(CGFloat)width withFont:(UIFont *)font
{
// Create copy that will be the returned result
NSMutableString *truncatedString = [self mutableCopy];
// Make sure string is longer than requested width
if ([self widthWithFont:font] > width)
{
// Accommodate for ellipsis we'll tack on the beginning
width -= [ellipsis widthWithFont:font];
// Get range for first character in string
NSRange range = {0, 1};
// Loop, deleting characters until string fits within width
while ([truncatedString widthWithFont:font] > width)
{
// Delete character at beginning
[truncatedString deleteCharactersInRange:range];
}
// Append ellipsis
[truncatedString replaceCharactersInRange:NSMakeRange(0, 0) withString:ellipsis];
}
return truncatedString;
}
- (CGFloat)widthWithFont:(UIFont *)font
{
if([self respondsToSelector:#selector(sizeWithAttributes:)])
return [self sizeWithAttributes:#{NSFontAttributeName:font}].width;
return [self sizeWithFont:font].width;
}
Using it:
...
// Make sure to import the header file where you want to use it
// assumes instance variable holds your string that populates the field
fieldString = #"abcdefghijklmnopqrstuvwxyz1234567890";
// Size will need to be less than text field's width to account for padding
_myTextField.text = [fieldString stringByTruncatingToWidth:(_myTextField.frame.size.width - 15) withFont:_myTextField.font];
...
// use textFieldShouldBeginEditing to make it animate from the start of the field to the end of the string if you prefer that. I found it a little distracting
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
textField.text = fieldString;
}
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField
{
fieldString = textField.text;
textField.text = [textField.text stringByTruncatingToWidth:(textField.frame.size.width - 15) withFont:textField.font];
return YES;
}
I had a similar requirement, I wrote a Swift version of #Stonz2 solution, it worked most of the times, havent used in production yet as the requirement was removed later... anyways posting it here
extension String {
func stringByTruncatingLeadingForWidth(width: CGFloat, withFont font: UIFont) -> String{
var modifiedString = self
var mutableWidth = width
let ellipsis = "..."
if (self.widthOfString(usingFont: font) > width) {
let ellipsisWidth = ellipsis.widthOfString(usingFont: font)
// else this will go for infinite loop...mutable width will go -ve
if mutableWidth > ellipsisWidth {
mutableWidth -= ellipsis.widthOfString(usingFont: font)
}
let range = NSMakeRange(0, 1)
while modifiedString.widthOfString(usingFont: font) > mutableWidth {
modifiedString.deleteCharactersInRange(range: range)
print(modifiedString)
print(mutableWidth)
}
guard let swiftRange = Range(NSMakeRange(0, 3), in: modifiedString) else { return "" }
modifiedString.replaceSubrange(swiftRange, with: [".",".","."])
}
return modifiedString
}
func widthOfString(usingFont font: UIFont) -> CGFloat {
let fontAttributes = [NSAttributedString.Key.font: font]
let size = self.size(withAttributes: fontAttributes)
return size.width
}
mutating func deleteCharactersInRange(range: NSRange) {
guard let swiftRange = Range(range, in: self) else { return }
self.removeSubrange(swiftRange)
}
}
var str1 = "Hello how are you"
let newStr = str1.stringByTruncatingLeadingForWidth(width: 100, withFont: .systemFont(ofSize: 15))
It's trivial to make hyperlinks clickable in a UITextView. You just set the "detect links" checkbox on the view in IB, and it detects HTTP links and turns them into hyperlinks.
However, that still means that what the user sees is the "raw" link. RTF files and HTML both allow you to set up a user-readable string with a link "behind" it.
It's easy to install attributed text into a text view (or a UILabel or UITextField, for that matter.) However, when that attributed text includes a link, it is not clickable.
Is there a way to make user-readable text clickable in a UITextView, UILabel or UITextField?
The markup is different on SO, but here is the general idea. What I want is text like this:
This morph was generated with Face Dancer, Click to view in the app store.
The only thing I can get is this:
This morph was generated with Face Dancer, Click on http://example.com/facedancer to view in the app store.
Use NSMutableAttributedString.
NSMutableAttributedString * str = [[NSMutableAttributedString alloc] initWithString:#"Google"];
[str addAttribute: NSLinkAttributeName value: #"http://www.google.com" range: NSMakeRange(0, str.length)];
yourTextView.attributedText = str;
Edit:
This is not directly about the question but just to clarify, UITextField and UILabel does not support opening URLs. If you want to use UILabel with links you can check TTTAttributedLabel.
Also you should set dataDetectorTypes value of your UITextView to UIDataDetectorTypeLink or UIDataDetectorTypeAll to open URLs when clicked. Or you can use delegate method as suggested in the comments.
I found this really useful but I needed to do it in quite a few places so I've wrapped my approach up in a simple extension to NSMutableAttributedString:
Swift 3
extension NSMutableAttributedString {
public func setAsLink(textToFind:String, linkURL:String) -> Bool {
let foundRange = self.mutableString.range(of: textToFind)
if foundRange.location != NSNotFound {
self.addAttribute(.link, value: linkURL, range: foundRange)
return true
}
return false
}
}
Swift 2
import Foundation
extension NSMutableAttributedString {
public func setAsLink(textToFind:String, linkURL:String) -> Bool {
let foundRange = self.mutableString.rangeOfString(textToFind)
if foundRange.location != NSNotFound {
self.addAttribute(NSLinkAttributeName, value: linkURL, range: foundRange)
return true
}
return false
}
}
Example usage:
let attributedString = NSMutableAttributedString(string:"I love stackoverflow!")
let linkWasSet = attributedString.setAsLink("stackoverflow", linkURL: "http://stackoverflow.com")
if linkWasSet {
// adjust more attributedString properties
}
Objective-C
I've just hit a requirement to do the same in a pure Objective-C project, so here's the Objective-C category.
#interface NSMutableAttributedString (SetAsLinkSupport)
- (BOOL)setAsLink:(NSString*)textToFind linkURL:(NSString*)linkURL;
#end
#implementation NSMutableAttributedString (SetAsLinkSupport)
- (BOOL)setAsLink:(NSString*)textToFind linkURL:(NSString*)linkURL {
NSRange foundRange = [self.mutableString rangeOfString:textToFind];
if (foundRange.location != NSNotFound) {
[self addAttribute:NSLinkAttributeName value:linkURL range:foundRange];
return YES;
}
return NO;
}
#end
Example usage:
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:"I love stackoverflow!"];
BOOL linkWasSet = [attributedString setAsLink:#"stackoverflow" linkURL:#"http://stackoverflow.com"];
if (linkWasSet) {
// adjust more attributedString properties
}
Make Sure that the NSTextField's Behavior attribute is set as Selectable.
I just created a subclass of UILabel to specially address such use cases. You can add multiple links easily and define different handlers for them. It also supports highlighting the pressed link when you touch down for touch feedback. Please refer to https://github.com/null09264/FRHyperLabel.
In your case, the code may like this:
FRHyperLabel *label = [FRHyperLabel new];
NSString *string = #"This morph was generated with Face Dancer, Click to view in the app store.";
NSDictionary *attributes = #{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]};
label.attributedText = [[NSAttributedString alloc]initWithString:string attributes:attributes];
[label setLinkForSubstring:#"Face Dancer" withLinkHandler:^(FRHyperLabel *label, NSString *substring){
[[UIApplication sharedApplication] openURL:aURL];
}];
Sample Screenshot (the handler is set to pop an alert instead of open a url in this case)
Minor improvement to ujell's solution: If you use NSURL instead of a NSString, you can use any URL (e.g. custom urls)
NSURL *URL = [NSURL URLWithString: #"whatsapp://app"];
NSMutableAttributedString * str = [[NSMutableAttributedString alloc] initWithString:#"start Whatsapp"];
[str addAttribute: NSLinkAttributeName value:URL range: NSMakeRange(0, str.length)];
yourTextField.attributedText = str;
Have fun!
Swift 4:
var string = "Google"
var attributedString = NSMutableAttributedString(string: string, attributes:[NSAttributedStringKey.link: URL(string: "http://www.google.com")!])
yourTextView.attributedText = attributedString
Swift 3.1:
var string = "Google"
var attributedString = NSMutableAttributedString(string: string, attributes:[NSLinkAttributeName: URL(string: "http://www.google.com")!])
yourTextView.attributedText = attributedString
I too had a similar requirement, initially I used UILabel and then I realized that UITextView is better. I made UITextView behave like UILabel by disabling interaction and scrolling and made a category method for NSMutableAttributedString to set link to text same as what Karl had done (+1 for that) this is my obj c version
-(void)setTextAsLink:(NSString*) textToFind withLinkURL:(NSString*) url
{
NSRange range = [self.mutableString rangeOfString:textToFind options:NSCaseInsensitiveSearch];
if (range.location != NSNotFound) {
[self addAttribute:NSLinkAttributeName value:url range:range];
[self addAttribute:NSForegroundColorAttributeName value:[UIColor URLColor] range:range];
}
}
you can use the below delegate then to handle the action
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)url inRange:(NSRange)characterRange
{
// do the task
return YES;
}
Use UITextView it supports clickable Links.
Create attributed string using the following code
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:strSomeTextWithLinks];
Then set UITextView text as follows
NSDictionary *linkAttributes = #{NSForegroundColorAttributeName: [UIColor redColor],
NSUnderlineColorAttributeName: [UIColor blueColor],
NSUnderlineStyleAttributeName: #(NSUnderlinePatternSolid)};
customTextView.linkTextAttributes = linkAttributes; // customizes the appearance of links
textView.attributedText = attributedString;
Make sure that you enable "Selectable" behavior of the UITextView in XIB.
The quick answer is using UITextView instead of UILabel. You need to enable Selectable and disable Editable.
Then disable scroll indicators and bounces.
My solution using NSMutableAttributedString from html string NSHTMLTextDocumentType
NSString *s = #"<p><a href='https://itunes.apple.com/us/app/xxxx/xxxx?mt=8'>https://itunes.apple.com/us/app/xxxx/xxxx?mt=8</a></p>";
NSMutableAttributedString *text = [[NSMutableAttributedString alloc]
initWithData: [s dataUsingEncoding:NSUnicodeStringEncoding]
options: #{ NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType }
documentAttributes: nil
error: nil
];
cell.content.attributedText = text;
The heart of my question was that I wanted to be able to create clickable links in text views/fields/labels without having to write custom code to manipulate the text and add the links. I wanted it to be data-driven.
I finally figured out how to do it. The issue is that IB doesn't honor embedded links.
Furthermore, the iOS version of NSAttributedString doesn't let you initialize an attributed string from an RTF file. The OS X version of NSAttributedString does have an initializer that takes an RTF file as input.
NSAttributedString conforms to the NSCoding protocol, so you can convert it to/from NSData
I created an OS X command line tool that takes an RTF file as input and outputs a file with the extension .data that contains the NSData from NSCoding. I then put the .data file into my project and add a couple of lines of code that loads the text into the view. The code looks like this (this project was in Swift) :
/*
If we can load a file called "Dates.data" from the bundle and convert it to an attributed string,
install it in the dates field. The contents contain clickable links with custom URLS to select
each date.
*/
if
let datesPath = NSBundle.mainBundle().pathForResource("Dates", ofType: "data"),
let datesString = NSKeyedUnarchiver.unarchiveObjectWithFile(datesPath) as? NSAttributedString
{
datesField.attributedText = datesString
}
For apps that use a lot of formatted text, I create a build rule that tells Xcode that all the .rtf files in a given folder are source and the .data files are the output. Once I do that, I simply add .rtf files to the designated directory, (or edit existing files) and the build process figures out that they are new/updated, runs the command line tool, and copies the files into the app bundle. It works beautifully.
I wrote a blog post that links to a sample (Swift) project demonstrating the technique. You can see it here:
Creating clickable URLs in a UITextField that open in your app
Swift 3 example to detect actions on attributed text taps
https://stackoverflow.com/a/44226491/5516830
let termsAndConditionsURL = TERMS_CONDITIONS_URL;
let privacyURL = PRIVACY_URL;
override func viewDidLoad() {
super.viewDidLoad()
self.txtView.delegate = self
let str = "By continuing, you accept the Terms of use and Privacy policy"
let attributedString = NSMutableAttributedString(string: str)
var foundRange = attributedString.mutableString.range(of: "Terms of use") //mention the parts of the attributed text you want to tap and get an custom action
attributedString.addAttribute(NSLinkAttributeName, value: termsAndConditionsURL, range: foundRange)
foundRange = attributedString.mutableString.range(of: "Privacy policy")
attributedString.addAttribute(NSLinkAttributeName, value: privacyURL, range: foundRange)
txtView.attributedText = attributedString
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "WebView") as! SKWebViewController
if (URL.absoluteString == termsAndConditionsURL) {
vc.strWebURL = TERMS_CONDITIONS_URL
self.navigationController?.pushViewController(vc, animated: true)
} else if (URL.absoluteString == privacyURL) {
vc.strWebURL = PRIVACY_URL
self.navigationController?.pushViewController(vc, animated: true)
}
return false
}
Like wise you can add any action you want with shouldInteractWith URLUITextFieldDelegate method.
Cheers!!
I have written a method, that adds a link(linkString) to a string (fullString) with a certain url(urlString):
- (NSAttributedString *)linkedStringFromFullString:(NSString *)fullString withLinkString:(NSString *)linkString andUrlString:(NSString *)urlString
{
NSRange range = [fullString rangeOfString:linkString options:NSLiteralSearch];
NSMutableAttributedString *str = [[NSMutableAttributedString alloc] initWithString:fullString];
NSMutableParagraphStyle *paragraphStyle = NSMutableParagraphStyle.new;
paragraphStyle.alignment = NSTextAlignmentCenter;
NSDictionary *attributes = #{NSForegroundColorAttributeName:RGB(0x999999),
NSFontAttributeName:[UIFont fontWithName:#"HelveticaNeue-Light" size:10],
NSParagraphStyleAttributeName:paragraphStyle};
[str addAttributes:attributes range:NSMakeRange(0, [str length])];
[str addAttribute: NSLinkAttributeName value:urlString range:range];
return str;
}
You should call it like this:
NSString *fullString = #"A man who bought the Google.com domain name for $12 and owned it for about a minute has been rewarded by Google for uncovering the flaw.";
NSString *linkString = #"Google.com";
NSString *urlString = #"http://www.google.com";
_youTextView.attributedText = [self linkedStringFromFullString:fullString withLinkString:linkString andUrlString:urlString];
I needed to keep using a pure UILabel, so called this from my tap recognizer (this is based on malex's response here: Character index at touch point for UILabel )
UILabel* label = (UILabel*)gesture.view;
CGPoint tapLocation = [gesture locationInView:label];
// create attributed string with paragraph style from label
NSMutableAttributedString* attr = [label.attributedText mutableCopy];
NSMutableParagraphStyle* paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.alignment = label.textAlignment;
[attr addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, label.attributedText.length)];
// init text storage
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attr];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
// init text container
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(label.frame.size.width, label.frame.size.height+100) ];
textContainer.lineFragmentPadding = 0;
textContainer.maximumNumberOfLines = label.numberOfLines;
textContainer.lineBreakMode = label.lineBreakMode;
[layoutManager addTextContainer:textContainer];
// find tapped character
NSUInteger characterIndex = [layoutManager characterIndexForPoint:tapLocation
inTextContainer:textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
// process link at tapped character
[attr enumerateAttributesInRange:NSMakeRange(characterIndex, 1)
options:0
usingBlock:^(NSDictionary<NSString *,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
if (attrs[NSLinkAttributeName]) {
NSString* urlString = attrs[NSLinkAttributeName];
NSURL* url = [NSURL URLWithString:urlString];
[[UIApplication sharedApplication] openURL:url];
}
}];
Use UITextView and set dataDetectorTypes for Link.
like this:
testTextView.editable = false
testTextView.dataDetectorTypes = .link
If you want to detect link, phone number,address etc..then
testTextView.dataDetectorTypes = .all
Just find a code-free solution for UITextView:
Enable Detection->Links options, the URL and also email will be detected and clickable!
Update:
There were 2 key parts to my question:
How to make a link where the text shown for the clickable link is different than the actual link that is invoked:
How to set up the links without having to use custom code to set the attributes on the text.
It turns out that iOS 7 added the ability to load attributed text from NSData.
I created a custom subclass of UITextView that takes advantage of the #IBInspectable attribute and lets you load contents from an RTF file directly in IB. You simply type the filename into IB and the custom class does the rest.
Here are the details:
In iOS 7, NSAttributedString gained the method initWithData:options:documentAttributes:error:. That method lets you load an NSAttributedString from an NSData object. You can first load an RTF file into NSData, then use initWithData:options:documentAttributes:error: to load that NSData into your text view. (Note that there is also a method initWithFileURL:options:documentAttributes:error: that will load an attributed string directly from a file, but that method was deprecated in iOS 9. It's safer to use the method initWithData:options:documentAttributes:error:, which wasn't deprecated.
I wanted a method that let me install clickable links into my text views without having to create any code specific to the links I was using.
The solution I came up with was to create a custom subclass of UITextView I call RTF_UITextView and give it an #IBInspectable property called RTF_Filename. Adding the #IBInspectable attribute to a property causes Interface Builder to expose that property in the "Attributes Inspector." You can then set that value from IB wihtout custom code.
I also added an #IBDesignable attribute to my custom class. The #IBDesignable attribute tells Xcode that it should install a running copy of your custom view class into Interface builder so you can see it in the graphical display of your view hierarchy. ()Unfortunately, for this class, the #IBDesignable property seems to be flaky. It worked when I first added it, but then I deleted the plain text contents of my text view and the clickable links in my view went away and I have not been able to get them back.)
The code for my RTF_UITextView is very simple. In addition to adding the #IBDesignable attribute and an RTF_Filename property with the #IBInspectable attribute, I added a didSet() method to the RTF_Filename property. The didSet() method gets called any time the value of the RTF_Filename property changes. The code for the didSet() method is quite simple:
#IBDesignable
class RTF_UITextView: UITextView
{
#IBInspectable
var RTF_Filename: String?
{
didSet(newValue)
{
//If the RTF_Filename is nil or the empty string, don't do anything
if ((RTF_Filename ?? "").isEmpty)
{
return
}
//Use optional binding to try to get an URL to the
//specified filename in the app bundle. If that succeeds, try to load
//NSData from the file.
if let fileURL = NSBundle.mainBundle().URLForResource(RTF_Filename, withExtension: "rtf"),
//If the fileURL loads, also try to load NSData from the URL.
let theData = NSData(contentsOfURL: fileURL)
{
var aString:NSAttributedString
do
{
//Try to load an NSAttributedString from the data
try
aString = NSAttributedString(data: theData,
options: [:],
documentAttributes: nil
)
//If it succeeds, install the attributed string into the field.
self.attributedText = aString;
}
catch
{
print("Nerp.");
}
}
}
}
}
Note that if the #IBDesignable property isn't going to reliably allow you to preview your styled text in Interface builder then it might be better to set the above code up as an extension of UITextView rather than a custom subclass. That way you could use it in any text view without having to change the text view to the custom class.
See my other answer if you need to support iOS versions prior to iOS 7.
You can download a sample project that includes this new class from gitHub:
DatesInSwift demo project on Github
Swift Version :
// Attributed String for Label
let plainText = "Apkia"
let styledText = NSMutableAttributedString(string: plainText)
// Set Attribuets for Color, HyperLink and Font Size
let attributes = [NSFontAttributeName: UIFont.systemFontOfSize(14.0), NSLinkAttributeName:NSURL(string: "http://apkia.com/")!, NSForegroundColorAttributeName: UIColor.blueColor()]
styledText.setAttributes(attributes, range: NSMakeRange(0, plainText.characters.count))
registerLabel.attributedText = styledText
In case you're having issues with what #Karl Nosworthy and #esilver had provided above, I've updated the NSMutableAttributedString extension to its Swift 4 version.
extension NSMutableAttributedString {
public func setAsLink(textToFind:String, linkURL:String) -> Bool {
let foundRange = self.mutableString.range(of: textToFind)
if foundRange.location != NSNotFound {
_ = NSMutableAttributedString(string: textToFind)
// Set Attribuets for Color, HyperLink and Font Size
let attributes = [NSFontAttributeName: UIFont.bodyFont(.regular, shouldResize: true), NSLinkAttributeName:NSURL(string: linkURL)!, NSForegroundColorAttributeName: UIColor.blue]
self.setAttributes(attributes, range: foundRange)
return true
}
return false
}
}
A quick addition to Duncan C's original description vis-á-vie IB behavior. He writes: "It's trivial to make hyperlinks clickable in a UITextView. You just set the "detect links" checkbox on the view in IB, and it detects http links and turns them into hyperlinks."
My experience (at least in xcode 7) is that you also have to unclick the "Editable" behavior for the urls to be detected & clickable.
In Swift 5.5
Since Swift 5.5 NSAttributedString is completely localizable and easy to use without even defining the number of characters.
func attributedStringBasics(important: Bool) {
var buy = AttributedString("Buy a new iPhone!")
buy.font = .body.bold()
var website = AttributedString("Visit Apple")
website.font = .body.italic()
website.link = URL(string: "http://www.apple.com")
var container = AttributeContainer()
if important {
container.foregroundColor = .red
container.underlineColor = .primary
} else {
container.foregroundColor = .primary
}
buy.mergeAttributes(container)
website.mergeAttributes(container)
print(buy)
print(website)
}
The excellent library from #AliSoftware OHAttributedStringAdditions makes it easy to add links in UILabel here is the documentation: https://github.com/AliSoftware/OHAttributedStringAdditions/wiki/link-in-UILabel
If you want to use the NSLinkAttributeName in a UITextView, then you may consider using the AttributedTextView library. It's a UITextView subclass that makes it very easy to handle these. For more info see: https://github.com/evermeer/AttributedTextView
You can make any part of the text interact like this (where textView1 is a UITextView IBoutlet):
textView1.attributer =
"1. ".red
.append("This is the first test. ").green
.append("Click on ").black
.append("evict.nl").makeInteract { _ in
UIApplication.shared.open(URL(string: "http://evict.nl")!, options: [:], completionHandler: { completed in })
}.underline
.append(" for testing links. ").black
.append("Next test").underline.makeInteract { _ in
print("NEXT")
}
.all.font(UIFont(name: "SourceSansPro-Regular", size: 16))
.setLinkColor(UIColor.purple)
And for handling hashtags and mentions you can use code like this:
textView1.attributer = "#test: What #hashtags do we have in #evermeer #AtributedTextView library"
.matchHashtags.underline
.matchMentions
.makeInteract { link in
UIApplication.shared.open(URL(string: "https://twitter.com\(link.replacingOccurrences(of: "#", with: ""))")!, options: [:], completionHandler: { completed in })
}
if you want active substring in your UITextView then you can use my extended TextView... its short and simple. You can edit it as you want.
result:
code:
https://github.com/marekmand/ActiveSubstringTextView
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:strSomeTextWithLinks];
NSDictionary *linkAttributes = #{NSForegroundColorAttributeName: [UIColor redColor],
NSUnderlineColorAttributeName: [UIColor blueColor],
NSUnderlineStyleAttributeName: #(NSUnderlinePatternSolid)};
customTextView.linkTextAttributes = linkAttributes; // customizes the appearance of links
textView.attributedText = attributedString;
KEY POINTS:
Make sure that you enable "Selectable" behavior of the UITextView in XIB.
Make sure that you disable "Editable" behavior of the UITextView in XIB.
I'm finally getting round to wrestling with Auto Layout and can't seem to figure out how to get right-to-left (RTL) support to work the way I'd expect/want...
I have designed the view in Interface Builder as shown:
With the resulting app running as expected when using English:
However when switching to an RTL language (Arabic in this case), the entire view flips (which is great) but the UILabel's text is still left aligned. I'd expect it to be right aligned to keep it up against the UIImageView.
Clearly I'm missing something and/or this isn't covered by Auto Layout.
Am I supposed to set the textAlignment manually when using an RTL language?
You want NSTextAlignmentNatural. That infers the text alignment from the loaded application language (not from the script).
For iOS 9 and later (using Xcode 7), you can set this in the storyboard (choose the --- alignment option). If you need to target earlier releases, you'll need to create an outlet to the label and set the alignment in awakeFromNib.
- (void)awakeFromNib {
[[self label] setTextAlignment:NSTextAlignmentNatural];
}
For me those solutions didn't help, and I ended up doing something pretty ugly but it's the only one that did the trick for me. I added it as an NSString category:
NSString+Extras.h:
#import <Foundation/Foundation.h>
#interface NSString (Extras)
- (NSTextAlignment)naturalTextAligment;
#end
NSString+Extras.m:
#import "NSString+Extras.h"
#implementation NSString (Extras)
- (NSTextAlignment)naturalTextAligment {
NSArray *tagschemes = [NSArray arrayWithObjects:NSLinguisticTagSchemeLanguage, nil];
NSLinguisticTagger *tagger = [[NSLinguisticTagger alloc] initWithTagSchemes:tagschemes options:0];
[tagger setString:self];
NSString *language = [tagger tagAtIndex:0 scheme:NSLinguisticTagSchemeLanguage tokenRange:NULL sentenceRange:NULL];
if ([language rangeOfString:#"he"].location != NSNotFound || [language rangeOfString:#"ar"].location != NSNotFound) {
return NSTextAlignmentRight;
} else {
return NSTextAlignmentLeft;
}
}
#end
To detect the language I used this SO answer.
Follow up from Ken's answer
Setting textAlignment to NSTextAlignmentNatural is not possible on UILabel, it will result in an exception getting thrown:
Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: 'textAlignment does not accept NSTextAlignmentNatural'
It does work when using attributed text and this can be set in Interface Builder as shown:
However, it would appear that attributed text is not picked up when localising the storyboard.
To get around this I have left the UILabel configured as plain in Interface Builder and created an NSAttributedString with the label's text, set the alignment on the attributed string and assign it to the label's attributedText property:
-(void)viewDidLoad
{
[super viewDidLoad];
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.alignment = NSTextAlignmentNatural;
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:self.lbl.text];
[string addAttribute:NSParagraphStyleAttributeName
value:paragraphStyle
range:NSMakeRange(0, string.length)];
self.lbl.attributedText = string;
}
This works fine in this simple case but I can see it falling over when you need more complex attributed string styling. But obviously in that case you'd probably just be using NSLocalizedString or equivalents when creating the NSAttributedString.
#Aviel answer as a swift UILabel extension
//MARK: UILabel extension
extension UILabel {
func decideTextDirection () {
let tagScheme = [NSLinguisticTagSchemeLanguage]
let tagger = NSLinguisticTagger(tagSchemes: tagScheme, options: 0)
tagger.string = self.text
let lang = tagger.tagAtIndex(0, scheme: NSLinguisticTagSchemeLanguage,
tokenRange: nil, sentenceRange: nil)
if lang?.rangeOfString("he") != nil || lang?.rangeOfString("ar") != nil {
self.textAlignment = NSTextAlignment.Right
} else {
self.textAlignment = NSTextAlignment.Left
}
}
}
How to use it ?
label.text = "كتابة باللغة العربية" // Assign text
label.decideTextDirection() // Decide direction
I think you don't want to use text alignment in this case, for a label.
You can just let the width be determined by intrinsicContentSize, and remove any width constraints on the label. You will achieve the desired effect of the label text aligned to the view.
For x axis, you only need this constraint between label and imageview:
[imageview]-[label]
This is only a horizontal spacing constraint. No leading or trailing to superview.
#Aviel answer as a swift 4 UILabel extension
extension UILabel {
func decideTextDirection () {
let tagScheme = [NSLinguisticTagScheme.language]
let tagger = NSLinguisticTagger(tagSchemes: tagScheme, options: 0)
tagger.string = self.text
let lang = tagger.tag(at: 0, scheme: NSLinguisticTagScheme.language,
tokenRange: nil, sentenceRange: nil)
if lang?.rawValue.range(of: "he") != nil || lang?.rawValue.range(of: "ar") != nil {
self.textAlignment = NSTextAlignment.right
} else {
self.textAlignment = NSTextAlignment.left
}
}
}
usage
label.text = "كتابة باللغة العربية" // Assign text
label.decideTextDirection() // Decide direction
This function helped me out
-(void)fnForWritingDirection:(UILabel*)label textFor:(NSString *)stringForText{
NSMutableAttributedString* attrStr = [[NSMutableAttributedString alloc] initWithString: [stringForText stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
[paragraphStyle setBaseWritingDirection:NSWritingDirectionRightToLeft];
[attrStr addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [attrStr length])];
label.attributedText=attrStr;
}
this worked for me:
extension UILabel {
func decideTextDirection () {
let tagScheme = [NSLinguisticTagScheme.language]
let tagger = NSLinguisticTagger(tagSchemes: tagScheme, options: 0)
tagger.string = self.text
let lang = tagger.tag(at: 0, scheme: NSLinguisticTagScheme.language, tokenRange: nil, sentenceRange: nil)
let langDir = NSLocale.characterDirection(forLanguage: lang?.rawValue ?? "en")
if langDir == .rightToLeft { self.textAlignment = NSTextAlignment.right }
else { self.textAlignment = NSTextAlignment.left }
}
}
usage:
nameLabel.text = "مهدی"
nameLabel.decideTextDirection()
You can use MyLinearLayout to easy support RTL and LRT.
Here is my version. It's simpler and also handles multiple languages in the source document.
The Main point is to use the dominantLanguage:
let lang = tagger.dominantLanguage
Code Snippet:
extension UILabel {
func determineTextDirection () {
guard self.text != nil else {return}
let tagger = NSLinguisticTagger(tagSchemes: [.language], options: 0)
tagger.string = self.text
let lang = tagger.dominantLanguage
let rtl = lang == "he" || lang == "ar"
self.textAlignment = rtl ? .right : .left
}
}
Usage:
titleLabel.text = "UILabel היפוך שפה עבור"
titleLabel.determineTextDirection()
Finally: Note that if the App is localized and you may rely on the phones language - the solution your after is:
"Natural Text Alignment for RTL":
i.e:
titleLabel.textAlignment = .natural
Use the NSLinguisticTagger when your app shows multiple lines with different languages. or when you allow free search in any language, regardless of the local.