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
A Basic Magazine Layout
If you thought a monthly magazine of Zombie news could possibly fit onto one measly page, you'd be very wrong! Luckily Core Text becomes particularly useful when laying out columns since CTFrameGetVisibleStringRange
can tell you how much text will fit into a given frame. Meaning, you can create a column, then once its full, you can create another column, etc.
For this app, you'll have to print columns, then pages, then a whole magazine lest you offend the undead, so... time to turn your CTView
subclass into a UIScrollView
.
Open CTView.swift and change the class CTView
line to:
class CTView: UIScrollView {
See that, zombies? The app can now support an eternity of undead adventures! Yep -- with one line, scrolling and paging is now available.
Up until now, you've created your framesetter and frame inside draw(_:)
, but since you'll have many columns with different formatting, it's better to create individual column instances instead.
Create a new Cocoa Touch Class file named CTColumnView
subclassing UIView
.
Open CTColumnView.swift and add the following starter code:
import UIKit
import CoreText
class CTColumnView: UIView {
// MARK: - Properties
var ctFrame: CTFrame!
// MARK: - Initializers
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
required init(frame: CGRect, ctframe: CTFrame) {
super.init(frame: frame)
self.ctFrame = ctframe
backgroundColor = .white
}
// MARK: - Life Cycle
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.textMatrix = .identity
context.translateBy(x: 0, y: bounds.size.height)
context.scaleBy(x: 1.0, y: -1.0)
CTFrameDraw(ctFrame, context)
}
}
This code renders a CTFrame
just as you'd originally done in CTView
. The custom initializer, init(frame:ctframe:)
, sets:
- The view's frame.
- The
CTFrame
to draw into the context. - And the view's backgound color to white.
Next, create a new swift file named CTSettings.swift which will hold your column settings.
Replace the contents of CTSettings.swift with the following:
import UIKit
import Foundation
class CTSettings {
//1
// MARK: - Properties
let margin: CGFloat = 20
var columnsPerPage: CGFloat!
var pageRect: CGRect!
var columnRect: CGRect!
// MARK: - Initializers
init() {
//2
columnsPerPage = UIDevice.current.userInterfaceIdiom == .phone ? 1 : 2
//3
pageRect = UIScreen.main.bounds.insetBy(dx: margin, dy: margin)
//4
columnRect = CGRect(x: 0,
y: 0,
width: pageRect.width / columnsPerPage,
height: pageRect.height).insetBy(dx: margin, dy: margin)
}
}
- The properties will determine the page margin (default of 20 for this tutorial); the number of columns per page; the frame of each page containing the columns; and the frame size of each column per page.
- Since this magazine serves both iPhone and iPad carrying zombies, show two columns on iPad and one column on iPhone so the number of columns is appropriate for each screen size.
- Inset the entire bounds of the page by the size of the margin to calculate
pageRect
. - Divide
pageRect
's width by the number of columns per page and inset that new frame with the margin forcolumnRect
.
Open, CTView.swift, replace the entire contents with the following:
import UIKit
import CoreText
class CTView: UIScrollView {
//1
func buildFrames(withAttrString attrString: NSAttributedString,
andImages images: [[String: Any]]) {
//3
isPagingEnabled = true
//4
let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)
//4
var pageView = UIView()
var textPos = 0
var columnIndex: CGFloat = 0
var pageIndex: CGFloat = 0
let settings = CTSettings()
//5
while textPos < attrString.length {
}
}
}
-
buildFrames(withAttrString:andImages:)
will createCTColumnView
s then add them to the scrollview. - Enable the scrollview's paging behavior; so, whenever the user stops scrolling, the scrollview snaps into place so exactly one entire page is showing at a time.
-
CTFramesetter
framesetter
will create each column'sCTFrame
of attributed text. -
UIView
pageView
s will serve as a container for each page's column subviews;textPos
will keep track of the next character;columnIndex
will keep track of the current column;pageIndex
will keep track of the current page; andsettings
gives you access to the app's margin size, columns per page, page frame and column frame settings. - You're going to loop through
attrString
and lay out the text column by column, until the current text position reaches the end.
Time to start looping attrString
. Add the following within while textPos < attrString.length {
.:
//1
if columnIndex.truncatingRemainder(dividingBy: settings.columnsPerPage) == 0 {
columnIndex = 0
pageView = UIView(frame: settings.pageRect.offsetBy(dx: pageIndex * bounds.width, dy: 0))
addSubview(pageView)
//2
pageIndex += 1
}
//3
let columnXOrigin = pageView.frame.size.width / settings.columnsPerPage
let columnOffset = columnIndex * columnXOrigin
let columnFrame = settings.columnRect.offsetBy(dx: columnOffset, dy: 0)
- If the column index divided by the number of columns per page equals 0, thus indicating the column is the first on its page, create a new page view to hold the columns. To set its frame, take the margined
settings.pageRect
and offset its x origin by the current page index multiplied by the width of the screen; so within the paging scrollview, each magazine page will be to the right of the previous one. - Increment the
pageIndex
. - Divide
pageView
's width bysettings.columnsPerPage
to get the first column's x origin; multiply that origin by the column index to get the column offset; then create the frame of the current column by taking the standardcolumnRect
and offsetting its x origin bycolumnOffset
.
Next, add the following below columnFrame
initialization:
//1
let path = CGMutablePath()
path.addRect(CGRect(origin: .zero, size: columnFrame.size))
let ctframe = CTFramesetterCreateFrame(framesetter, CFRangeMake(textPos, 0), path, nil)
//2
let column = CTColumnView(frame: columnFrame, ctframe: ctframe)
pageView.addSubview(column)
//3
let frameRange = CTFrameGetVisibleStringRange(ctframe)
textPos += frameRange.length
//4
columnIndex += 1
- Create a
CGMutablePath
the size of the column, then starting fromtextPos
, render a newCTFrame
with as much text as can fit. - Create a
CTColumnView
with aCGRect
columnFrame
andCTFrame
ctframe
then add the column topageView
. - Use
CTFrameGetVisibleStringRange(_:)
to calculate the range of text contained within the column, then incrementtextPos
by that range length to reflect the current text position. - Increment the column index by 1 before looping to the next column.
Lastly set the scroll view's content size after the loop:
contentSize = CGSize(width: CGFloat(pageIndex) * bounds.size.width,
height: bounds.size.height)
By setting the content size to the screen width times the number of pages, the zombies can now scroll through to the end.
Open ViewController.swift, and replace
(view as? CTView)?.importAttrString(parser.attrString)
with the following:
(view as? CTView)?.buildFrames(withAttrString: parser.attrString, andImages: parser.images)
Build and run the app on an iPad. Check that double column layout! Drag right and left to go between pages. Lookin' good. :]
You've columns and formatted text, but you're missing images. Drawing images with Core Text isn't so straightforward - it's a text framework after all - but with the help of the markup parser you've already created, adding images shouldn't be too bad.