The original link: www.raywenderlich.com/153591/core…





Magazines, Core Text and brains!

What’s New: This tutorial has been updated by Lyndsey Scott to Swift 4 and Xcode 9. The original tutorial was written by Marin Todorov.

Core Text is a low-level Text engine that provides fine-grained control over layout and formatting when used in conjunction with the Core Graphics/Quartz framework.

With the release of iOS 7, Apple released an advanced library called Text Kit that can be used to store, lay out, and display Text with various typographical features. While Text Kit is powerful and sufficient for laying out Text in most cases, Core Text provides more control. For example, if you want to use Quartz directly, use Core Text instead. If you need to build your own layout engine, Core Text will help you generate glyphs and arrange them according to each other, with all the features of good typography.

This tutorial will guide you through creating a very simple magazine application using Core Text… For the zombies! Well, zombie readers have kindly agreed that as long as you use Core Text carefully, this tutorial won’t eat your brain… So, you might as well start as soon as possible.

Note: To fully understand this tutorial, you first need to understand the basics of iOS development. If you are new to iOS development, you should check out the other tutorials on this site first.

start

Open Xcode and create a new Swift Universal Project with the Single View Application template named CoreTextMagazine.

Then, add the Core Text framework to your project:

  1. Click the project file in the Project navigator (on the left navigation bar)
  2. Under the “General” button, scroll to the bottom of the “Linked Frameworks and Libraries”
  3. Click the “+” button and go to “CoreText”
  4. Select “CoreText. Framework “and click the “Add” button. It’s that simple!

Now that the project is configured, it’s time to start writing code.

Add a Core Text View

First, you’ll create a custom UIView that will use Core Text in the draw(_:) method of the UIView.

Create a new Cocoa Touch Class file that extends from UIView and call it CTView. Open ctView.swift and add the following code to the import UIKit statement:

import CoreTextCopy the code

Then, set this custom view as the main view of the application. Open main.storyboard, open the Utilities menu on the right, and click the Identity Inspector icon on the top toolbar. In the left menu of Interface Builder, select View. The Class field in the Utilities menu should now say UIView. Type CTView in the Class field to subclass the view of the main view controller, then hit Enter.





Next, open ctView.swift and replace all the commented draw(_:) methods with the following code:

//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) }Copy the code

Let’s examine the code step by step:

  1. When the view is created, draw(_:) runs automatically and renders the background layer of the view.
  2. Opens the current graphics context for drawing.
  3. Create a path that limits the drawing area, in this case the bounds of the entire view.
  4. In Core Text, you use NSAttributedString instead of String or NSString to hold Text and attributes. Initialize a “Hello World” property string.
  5. CTFramesetterCreateWithAttributedString attributes using the supplied string to create a CTFramesetter. CTFramesetter manages the fonts and drawing areas you reference.
  6. You can create a CTFrame by having CTFramesetterCreateFrame render the entire string in the path.
  7. CTFrameDraw draws the CTFrame in the given context.

    That’s all you need to do to draw simple text! Build, run and view the results.





Oh no… It doesn’t seem right. Like many of the underlying apis, Core Text uses the Y-FLIPPED coordinate system. To make matters worse, the content flips vertically too!

Add the following code to the Guard let context statement to correct the direction of the content:

// Flip the coordinate system context.textMatrix = .identity context.translateBy(x: 0, y: Size. Height) context.scaleby (x: 1.0, y: -1.0)Copy the code

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 solve this problem with constraints.





Congratulations on your first Core Text software! Zombies are happy to see your progress.

Core Text object model

If you’re a little confused about CTFramesetter and CTFrame, it’s time to explain them. :] Core Text object model looks like this:





When you provide an NSAttributedString to create an instance of the CTFramesetter object, an instance of CTTypesetter is automatically created for you to manage your fonts. You will then use this CTFramesetter to create one or more frames when rendering text.

When you create a frame, you can render the text with a subrange that provides the text for the frame. Core Text automatically creates a CTLine for each line of Text and a CTRun for each character with the same format. For example, Core Text will only create a CTRun for a few red words on the same line, a CTRun for the following plain Text, a CTRun for bold paragraphs, and so on. Core Text creation creates a CTRun based on the attributes in the NSAttributedString that you provide. In addition, each of the CTRun objects mentioned above can take on different attributes, which means that you have good control over the spacing, hyphen, width, height, and so on.

Go deep into the magazine App!

