Swift: Creating styled and attributed text with NSAttributedString and NSParagraphStyle (updated)


Note: Code has been updated following changes to UIFont in Xcode (6.1).

Cut and paste this code into your viewDidLoad. Run in the iOS Simulator on an iPhone (to see full effect of paragraph styling). The comments explain what is happening.
// Define string attributes
let font = UIFont(name: "Georgia", size: 18.0) ?? UIFont.systemFontOfSize(18.0)
let textFont = [NSFontAttributeName:font]

let fontItal = UIFont(name: "Georgia-Italic", size: 18.0) ?? UIFont.systemFontOfSize(18.0)
let italFont = [NSFontAttributeName:fontItal]
        
// Create a string that will be our paragraph
let para = NSMutableAttributedString()
        
// Create locally formatted strings
let attrString1 = NSAttributedString(string: "Hello Swift! This is a tutorial looking at ", attributes:textFont)
let attrString2 = NSAttributedString(string: "attributed", attributes:italFont)
let attrString3 = NSAttributedString(string: " strings.", attributes:textFont)
        
// Add locally formatted strings to paragraph
para.appendAttributedString(attrString1)
para.appendAttributedString(attrString2)
para.appendAttributedString(attrString3)
        
// Define paragraph styling
let paraStyle = NSMutableParagraphStyle()
paraStyle.firstLineHeadIndent = 15.0
paraStyle.paragraphSpacingBefore = 10.0
        
// Apply paragraph styles to paragraph
para.addAttribute(NSParagraphStyleAttributeName, value: paraStyle, range: NSRange(location: 0,length: para.length))
        
// Create UITextView
let view = UITextView(frame: CGRect(x: 0, y: 20, width: CGRectGetWidth(self.view.frame), height: CGRectGetWidth(self.view.frame)-20))
        
// Add string to UITextView
view.attributedText = para
        
// Add UITextView to main view
self.view.addSubview(view)
        
// For a more detailed look at UITextView (not yet in Swift) see: http://sketchytech.blogspot.co.uk/2013/11/making-most-of-uitextview-in-ios-7.html?q=UITextview

UIFont Class

The reason for using
let font = UIFont(name: "Georgia", size: 18.0) ?? UIFont.systemFontOfSize(18.0)
is revealed if we look at the UIFont class as represented in the header file:
class UIFont : NSObject, NSCopying {
    
    // Returns an instance of the font associated with the text style and scaled appropriately for the user's selected content size category. See UIFontDescriptor.h for the complete list.
    @availability(iOS, introduced=7.0)
    class func preferredFontForTextStyle(style: String) -> UIFont
    
    // Returns a font using CSS name matching semantics.
    init?(name fontName: String, size fontSize: CGFloat) -> UIFont
    
    // Returns an array of font family names for all installed fonts
    class func familyNames() -> [AnyObject]
    
    // Returns an array of font names for the specified family name
    class func fontNamesForFamilyName(familyName: String) -> [AnyObject]
    
    // Some convenience methods to create system fonts
    class func systemFontOfSize(fontSize: CGFloat) -> UIFont
    class func boldSystemFontOfSize(fontSize: CGFloat) -> UIFont
    class func italicSystemFontOfSize(fontSize: CGFloat) -> UIFont
    
    // Font attributes
    var familyName: String { get }
    var fontName: String { get }
    var pointSize: CGFloat { get }
    var ascender: CGFloat { get }
    var descender: CGFloat { get }
    var capHeight: CGFloat { get }
    var xHeight: CGFloat { get }
    @availability(iOS, introduced=4.0)
    var lineHeight: CGFloat { get }
    var leading: CGFloat { get }
    
    // Create a new font that is identical to the current font except the specified size
    func fontWithSize(fontSize: CGFloat) -> UIFont
    
    // Returns a font matching the font descriptor. If fontSize is greater than 0.0, it has precedence over UIFontDescriptorSizeAttribute in fontDescriptor.
    init(descriptor: UIFontDescriptor, size pointSize: CGFloat) -> UIFont
    
    // Returns a font descriptor which describes the font.
    @availability(iOS, introduced=7.0)
    func fontDescriptor() -> UIFontDescriptor
}
we see in the following line that UIFont has an optional initialiser:
init?(name fontName: String, size fontSize: CGFloat) -> UIFont
meaning that it won't necessarily create an instance and might instead return nil. For example, if a font with the specified name does not exist. It is therefore possible to conceive of a situation where we might have a font that we'd most of all like but also have fallbacks:
let font = UIFont(name: "Garamond", size: 18.0) ?? UIFont(name: "Voyager", size: 18.0) ?? UIFont(name: "Georgia", size: 18.0) ?? UIFont.systemFontOfSize(20.0)
The final option being a guaranteed font returned by a type (or class) method.

Checking against available fonts

The alternative to using the nil coalescing operator would be to check first whether a font exists:
contains(UIFont.familyNames() as [String],"Georgia")
And we can imagine using
let fontName = "Georgia"
let font = contains(UIFont.familyNames() as [String],fontName) ? UIFont(name: fontName, size: 18.0) : UIFont.systemFontOfSize(18.0)
but not only is it wordier than simply writing
let font = UIFont(name: "Georgia", size: 18.0) ?? UIFont.systemFontOfSize(18.0)
and possibly more memory intensive, but the code (which searches the family names) returns an optional. This either means we need to force unwrap
let font:UIFont! = contains(UIFont.familyNames() as [String],fontName) ? UIFont(name: fontName, size: 18.0) : UIFont.systemFontOfSize(18.0)
or we need to test for nil before use, which potentially entangles us in a web of optionals and nil testing.

Conclusion

The nil coalescing operator approach, which at first might appear a rather slap dash way of doing things, becomes a way of slicing through the gordian knot of optionals that can materialise with fonts in particular, where it cannot be guaranteed that a system has the desired font.

So rather than feel a responsibility to search the family names, it would appear a better practice to take advantage of nil coalescing, and reserve use of the familyNames() type method for when a list of font names, for example, is presented to the user.

Extras

It is possible to add any NSObject:AnyObject pair to the attributes dictionary of an NSAttributedString meaning that for example one could add id and class values and any other kind of value too. Only the recognized keys are taken into account when drawing the attributed strings:
let NSFontAttributeName: String
let NSParagraphStyleAttributeName: String
let NSForegroundColorAttributeName: String
let NSBackgroundColorAttributeName: String
let NSLigatureAttributeName: String
let NSKernAttributeName: String
let NSStrikethroughStyleAttributeName: String
let NSUnderlineStyleAttributeName: String
let NSStrokeColorAttributeName: String
let NSStrokeWidthAttributeName: String
let NSShadowAttributeName: String
let NSTextEffectAttributeName: String
let NSAttachmentAttributeName: String
let NSLinkAttributeName: String
let NSBaselineOffsetAttributeName: String
let NSUnderlineColorAttributeName: String
let NSStrikethroughColorAttributeName: String
let NSObliquenessAttributeName: String
let NSExpansionAttributeName: String
let NSWritingDirectionAttributeName: String
let NSVerticalGlyphFormAttributeName: String
But all are stored, which could be convenient for retaining style information used in a document format when translating to and from attributed strings.



Comments

  1. i need to change color of text in uitextview...

    ReplyDelete
    Replies
    1. You need to add the NSForegroundColorAttributeName key to your string attributes dictionary and give it a value of a UIColor. Let me know if you need sample code written out.

      Delete

Post a Comment