Core Text Tutorial for iOS: Making a Magazine App
Learn how to make your own magazine app with custom text layout in this Core Text tutorial for iOS. By Lyndsey Scott.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Core Text Tutorial for iOS: Making a Magazine App
35 mins
Update note: This tutorial has been updated to Swift 4 and Xcode 9 by Lyndsey Scott. The original tutorial was written by Marin Todorov.
Core Text is a low-level text engine that when used alongside the Core Graphics/Quartz framework, gives you fine-grained control over layout and formatting.
With iOS 7, Apple released a high-level library called Text Kit, which stores, lays out and displays text with various typesetting characteristics. Although Text Kit is powerful and usually sufficient when laying out text, Core Text can provide more control. For example, if you need to work directly with Quartz, use Core Text. If you need to build your own layout engines, Core Text will help you generate “glyphs and position them relative to each other with all the features of fine typesetting.”
This tutorial takes you through the process of creating a very simple magazine application using Core Text… for Zombies!
Oh, and Zombie Monthly’s readership has kindly agreed not to eat your brains as long as you’re busy using them for this tutorial… So you may want to get started soon! *gulp*
Note: To get the most out of this tutorial, you need to know the basics of iOS development first. If you’re new to iOS development, you should check out some of the other tutorials on this site first.
Note: To get the most out of this tutorial, you need to know the basics of iOS development first. If you’re new to iOS development, you should check out some of the other tutorials on this site first.
Getting Started
Open Xcode, create a new Swift universal project with the Single View Application Template and name it CoreTextMagazine.
Next, add the Core Text framework to your project:
- Click the project file in the Project navigator (the strip on the left hand side)
- Under “General”, scroll down to “Linked Frameworks and Libraries” at the bottom
- Click the “+” and search for “CoreText”
- Select “CoreText.framework” and click the “Add” button. That’s it!
Now the project is setup, it’s time to start coding.
Adding a Core Text View
For starters, you’ll create a custom UIView, which will use Core Text in its draw(_:) method.
Create a new Cocoa Touch Class file named CTView subclassing UIView .
Open CTView.swift, and add the following under import UIKit:
import CoreText
Next, set this new custom view as the main view in the application. Open Main.storyboard, open the Utilities menu on the right-hand side, then select the Identity Inspector icon in its top toolbar. In the left-hand menu of the Interface Builder, select View. The Class field of the Utilities menu should now say UIView. To subclass the main view controller’s view, type CTView into the Class field and hit Enter.
Next, open CTView.swift and replace the commented out draw(_:) with the following:
//1
override func draw(_ rect: CGRect) {
// 2
guard let context = UIGraphicsGetCurrentContext() else { return }
// 3
let path = CGMutablePath()
path.addRect(bounds)
// 4
let attrString = NSAttributedString(string: "Hello World")
// 5
let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)
// 6
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrString.length), path, nil)
// 7
CTFrameDraw(frame, context)
}
Let’s go over this step-by-step.
- Upon view creation,
draw(_:)will run automatically to render the view’s backing layer. - Unwrap the current graphic context you’ll use for drawing.
- Create a path which bounds the drawing area, the entire view’s bounds in this case
- In Core Text, you use
NSAttributedString, as opposed toStringorNSString, to hold the text and its attributes. Initialize “Hello World” as an attributed string. -
CTFramesetterCreateWithAttributedStringcreates aCTFramesetterwith the supplied attributed string.CTFramesetterwill manage your font references and your drawing frames. - Create a
CTFrame, by havingCTFramesetterCreateFramerender the entire string withinpath. -
CTFrameDrawdraws theCTFramein the given context.
That’s all you need to draw some simple text! Build, run and see the result.
Uh-oh… That doesn’t seem right, does it? Like many of the low level APIs, Core Text uses a Y-flipped coordinate system. To make matters worse, the content is also flipped vertically!
Add the following code directly below the guard let context statement to fix the content orientation:
// Flip the coordinate system
context.textMatrix = .identity
context.translateBy(x: 0, y: bounds.size.height)
context.scaleBy(x: 1.0, y: -1.0)
This code flips the content by applying a transformation to the view’s context.
Build and run the app. Don’t worry about status bar overlap, you’ll learn how to fix this with margins later.
Congrats on your first Core Text app! The zombies are pleased with your progress.
The Core Text Object Model
If you’re a bit confused about the CTFramesetter and the CTFrame – that’s OK because it’s time for some clarification. :]
Here’s what the Core Text object model looks like:

When you create a CTFramesetter reference and provide it with an NSAttributedString, an instance of CTTypesetter is automatically created for you to manage your fonts. Next you use the CTFramesetter to create one or more frames in which you’ll be rendering text.
When you create a frame, you provide it with the subrange of text to render inside its rectangle. Core Text automatically creates a CTLine for each line of text and a CTRun for each piece of text with the same formatting. For example, Core Text would create a CTRun if you had several words in a row colored red, then another CTRun for the following plain text, then another CTRun for a bold sentence, etc. Core Text creates CTRuns for you based on the attributes of the supplied NSAttributedString. Furthermore, each of these CTRun objects can adopt different attributes, so you have fine control over kerning, ligatures, width, height and more.
Onto the Magazine App!
Download and unarchive the zombie magazine materials.
Drag the folder into your Xcode project. When prompted make sure Copy items if needed and Create groups are selected.
To create the app, you’ll need to apply various attributes to the text. You’ll create a simple text markup parser which will use tags to set the magazine’s formatting.
Create a new Cocoa Touch Class file named MarkupParser subclassing NSObject.
First things first, take a quick look at zombies.txt. See how it contains bracketed formatting tags throughout the text? The “img src” tags reference magazine images and the “font color/face” tags determine text color and font.
Open MarkupParser.swift and replace its contents with the following:
import UIKit
import CoreText
class MarkupParser: NSObject {
// MARK: - Properties
var color: UIColor = .black
var fontName: String = "Arial"
var attrString: NSMutableAttributedString!
var images: [[String: Any]] = []
// MARK: - Initializers
override init() {
super.init()
}
// MARK: - Internal
func parseMarkup(_ markup: String) {
}
}
Here you’ve added properties to hold the font and text color; set their defaults; created a variable to hold the attributed string produced by parseMarkup(_:); and created an array which will eventually hold the dictionary information defining the size, location and filename of images found within the text.
Writing a parser is usually hard work, but this tutorial’s parser will be very simple and support only opening tags — meaning a tag will set the style of the text following it until a new tag is found. The text markup will look like this:
These are <font color="red">red<font color="black"> and <font color="blue">blue <font color="black">words.
and produce output like this:
These are red and blue words.
Lets’ get parsin’!
Add the following to parseMarkup(_:):
//1
attrString = NSMutableAttributedString(string: "")
//2
do {
let regex = try NSRegularExpression(pattern: "(.*?)(<[^>]+>|\\Z)",
options: [.caseInsensitive,
.dotMatchesLineSeparators])
//3
let chunks = regex.matches(in: markup,
options: NSRegularExpression.MatchingOptions(rawValue: 0),
range: NSRange(location: 0,
length: markup.characters.count))
} catch _ {
}
-
attrStringstarts out empty, but will eventually contain the parsed markup. - This regular expression, matches blocks of text with the tags immediately follow them. It says, “Look through the string until you find an opening bracket, then look through the string until you hit a closing bracket (or the end of the document).”
- Search the entire range of the markup for
regexmatches, then produce an array of the resultingNSTextCheckingResults.
Note: To learn more about regular expressions, check out NSRegularExpression Tutorial.
Note: To learn more about regular expressions, check out NSRegularExpression Tutorial.
Now you’ve parsed all the text and formatting tags into chunks, you’ll loop through chunks to build the attributed string.
But before that, did you notice how matches(in:options:range:) accepts an NSRange as an argument? There’s going to be lots of NSRange to Range conversions as you apply NSRegularExpression functions to your markup String. Swift’s been a pretty good friend to us all, so it deserves a helping hand.
Still in MarkupParser.swift, add the following extension to the end of the file:
// MARK: - String
extension String {
func range(from range: NSRange) -> Range<String.Index>? {
guard let from16 = utf16.index(utf16.startIndex,
offsetBy: range.location,
limitedBy: utf16.endIndex),
let to16 = utf16.index(from16, offsetBy: range.length, limitedBy: utf16.endIndex),
let from = String.Index(from16, within: self),
let to = String.Index(to16, within: self) else {
return nil
}
return from ..< to
}
}
This function converts the String's starting and ending indices as represented by an NSRange, to String.UTF16View.Index format, i.e. the positions in a string’s collection of UTF-16 code units; then converts each String.UTF16View.Index to String.Index format; which when combined, produces Swift's range format: Range. As long as the indices are valid, the method will return the Range representation of the original NSRange.
Your Swift is now chill. Time to head back to processing the text and tag chunks.
Inside parseMarkup(_:) add the following below let chunks (within the do block):
let defaultFont: UIFont = .systemFont(ofSize: UIScreen.main.bounds.size.height / 40)
//1
for chunk in chunks {
//2
guard let markupRange = markup.range(from: chunk.range) else { continue }
//3
let parts = markup[markupRange].components(separatedBy: "<")
//4
let font = UIFont(name: fontName, size: UIScreen.main.bounds.size.height / 40) ?? defaultFont
//5
let attrs = [NSAttributedStringKey.foregroundColor: color, NSAttributedStringKey.font: font] as [NSAttributedStringKey : Any]
let text = NSMutableAttributedString(string: parts[0], attributes: attrs)
attrString.append(text)
}
- Loop through
chunks. - Get the current
NSTextCheckingResult's range, unwrap theRange<String.Index>and proceed with the block as long as it exists. - Break
chunkinto parts separated by "<". The first part contains the magazine text and the second part contains the tag (if it exists). - Create a font using
fontName, currently "Arial" by default, and a size relative to the device screen. IffontNamedoesn't produce a validUIFont, setfontto the default font. - Create a dictionary of the font format, apply it to
parts[0]to create the attributed string, then append that string to the result string.
To process the "font" tag, insert the following after attrString.append(text):
// 1
if parts.count <= 1 {
continue
}
let tag = parts[1]
//2
if tag.hasPrefix("font") {
let colorRegex = try NSRegularExpression(pattern: "(?<=color=\")\\w+",
options: NSRegularExpression.Options(rawValue: 0))
colorRegex.enumerateMatches(in: tag,
options: NSRegularExpression.MatchingOptions(rawValue: 0),
range: NSMakeRange(0, tag.characters.count)) { (match, _, _) in
//3
if let match = match,
let range = tag.range(from: match.range) {
let colorSel = NSSelectorFromString(tag[range]+"Color")
color = UIColor.perform(colorSel).takeRetainedValue() as? UIColor ?? .black
}
}
//5
let faceRegex = try NSRegularExpression(pattern: "(?<=face=\")[^\"]+",
options: NSRegularExpression.Options(rawValue: 0))
faceRegex.enumerateMatches(in: tag,
options: NSRegularExpression.MatchingOptions(rawValue: 0),
range: NSMakeRange(0, tag.characters.count)) { (match, _, _) in
if let match = match,
let range = tag.range(from: match.range) {
fontName = String(tag[range])
}
}
} //end of font parsing
- If less than two parts, skip the rest of the loop body. Otherwise, store that second part as
tag. - If
tagstarts with "font", create a regex to find the font's "color" value, then use that regex to enumerate throughtag's matching "color" values. In this case, there should be only one matching color value. - If
enumerateMatches(in:options:range:using:)returns a validmatchwith a valid range intag, find the indicated value (ex.<font color="red">returns "red") and append "Color" to form aUIColorselector. Perform that selector then set your class'scolorto the returned color if it exists, to black if not. - Similarly, create a regex to process the font's "face" value. If it finds a match, set
fontNameto that string.
Great job! Now parseMarkup(_:) can take markup and produce an NSAttributedString for Core Text.
It's time to feed your app to some zombies! I mean, feed some zombies to your app... zombies.txt, that is. ;]
It's actually the job of a UIView to display content given to it, not load content. Open CTView.swift and add the following above draw(_:):
// MARK: - Properties
var attrString: NSAttributedString!
// MARK: - Internal
func importAttrString(_ attrString: NSAttributedString) {
self.attrString = attrString
}
Next, delete let attrString = NSAttributedString(string: "Hello World") from draw(_:).
Here you've created an instance variable to hold an attributed string and a method to set it from elsewhere in your app.
Next, open ViewController.swift and add the following to viewDidLoad():
// 1
guard let file = Bundle.main.path(forResource: "zombies", ofType: "txt") else { return }
do {
let text = try String(contentsOfFile: file, encoding: .utf8)
// 2
let parser = MarkupParser()
parser.parseMarkup(text)
(view as? CTView)?.importAttrString(parser.attrString)
} catch _ {
}
Let’s go over this step-by-step.
- Load the text from the
zombie.txtfile into aString. - Create a new parser, feed in the text, then pass the returned attributed string to
ViewController'sCTView.
Build and run the app!
That's awesome? Thanks to about 50 lines of parsing you can simply use a text file to hold the contents of your magazine app.