Download and unzip the Zombie Magazine Materials. Drag and drop the extracted folder into your Xcode project. When the dialog box pops up, make sure Copy Items if needed and Create Groups are checked.

To create this app, you need to apply various properties to the text. You will create a simple text tag parser that formats magazines with tags.

Create a new Cocoa Touch Class file, named MarkupParser, inherited from NSObject.

First, let’s take a quick look at zombies. TXT. See how it includes the formatting tag in parentheses throughout the text. The img SRC tag points to the magazine’s image, while the “font color/face” tag determines the color and font of the text.

Open markupParser. swift and replace its contents with the following code:

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) {

  }
}Copy the code

In this code you add properties to hold the font and text colors, setting their initial values. A variable is created to hold the property string generated by parseMarkup(_:). An array is also created to hold key-value pairs that define the size, location, and file name of the image parsed from the text.

Normally, writing a parser is not an easy task, but the parser implemented in this tutorial will be very simple and will only support open labels, which means that a label will determine the style of the text that follows it until a new label is found. This text is marked as follows:

These are <font color="red">red<font color="black"> and
<font color="blue">blue <font color="black">words.Copy the code

The output is as follows:





Add the following to the parseMarkup(_:) method:

//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 _ { }Copy the code
  1. AttrString is initially empty, but eventually contains parsed tokens.
  2. This regular expression matches the text blocks immediately following these labels. It’s like saying, “Look at the string until you find an opening parenthesis, and then look at the string until you find an closing parenthesis (or the end of the document).”
  3. Search the entire range of tags that regex matches, and generate an array of NSTextCheckingResults.

To learn more about regular expressions, visit the NSRegularExpression Tutorial.

Now that you have parsed all the text and put all the formatted labels into the chunks, all you need to do is traverse the Chunks array to generate the corresponding property string.

But before that, did you notice how the matches(in: Options :range:) method accepts an NSRange as an argument? There’s a lot of NSRange to Range transformations when you apply NSRegularExpression to your tag String. Swift has become a good helper to all of us, so she deserves help.

Again 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 } }Copy the code

This function converts the start and end Index of the String represented by NSRange to string.utf16view. Index, the set of positions in the UTF-16 String. Utf16view. Index format is then converted to String.index format. When combined, the string. Index format generates the Swift Range format: Range. As long as the index is valid, this function returns the Range format corresponding to the original NSRange format.

Now it’s time to go back to the text and label arrays.





In the parseMarkup(_:) function, add the following code to let chunks (in the do loop 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.substring(with: 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)
}Copy the code
  1. Loop chunks array.
  2. Get the range of the current NSTextCheckingResult, expand range < string.index > and continue executing the following blocks as long as range exists.
  3. Chunk is divided into parts with “<“. The first section contains the text of the magazine and the second section contains the corresponding tag (if any).
  4. The font was generated with fontName, now the default font is “Arial”, and the font size was created based on the device screen. If fontName does not produce a valid UIFont, set the default font to the current font.
  5. Create a dictionary of font formats, apply it to parts[0] to create an attribute string, and then append that string to the result string.

Insert the following code to handle the “font” tag under 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.substring(with: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 = tag.substring(with: range) } } } //end of font parsingCopy the code
  1. If the parts array has less than two elements, the loop block is skipped. Otherwise, save the second part of parts as a tag.
  2. If a tag starts with “font”, a regular expression is created to match the “color” value of the font, and the regular expression is then used to enumerate the “color” value of the matched tag. In this case, there should be only one matching color value.
  3. If enumerateMatches(in:options:range:using:) returns a valid match and a valid range for the tag, search for the indicator (e.g. returns “red”). And then use that color value to generate a UI selector. The color returned by performing this selector (if it exists) is assigned to your class’s color property; if the returned color does not exist, the color property is assigned to black.
  4. Similarly, create a regular expression to handle the “face” value of the font in the text. If a “face” value is matched, the fontName property of the class is set to the matching “face” value.

Well done! Now the parseMarkup(_:) function can get the markup in the text and generate a corresponding NSAttributedString.

It’s also time to feed your app to some zombies! I mean, feed some zombies to your app… In other words, zombies. TXT.

In fact, it’s UIView’s job to display the content it’s given, not to load the content. Open ctView.swift and add the following code before the draw(_:) method:

// MARK: - Properties
var attrString: NSAttributedString!

// MARK: - Internal
func importAttrString(_ attrString: NSAttributedString) {
  self.attrString = attrString
}Copy the code

