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 toString
orNSString
, to hold the text and its attributes. Initialize “Hello World” as an attributed string. -
CTFramesetterCreateWithAttributedString
creates aCTFramesetter
with the supplied attributed string.CTFramesetter
will manage your font references and your drawing frames. - Create a
CTFrame
, by havingCTFramesetterCreateFrame
render the entire string withinpath
. -
CTFrameDraw
draws theCTFrame
in 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 CTRun
s 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 _ {
}
-
attrString
starts 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
regex
matches, then produce an array of the resultingNSTextCheckingResult
s.
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
chunk
into 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. IffontName
doesn't produce a validUIFont
, setfont
to 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
tag
starts 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 validmatch
with a valid range intag
, find the indicated value (ex.<font color="red">
returns "red") and append "Color" to form aUIColor
selector. Perform that selector then set your class'scolor
to 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
fontName
to 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.txt
file 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.