Next, remove let attrString = NSAttributedString(string: “Hello World”) from draw(_:).

In this code you create an instance variable that holds the property string and a function so that the rest of your app can set the property string.

Then, open viewController.swift and add the following code 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 _ { }Copy the code

Then walk through the code step by step:

  1. Load the text from the zombie.txt file.
  2. Create a new parser, pass in text as an argument, and assign the returned property string to the CTView of the ViewController.

Build and run this app!





It’s fantastic! Thanks to these 50 + lines of parsing code, you can easily hold the contents of your magazine app in a text file.

Basic magazine layout

If you think the monthly magazine of zombie news can only be crammed into one pathetic page, think again! Fortunately, the Core of the Text in the Text column layout quite useful, because CTFrameGetVisibleStringRange can tell the situation of a given frame shows how much Text is appropriate. That is, you can create a column of text, and when the column is filled with text, you can know and start a new column.

In the case of this app, you need to print out columns, then assemble columns into pages, and then assemble pages into a paper. To offend the dead, so… Change your CTView to inherit from UIScrollView as soon as possible. Open ctView. swift and change the class CTView line to the following code:

class CTView: UIScrollView {Copy the code

Do you see that, Zombie master? Now the app has support for immortality! That’s right, lines, scrolling, and paging are now available.





So far, you have created framesetter and frame in the draw(_:) method, but since you have many text columns in different formats, it is best to create a separate instance representing the text column in question.

Create a new Cocoa Touch Class file named CTColumnView, inherited from UIView. Open ctColumnView.swift and add the following initial 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)
  }
}Copy the code

This code generates a CTFrame just as it did in the original CTView. The custom initialization function init(frame: ctFrame 🙂 sets:

  1. The frame of this view.
  2. The CTFrame drawn in the current context.
  3. And set the background color of the view to white.

Next, create a new swift file called CTSettings.swift to hold the Settings for your text column. Replace the contents of ctSettings. swift with the following code:

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)
  }
}Copy the code
  1. These properties are used to determine the page constraint (the default constraint for this tutorial is 20), the number of columns on each page, the frame for each page containing text columns, and the frame for each text column on each page.
  2. Since the magazine is aimed at zombies with iphones and ipads, two columns on the iPad and one column on the iPhone, the number of columns is appropriate for any screen size.
  3. Use the size of the constraint calculated pageRect to layout the page boundaries.
  4. Divide the width of pageRect with the number of text columns per page and work with constraints to calculate the columnRect.

Open ctView.swift and replace the entire contents of the file with the following code:

import UIKit
import CoreText

class CTView: UIScrollView {

  //1
  func buildFrames(withAttrString attrString: NSAttributedString,
                   andImages images: [[String: Any]]) {
    //2
    isPagingEnabled = true
    //3
    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 {
    }
  }
}Copy the code
  1. BuildFrames (withAttrString: andImages:) function will create and add CTColumnView to scroll view.
  2. Run the page turning behavior of scroll view; That is, whenever the user stops scrolling, the scroll view can get stuck so that only one full page is displayed at a time.
  3. CTFramesetter framesetter provides a property string for each column of CTFrame that you create.
  4. UIView pageView is going to be the container for the subview corresponding to the text column of each page; TextPos keeps track of the next text; ColumnIndex keeps track of the current column; PageIndex keeps track of the current page; Settings also gives you access to the app’s constraint size, columns per page, page frame, and column frame Settings.
  5. You’ll iterate through the attrString and then lay out the text column by column until the current text position is at the end.

It’s time to start traversing the attrString. Add the following code to 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)Copy the code
  1. If the column index by the number of columns per page equals 0, which indicates that this is the first column of the page, a new page view is created to hold these columns. To set the frame for these columns, you need to get the computed constraint setting. pageRect to calculate the offset from the origin by multiplying the current page index by the screen width. This ensures that each page of the magazine is to the right of the previous page within the page scrolling view.
  2. Since the added pageIndex.
  3. In settings.columnsPerPage, divide the width of pageView by the x origin of the first column and multiply the column by the column index to obtain the column offset; Then create a frame for the current column by taking the standard columnRect and offsetting its X origin by columnOffset.

Next add the following code to the columnFrame initialization method:

//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 += 1Copy the code
  1. Create a CGMutablePath sized column and then render enough text from textPos in the appropriate range into CTFrame.
  2. Create a CTColumnView with columnFrame of type CGRect and CTFrame of type CTFrame and add this column to pageView.
  3. With CTFrameGetVisibleStringRange (_) function calculation with column to limit the scope of the text, and then use the calculated values range from increasing textPos.
  4. Incrementing column’s index before traversing to the next column.

Finally, set the size of the scroll view after traversal is complete:

contentSize = CGSize(width: CGFloat(pageIndex) * bounds.size.width,
                     height: bounds.size.height)Copy the code

By setting the content size to screen width times the number of pages, Mr. Zombie can now scroll the magazine to the end.

Open viewController.swift and apply the following code:

(view as? CTView)? .importAttrString(parser.attrString)Copy the code

Replace it with the following code:

(view as? CTView)? .buildFrames(withAttrString: parser.attrString, andImages: parser.images)Copy the code

Build and run the app on the iPad. Check the two-column layout! Try dragging from page to page. It looks great!





You’ve got the text formatted in columns, but you’ve forgotten the images. Drawing images with Core Text isn’t that easy — Core Text is, after all, a Text processing framework — but adding images isn’t that bad with the help of the tag parser you just created.

Draw an image with Core Text

While Core Text can’t draw images directly, as a layout engine, it can leave space for images. By setting CTRun’s delegate, you can determine CTRun’s ascent space, decent space, and width. Like this:





When Core Text encounters a CTRun with a CTRunDelegate set, it asks the delegate: “How much space do I need for this data?” By setting these properties in a CTRunDelegate, you can leave the image empty in the text.

First let the parser support the “img” tag. Open markupparser. swift and find the “} //end of font parsing” statement. Add the following code to the end:

//1 else if tag.hasPrefix("img") { var filename:String = "" let imageRegex = try NSRegularExpression(pattern: "(? <=src=\")[^\"]+", options: NSRegularExpression.Options(rawValue: 0)) imageRegex.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) { filename = tag.substring(with: range) } } //2 let settings = CTSettings() var width: CGFloat = settings.columnRect.width var height: CGFloat = 0 if let image = UIImage(named: filename) { height = width * (image.size.height / image.size.width) // 3 if height > settings.columnRect.height - font.lineHeight { height = settings.columnRect.height - font.lineHeight width = height * (image.size.width / image.size.height) } } }Copy the code
  1. Use the re to find the image’s “SRC” value if the tag starts with “img”, such as filename for the image.
  2. Set the image width to the width of the column and set the image height while maintaining the image aspect ratio.
  3. If the image height is higher than the column height set the column height to the image height and reduce the image width to maintain the image aspect ratio.

Next, add the following code immediately after the if let image block:

//1
images += [["width": NSNumber(value: Float(width)),
            "height": NSNumber(value: Float(height)),
            "filename": filename,
            "location": NSNumber(value: attrString.length)]]
//2
struct RunStruct {
  let ascent: CGFloat
  let descent: CGFloat
  let width: CGFloat
}

let extentBuffer = UnsafeMutablePointer<RunStruct>.allocate(capacity: 1)
extentBuffer.initialize(to: RunStruct(ascent: height, descent: 0, width: width))
//3
var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { (pointer) in
}, getAscent: { (pointer) -> CGFloat in
  let d = pointer.assumingMemoryBound(to: RunStruct.self)
  return d.pointee.ascent
}, getDescent: { (pointer) -> CGFloat in
  let d = pointer.assumingMemoryBound(to: RunStruct.self)
  return d.pointee.descent
}, getWidth: { (pointer) -> CGFloat in
  let d = pointer.assumingMemoryBound(to: RunStruct.self)
  return d.pointee.width
})
//4
let delegate = CTRunDelegateCreate(&callbacks, extentBuffer)
//5
let attrDictionaryDelegate = [(kCTRunDelegateAttributeName as NSAttributedStringKey): (delegate as Any)]              
attrString.append(NSAttributedString(string: " ", attributes: attrDictionaryDelegate))Copy the code
  1. Adds a dictionary containing the image size, filename, and text position to the images array.
  2. Define the RunStruct structure diagram to hold properties that describe whitespace. It then initializes a pointer containing RunStruct whose ascent equals the height of the image and whose width equals the width of the image.
  3. Create a CTRunDelegateCallbacks that returns ascent, DECENT, and width.
  4. Create a delegate instance with CTRunDelegateCreate that binds callbacks to data.
  5. Create an attribute dictionary that contains the delegate instance, and then add a single space to the end of the attrString. The attribute dictionary is used to hold the location and size of these placeholder Spaces.

Now MarkupParser can handle “img” tags, you need to adjust CTColumnView and CTView to render images.

Open the CTColumnView. Swift. Add the following code to var ctFrame: ctFrame! After the statement:

var images: [(image: UIImage, frame: CGRect)] = []Copy the code

Then add the following code to the draw(_:) function:

for imageData in images {
  if let image = imageData.image.cgImage {
    let imgBounds = imageData.frame
    context.draw(image, in: imgBounds)
  }
}Copy the code

In this code you walk through each image and draw the image to its proper frame in context.

Then open ctView.swift and add the following property to the top of the class:

// MARK: - Properties
var imageIndex: Int!Copy the code

ImageIndex keeps track of the current imageIndex as you draw the CTColumnView.

Next, add a line of code below to buildFrames (withAttrString: above andImages:) function:

imageIndex = 0Copy the code

This marks the first element of the images array.

Then add the following attachImagesWithFrame (_ : ctframe: margin: columnView) function to buildFrames (withAttrString: andImages behind:) function:

func attachImagesWithFrame(_ images: [[String: Any]], ctframe: CTFrame, margin: CGFloat, columnView: CTColumnView) { //1 let lines = CTFrameGetLines(ctframe) as NSArray //2 var origins = [CGPoint](repeating: .zero, count: lines.count) CTFrameGetLineOrigins(ctframe, CFRangeMake(0, 0), &origins) //3 var nextImage = images[imageIndex] guard var imgLocation = nextImage["location"] as? Int else { return } //4 for lineIndex in 0.. <lines.count { let line = lines[lineIndex] as! CTLine //5 if let glyphRuns = CTLineGetGlyphRuns(line) as? [CTRun], let imageFilename = nextImage["filename"] as? String, let img = UIImage(named: imageFilename) { for run in glyphRuns { } } } }Copy the code
  1. Gets an array of CTLine objects for ctFrame.
  2. Copy the row origin coordinates of ctFrame into the origins array with CTFrameGetOrigins. By setting the range of length to 0, the CTFrameGetOrigins will know to traverse the entire CTFrame.
  3. Set nextImage to contain property data for the current image. If nextImage contains the location of the image, expand it and continue; Otherwise, return the function earlier.
  4. Iterate over the next line of text.
  5. The glyph of the line is traversed if the glyph, file name, and image file name are all present.

Next, add the following code to the for-loop block of the glyph:

// 1 let runRange = CTRunGetStringRange(run) if runRange.location > imgLocation || runRange.location + runRange.length <= imgLocation { continue } //2 var imgBounds: CGRect = .zero var ascent: CGFloat = 0 imgBounds.size.width = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, nil, nil)) imgBounds.size.height = ascent //3 let xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, nil) imgBounds.origin.x = origins[lineIndex].x + xOffset imgBounds.origin.y = origins[lineIndex].y //4 columnView.images  += [(image: img, frame: imgBounds)] //5 imageIndex! += 1 if imageIndex < images.count { nextImage = images[imageIndex] imgLocation = (nextImage["location"] as AnyObject).intValue }Copy the code
  1. If the range of the current glyph does not contain the next image, the rest of the loop is skipped. Otherwise, render the image here.
  2. Use CTRunGetTypographicBounds calculation image width and height is set to ascent.
  3. Use CTLineGetOffsetForStringIndex for line x offset, and then add it to the starting point of imgBounds coordinates.
  4. Adds the image and its frame to the current CTColumnView.
  5. Add an image index. If images[imageIndex] is an image, the nextImage and imgLocation are updated so that they reference the nextImage.





All right! Well done! Almost done. Just one more step.

In buildFrames (withAttrString: andImages:) internal pageView. AddSubview (column) to add the following code above attached image (if the image exists) :

if images.count > imageIndex {
  attachImagesWithFrame(images, ctframe: ctframe, margin: settings.margin, columnView: column)
}Copy the code

Build and run on iPhone and iPad:





Congratulations to you! Thanks for all your hard work, Mr. Zombie has decided not to eat your brain!

Where to go

See the full project here.

As mentioned in the introduction, Text Kit can often replace Core Text; So try writing the same tutorial in Text Kit and see what the difference is. This Core Text course will not be in vain! Text Kit provides toll free bridging to Core Text, so you can easily convert between frames based on your needs.

Any questions, comments or suggestions? Join the discussion forum!