使用core text制作ios杂志类app向导

翻译自:core text tutorial for ios: making a magazine app. 版权所有, 转载请著明出处,保留链接。

始于iOS 3.2+ 和OSX 10.5+,Core Text一个模板engine, 使我们能细粒度的控制文本的格式和展现。

它正好介于UIKit 和 Core Graphics/Quartz之间:

  • 在UIKit中,有UILabel控件,使用IB可以轻易地拖拽控件,在屏幕上加字或文本行,但不能改变每个字的颜色。
  • 在Core Graphics/Quartz中,可以做系统能做的每一件事,但是需要计算文本中每个符号的位置,然后把它画在屏幕上。
  • Core Text在两者之间。能完全控制位置,布局,颜色大小等属性,也能隐藏其他处理细节,例如换行和字体渲染等。

如果你做一个杂志或书的app,Core Text 将非常有用,它在iPad上表现非常好。

本文是iOS Core Text的Tutorial, 会利用Core Text 开发一个简单的杂志App(Zombies), 本向导会向你展示这个开发过程。

你将会学到:

  • 在屏幕上展示格式化的文本;
  • NICE文本的appearance;
  • 在文本内容中加入图片;
  • 做一个杂志app, 通过加载文本标志控制渲染文本的格式;

为了理解Core Text, 首先你需要了解基本的iOS开发知识。如果你是iOS的新手,你最后先读读本网站的其他向导

##建立一个Core Text 工程
开启Xcode, 定位到File\New\New Project, 选择iOS\Application\View-based 应用程序,然后点击下一步。把工程取名为CoreTextMagazine, 选择iPad设备,然后点击下一步,选择一个目录保存项目,然后点击创建。

下一步要做的在工程中添加Core Text框架:

  1. 在工程导航中点击工程文件
  2. 在目标列表中选择CoreTextMagazine工程
  3. 点击”Build phases”选项卡
  4. 扩展”Link Binary With Libraries”, 点击”+”按钮
  5. 选择”CoreText.framework”,并点击”Add”

core_text_tutorial_for_ios_making_a_magazine_app

以上是建立工程的全部,现在,是时候添加些代码了。

##增加一个Core Text view
为了尽快的了解Core Text,将会创建一个custom UIView, 在视图的drawRect:方法中,将会使用Core Text。

定位到File\New\New File, 选择iOS\Cocoa Touch\Objective-C 类,点击下一步,输入UIView的子类,点击下一步,命名为CTView类,点击保存。

在CTView.h中,在@interface前加入如下代码,引入Core Text 框架:

#import <CoreText/CoreText.h>

下一步,是把这个视图设置为应用程序的主视图。
在工程导航中,选择XIB文件”CoreTextMagazineViewController.xib”, 在XCode中展开实用工具条(当你点击视图顶部工具条的第三个选项卡时会出现)。从实用工具条中,点击顶部工具栏的第三个图标来选择身份选项卡。

core_text_tutorial_for_ios_making_a_magazine_app

在Interface 编辑器的空白处单击,选择window的视图,将会在实用工具条的类文本域中看淡”UIView”, 请把它改成”CTView”。
现在当你的应用程序启动时,会显示custom Core Text View, 但是先等下,让我们添加些代码画些文本上去。

打开CTView.m文件,删除预先定义的全部方法。增加如下代码,画一个”Hello world”在视图中:

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGMutablePathRef path = CGPathCreateMutable(); //1
    CGPathAddRect(path, NULL, self.bounds );

    NSAttributedString* attString = [[[NSAttributedString alloc]
        initWithString:@"Hello core text world!"] autorelease]; //2

    CTFramesetterRef framesetter =
        CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString); //3
    CTFrameRef frame =
        CTFramesetterCreateFrame(framesetter,
            CFRangeMake(0, [attString length]), path, NULL);

    CTFrameDraw(frame, context); //4

    CFRelease(frame); //5
    CFRelease(path);
    CFRelease(framesetter);
}

让我们来一点点的讨论,在代码中使用评论标记每一部分,如下。

  1. 这里首先创建一个path, 圈住想画文本的区域。在Mac上的Core Text支持不同的形状区域,例如矩形,圆形,但是现在iOS只支持矩形。在这个例子中,使用了全部的视图区域作为矩形来作画, 通过创建CGPath引用self.bounds实现。

  2. 在Core Text中,你将不会使用NSString, 而是使用NSAttributedString。NSAttributedString是一个强大的NSString子类, 它能让你应用格式属性到文本上去。此时,我们先使用格式化,只是创建了一个字符串来保存简单的文本。

  3. 在使用Core Text做画中,CTFramesetter是一个非常重要的类。它管理字体引用和文本作画的框架。现在你只需知道CTFramesetterCreateWithAttributedString创建了一个CTFramesetter, 并通过attributed 字符串初始化。在你有framesetter之后,你创建一个frame,并传递CTFramesetterCreateFrame,需要渲染的字符串范围,这里选择了全部字符串,矩形区域上将会显示文本。

  4. CTFrameDraw将把提供的frame画在上下文上.

  5. 释放使用到的对象。

注意到没,当你使用Core Text类时, 你是使用一系列的函数,例如CTFramesetterCreateWithAttributedString 和CTFramesetterCreateFrame, 而不是直接使用Objective-C对象。

你可能会问你自己“为什么我要使用C函数,原以为有了Objective-C后,就不需要使用C函数了?!”

well, 为了速度和简单,在iOS中有很多基础的类库是用C写的,别担心这,你会发现Core Text函数使用起来非常的容易。

只要记住一条,不要忘记使用CFRelease释放从函数中获取的引用。

相信与否,这就是你所全要做的,使用Core Text化简单的文本!点击Run,来看看结果吧。

core_text_tutorial_for_ios_making_a_magazine_app

这看起来不对,像许多低层的API一样,Core Text使用 Y-flipped 坐标系统。更糟的是,内容也被渲染成倒置的了。基于此,在混合使用UIKit和Core Text时,必须注意到这一点。

让我们来修复此BUG!在”CGContextRef context = UIGraphicsGetCurrentContext();”后增加如下代码:

// Flip the coordinate system
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

这是非常简单的代码,利用转换到视图内容来翻转内容,每次需要用CT绘画是,只需复制粘贴上述代码。

现在,再次运行–对比一下第一次运行的App!

core_text_tutorial_for_ios_making_a_magazine_app

##The Core Text Object Model
如果对CTFramesetter 和 CTFrame有点困惑,没关系,这里我解释下它们是怎么渲染文本内容的。

如下是Core Text的对象模型:

core_text_tutorial_for_ios_making_a_magazine_app

这里创建了一个CTFramesetter的引用,并提供其NSAttributedString。基于此,CTTypesetter的实例是自动创建的,它是一个管理字体的类。下一步,使用CTFramesetter创建一个或多个框架来渲染文字。

当创建frame时,需要告诉它渲染文本的范围。Core Text会自动的创建CTLine对象为每一行,为具有相同格式的文本块创建CTRun对象。

例如,有些字是红色的,Core Text 会创建一个CTRun实例,然后跟着平常的文本,会再创建一个CTRun,然后跟着粗体的句子,再创建一个CTRun。自己不需要创建CTRun, Core Text会为你创建,基于NSAttributedString的属性。

每个CTRun对象能采用不同的属性,你能控制字距,宽度,长度等。

##Onto the Magazine App
为了创建一个杂志App, 需要为文本标记不同的属性。我们能直接使用NSAttributedString方法,例如setAttributes:range方法,来为文本标识不同的属性,但是这在实际中不可行,除非你想写大量的代码。

为了使工作更加容易,写一个简单的文本标记解析器,而后,使用简单的标记来设置文本的格式在杂志内容中。

导航到File\New\New File, 选择iOS\Cocoa Touch\Objective-C类,点击下一步,输入NSObject的子类,点击下一步,命名新类MarkupParser.m,点击保存。

在MarkupParser.h中删除所有的代码,并复制黏贴如下代码,其定义一些属性和方法来解析。

#import <Foundation/Foundation.h>
#import <CoreText/CoreText.h>

@interface MarkupParser : NSObject {

    NSString* font;
    UIColor* color;
    UIColor* strokeColor;
    float strokeWidth;

    NSMutableArray* images;
}

@property (retain, nonatomic) NSString* font;
@property (retain, nonatomic) UIColor* color;
@property (retain, nonatomic) UIColor* strokeColor;
@property (assign, readwrite) float strokeWidth;

@property (retain, nonatomic) NSMutableArray* images;

-(NSAttributedString*)attrStringFromMarkup:(NSString*)html;

@end

下一步打开MarkupParser.m文件,并用如下内容代替文件中的内容:

#import "MarkupParser.h"

@implementation MarkupParser

@synthesize font, color, strokeColor, strokeWidth;
@synthesize images;

-(id)init
{
    self = [super init];
    if (self) {
        self.font = @"Arial";
        self.color = [UIColor blackColor];
        self.strokeColor = [UIColor whiteColor];
        self.strokeWidth = 0.0;
        self.images = [NSMutableArray array];
    }
    return self;
}

-(NSAttributedString*)attrStringFromMarkup:(NSString*)markup
{

}

-(void)dealloc
{
    self.font = nil;
    self.color = nil;
    self.strokeColor = nil;
    self.images = nil;

    [super dealloc];
}

@end

正如你看到的起始的解析代码非常简单,只包括字体,文本颜色,笔刷大小,笔刷颜色属性。稍后,我们会在文本中增加图片,所以创建了一个数组来存储图片。

写一个解析器是非常困难的工作,所以我只是利用正则表达式来构建一个简单的。这个向导式的解析器非常简单,只支持开放的标志。例如,标记会设置文本的样式直到标记结束,或者一个新的标记出现。标记了文本的内容看起来如下:

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

这会产生如下输出结果:

These are red and blue words.

基于这篇博文是向导式的,这样的标记足以阐明本博文的用意了。而对于你的工程来说,如果你喜欢可以开发的再复杂些。

让我们来解析吧!

在attrStringFromMarkup:方法中,增加如下代码:

NSMutableAttributedString* aString =
[[NSMutableAttributedString alloc] initWithString:@""]; //1

NSRegularExpression* regex = [[NSRegularExpression alloc]
    initWithPattern:@"(.*?)(<[^>]+>|\\Z)"
    options:NSRegularExpressionCaseInsensitive|NSRegularExpressionDotMatchesLineSeparators
    error:nil]; //2

NSArray* chunks = [regex matchesInString:markup options:0
    range:NSMakeRange(0, [markup length])];

[regex release];
  1. 第一,设置了一个空字符串,用来添加找到的文本;
  2. 第二,创建了正则表达式来匹配文本和标记。这个正则表达式匹配文本字符串和紧接着跟着的一个标记。其意思是,寻找任意个数的字符,直到遇到一个开括号,然后匹配任意个字符直到遇到闭括号。或者,当到达字符串的末尾时,停止处理。

为什么要创建该正则表达式?我们会利用它来查找字符串中匹配的地方,然后1. 渲染找到的文本;2. 改变样式根据找到的标志。重复这样的过程直到文本结束。

事实上,这个一个非常简单的解析器。

经过解析,获取了文本块和标记格式在chunks数组中,我们需要遍历它们从文本和标记中构建具有属性的字符串。

在该方法中增加如下代码:

for (NSTextCheckingResult* b in chunks) {
    NSArray* parts = [[markup substringWithRange:b.range] componentsSeparatedByString:@"<"]; //1

    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)self.font,24.0f, NULL);

    //apply the current text style //2
    NSDictionary* attrs = [NSDictionary dictionaryWithObjectsAndKeys:
                            (id)self.color.CGColor, kCTForegroundColorAttributeName,
                            (id)fontRef, kCTFontAttributeName,
                            (id)self.strokeColor.CGColor, (NSString *) kCTStrokeColorAttributeName,
                            (id)[NSNumber numberWithFloat: self.strokeWidth], (NSString *)kCTStrokeWidthAttributeName,
                            nil];
    [aString appendAttributedString:[[[NSAttributedString alloc] initWithString:[parts objectAtIndex:0] attributes:attrs] autorelease]];

    CFRelease(fontRef);

    //handle new formatting tag //3
    if ([parts count]>1) {
        NSString* tag = (NSString*)[parts objectAtIndex:1];
        if ([tag hasPrefix:@"font"]) {
            //stroke color
            NSRegularExpression* scolorRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=strokeColor=\")\\w+" options:0 error:NULL] autorelease];
            [scolorRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags,     BOOL *stop){
                if ([[tag substringWithRange:match.range] isEqualToString:@"none"]) {
                    self.strokeWidth = 0.0;
                } else {
                    self.strokeWidth = -3.0;
                    SEL colorSel = NSSelectorFromString([NSString stringWithFormat: @"%@Color", [tag substringWithRange:match.range]]);
                    self.strokeColor = [UIColor performSelector:colorSel];
                }
            }];

            //color
            NSRegularExpression* colorRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=color=\")\\w+" options:0 error:NULL] autorelease];
            [colorRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){
                SEL colorSel = NSSelectorFromString([NSString stringWithFormat: @"%@Color", [tag substringWithRange:match.range]]);
                self.color = [UIColor performSelector:colorSel];
            }];

            //face
            NSRegularExpression* faceRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=face=\")[^\"]+" options:0 error:NULL] autorelease];
            [faceRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){
                self.font = [tag substringWithRange:match.range];
            }];
        } //end of font parsing
    }
}

return (NSAttributedString*)aString;

这有很多代码,不过,别担心,我们来一部分一部分的解析。

  1. 遍历先前正则表达式匹配的块。并且在块中以”<”字符分割,那么会得到,在parts[0]中有增加到结果中的文本,在parts[1]有标记,它是紧随其后文本的样式。

  2. 创建一个字典,保存一些格式选项,这是传给NSAttributedString格式属性的一种方法。看一下键名,它们使Apple定义的常量,名如其意,也可以查找Apple的Core Text String Attributes Reference,获得详细的信息。然后调用appendAttributedString:方法,新的文本块将会被应用格式增加到结果文本中。

  3. 最后,检查下是否在文本后跟着一个标记,如果它起始于”font”,则用正则查找任意可能的标记属性;
    对face属性来说,字体的名称保存到self.font中,对color属性来说,应用了一些小技巧,例如:font color=”red”,red会被colorRegex获取,并且一个”redColor”的selector被创建,它会在UIColor类上运行,返回一个red color的实例。注意,该方法只对预定义的颜色有用(如果传进了一个不存在的selector,会引起代码crash),但这对这篇向导来说已经足够。处理笔刷颜色属性跟颜色属性差不多,只是当strokecolor是”none”时,设置stroke width为0.0, 此时,没有笔刷应用到文本上。

提示:如果对这部分的正则表达式怎么工作好奇,更详细的信息可以参考NSRegularExpression class reference

渲染格式的文本的一半工作已经做完。现在sttrStringFromMarkup:方法能输入makup,并且返回一个NSAttributedString给Core Text.

所以,现在我们传入一个字符串,试试看!

打开CTView.m文件,在@implementation前加入如下代码:

#import "MarkupParser.h"

找到attString定义的地方,并用如下代码替换:

MarkupParser* p = [[[MarkupParser alloc] init] autorelease]; 
NSAttributedString* attString = [p attrStringFromMarkup: @"Hello <font color=\"red\">core text <font color=\"blue\">world!"];

如下,创建了一个新的解析器,传入了一段文本,它会返回一个格式化的文本。

就是如此,运行试试看。

core_text_tutorial_for_ios_making_a_magazine_app

是不是很cool?多亏了那50行解析代码,使我们避免了处理文本区域文本格式的代码,现在,我们可以在一个文本文件中包含杂志的内容。除此之外,你可以扩展这个简单的解析器,来支持杂志app的其他样式。

##A Basic Magazine Layout
至此,我们能显示文本,这是一个好的开始。但是对于一本杂志而言,需要分栏,但是在这方面Core Text是比较困难的。

在处理布局代码前,我们先在app中加载长点的字符串,以至于有足够的行去分行。

定位到File\New\New File,选择iOS\Other\Empty,点击Next,命名为test.txt, 并保存。

这个文件中获取文本并增加到test.txt,并保存。

打开CTView.m,找到创建MarkupParser和NSAttrbutedString的两行,并删除。在drawRect:方法中移除加载文本文件的代码,因为这些代码不属于这里。UIView的任务是显示给它的内容,而不是加载内容。我们会把attString变量移动到这个类的实例变量和属性中。

打开CoreTextMagazineViewController.m文件,删除现存的内容,增加如下代码:

#import "CoreTextMagazineViewController.h"
#import "CTView.h"
#import "MarkupParser.h"

@implementation CoreTextMagazineViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSString *path = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"txt"];
    NSString* text = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
    MarkupParser* p = [[[MarkupParser alloc] init] autorelease];
    NSAttributedString* attString = [p attrStringFromMarkup: text];
    [(CTView*)self.view setAttString: attString];
}

@end

当application的视图加载的时,app从test.txt中读取文本,并把文本转化成attributed string, 并把它赋值于window view的一个attString属性,至此,我们并没有在CTView中增加该属性。

在CTView.h定义如下三个实例变量:

float frameXOffset;
float frameYOffset;

NSAttributedString* attString;

在CTView.h和CTView.h中增加相应的代码如下:

//CTView.h
@property (retain, nonatomic) NSAttributedString* attString;

//CTView.m
//just below @implementation ...
@synthesize attString;

//at the bottom of the file
-(void)dealloc
{
    self.attString = nil;
    [super dealloc];
}

现在运行试试看,你如看到如下画面:

core_text_tutorial_for_ios_making_a_magazine_app

怎样使这个文本分栏目?幸运的是,Core Text提供了一个函数 - CTFrameGetVisibleStringRange. 这个函数告诉我们在给定的frame中能装多少的文本。所以我们的想法是,创建column, 检查里面能放多少文本,如果多了,创建另一个column.(columns在这里的意思是CTFrame实例,columns无非是再高点的矩形)

首先,我们需要有分栏,然后有页面,然后是整本杂志。使CTView继承UIScrollView, 具有翻页和滚动功能。

打开CTView.h文件,改变@interface行如下:

@interface CTView : UIScrollView {

OK! 现在具有滚动和分页功能。来看看怎么使他们有效。

至此,我们是在drawRect:方法中创建framesetter和frame的。当具有比较的栏目和格式的时候,其实只需要做一次计算。因此创建一个新类CTColunmView,它只负责渲染传递给它的内容到CT,在CTView中我们只创建CTColumnView的实例,并把他们作为子视图添加到视图层次中。

总结如下:CTView处理滚动,翻页和构建分栏;CTColumnView渲染内容到屏幕。

定位到File\New\New File, 选择iOS\Cocoa Touch\Objective-C 类,点击下一步,输入UIView的子类,点击下一步,命名新类为CTColumnView.m,并点击保存。如下是CTColumnView类的初始代码:

//inside CTColumnView.h

#import <UIKit/UIKit.h>
#import <CoreText/CoreText.h>

@interface CTColumnView : UIView {
    id ctFrame;
}

-(void)setCTFrame:(id)f;
@end

//inside CTColumnView.m

#import “CTColumnView.h”

@implementation CTColumnView
-(void)setCTFrame: (id) f
{
    ctFrame = f;
}

-(void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();

    // Flip the coordinate system
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);

    CTFrameDraw((CTFrameRef)ctFrame, context);
}
@end

这个类做了很多的工作,而现在它只是渲染CTFrame。在杂志中,对每个文本栏,创建一个该类的实例。

首先,在CTView中增加一个属性了保存这些frames,并声明buildFrames方法,它会初始化columns的设置。

//CTView.h - at the top
#import "CTColumnView.h"

//CTView.h - as an ivar
NSMutableArray* frames;

//CTView.h - declare property
@property (retain, nonatomic) NSMutableArray* frames;

//CTView.h - in method declarations
- (void)buildFrames;

//CTView.m - just below @implementation
@synthesize frames;

//CTView.m - inside dealloc
self.frames = nil;

现在buildFrames能够创建文本frames,并把他们保存到frames数组中。代码如下:

- (void)buildFrames
{
    frameXOffset = 20; //1
    frameYOffset = 20;
    self.pagingEnabled = YES;
    self.delegate = self;
    self.frames = [NSMutableArray array];

    CGMutablePathRef path = CGPathCreateMutable(); //2
    CGRect textFrame = CGRectInset(self.bounds, frameXOffset, frameYOffset);
    CGPathAddRect(path, NULL, textFrame );

    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);

    int textPos = 0; //3
    int columnIndex = 0;

    while (textPos < [attString length]) { //4
        CGPoint colOffset = CGPointMake( (columnIndex+1)*frameXOffset + columnIndex*(textFrame.size.width/2), 20 );
        CGRect colRect = CGRectMake(0, 0 , textFrame.size.width/2-10, textFrame.size.height-40);

        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, colRect);

        //use the column path
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(textPos, 0), path, NULL);
        CFRange frameRange = CTFrameGetVisibleStringRange(frame); //5

        //create an empty column view
        CTColumnView* content = [[[CTColumnView alloc] initWithFrame: CGRectMake(0, 0, self.contentSize.width, self.contentSize.height)] autorelease];
        content.backgroundColor = [UIColor clearColor];
        content.frame = CGRectMake(colOffset.x, colOffset.y, colRect.size.width, colRect.size.height) ;

        //set the column view contents and add it as subview
        [content setCTFrame:(id)frame];  //6   
        [self.frames addObject: (id)frame];
        [self addSubview: content];

        //prepare for next frame
        textPos += frameRange.length;

        //CFRelease(frame);
        CFRelease(path);

        columnIndex++;
    }

    //set the total width of the scroll view
    int totalPages = (columnIndex+1) / 2; //7
    self.contentSize = CGSizeMake(totalPages*self.bounds.size.width, textFrame.size.height);
}
  1. 初始化工作。定义x,y方向的偏移量,使能翻页,并创建一个空的frames数组。
  2. buildFrames为视图的bounds创建path和frame。
  3. 声明textPos, 它保存文本中的当前位置。并且声明columnIndex, 计数创建了多少栏。
  4. 执行while循环,直到到达了文本的末尾。在循环内,创建栏的边框:colRect是一个矩形,它由columnIndex决定。
  5. 利用CTFrameGetVisibleStringRange函数算出那部分字符串在frame内。textPos增加range的长度,构建下一个column时,从下一个循环开始。
  6. 这里,像以前那样画frame,而是把它传递给了新创建的CTColumnView, 并保存到self.frames中。
  7. 最后,totalPages保存产生了多少页,CTView的contentSize属性会被设置,所以当有多于一页的内容时,我们就能滚动了。

当CT 建立后,调用buildFrames,在CoreTextMagazineViewController.m的viewDidLoad的末尾增加如下代码:

[(CTView *)[self view] buildFrames];

另一件事情需要做的是,在CTView.m文件中,找到drawRect:方法,并删除它。因为我们在CTColumnView类中做了全部的渲染工作,所以让CTView drawRect:方法只做UIScrollView实现的标准工作就可以了。

现在运行试试看,并向右,向左拖拽,如下图:

core_text_tutorial_for_ios_making_a_magazine_app

我们现在又分栏,格式化的文本,但是缺少图片,利用Core Text来显示图片不是非常的容易,毕竟它只是一个文本框架。

但是多亏了标记解析器,我们可以很迅速的在文本中增加图片。

##Drawing Images in Core Text
基本上,Core Text 没有可能去画图片。然而,由于它是一个布局engine, 所以它能做的是留一块空白的区域去画图片。因为所有的代码都在drawRect:方法中,在其中画中图片是非常容易的。

让我们来看看怎么在文本中留出空间画图片。记住所有的文本块是CTRUN实例,对于给定的一个CTRun,只是简单了设置其代理delegate对象,来负责让Core Text知道 上边距,下边距,宽度等。

core_text_tutorial_for_ios_making_a_magazine_app

当Core Text遇到一个CTRun对象,它会询问它的代理,这块数据区要预留多少宽度,多少高度。以这种方式在文本中构建一个空白的块,然后在上面作画。

我们以在标记解析器中增加”img”标志来开始讲解。打开MarkupParser.m文件,并找到末尾的”}”处,增加如下代码处理”img”标签。

if ([tag hasPrefix:@"img"]) {

    __block NSNumber* width = [NSNumber numberWithInt:0];
    __block NSNumber* height = [NSNumber numberWithInt:0];
    __block NSString* fileName = @"";

    //width
    NSRegularExpression* widthRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=width=\")[^\"]+" options:0 error:NULL] autorelease];
    [widthRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){ 
        width = [NSNumber numberWithInt: [[tag substringWithRange: match.range] intValue] ];
    }];

    //height
    NSRegularExpression* faceRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=height=\")[^\"]+" options:0 error:NULL] autorelease];
    [faceRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){
        height = [NSNumber numberWithInt: [[tag substringWithRange:match.range] intValue]];
    }];

    //image
    NSRegularExpression* srcRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=src=\")[^\"]+" options:0 error:NULL] autorelease];
    [srcRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){
        fileName = [tag substringWithRange: match.range];
    }];

    //add the image for drawing
    [self.images addObject:
    [NSDictionary dictionaryWithObjectsAndKeys:
        width, @"width",
        height, @"height",
        fileName, @"fileName",
        [NSNumber numberWithInt: [aString length]], @"location",
        nil]
    ];

    //render empty space for drawing the image in the text //1
    CTRunDelegateCallbacks callbacks;
    callbacks.version = kCTRunDelegateVersion1;
    callbacks.getAscent = ascentCallback;
    callbacks.getDescent = descentCallback;
    callbacks.getWidth = widthCallback;
    callbacks.dealloc = deallocCallback;

    NSDictionary* imgAttr = [[NSDictionary dictionaryWithObjectsAndKeys: //2
                            width, @"width",
                            height, @"height",
                            nil] retain];

    CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, imgAttr); //3
    NSDictionary *attrDictionaryDelegate = [NSDictionary dictionaryWithObjectsAndKeys:
                                        //set the delegate
                                        (id)delegate, (NSString*)kCTRunDelegateAttributeName,
                                        nil];

    //add a space to the text so that it can call the delegate
    [aString appendAttributedString:[[[NSAttributedString alloc] initWithString:@" " attributes:attrDictionaryDelegate] autorelease]];
}

让我们来看看代码。事实上,解析”img”标签跟前面解析字体标签的处理过程一样。使用了三个正则表达式来读取图像的宽度,高度和源文件位置。解析完后,创建一个字典保存这些属性,并在字典中九路图像在文本中的位置。

现在看第一部分-CTRunDelegateCallbacks是一个C结构体,保存对函数的引用。这个结构体将传递给CTRunDelegate. 正如你猜到的那样,getWidth方法能获取CTRun的宽度,getAscent能获取CTRun的高度,等等,在代码中提供了这些函数的句柄,稍后我们会添加函数体的代码。

第二部分非常重要-imgAttr字典保存着图片的维度,这个对象将会传递给函数句柄,即当getAscent触发时,它会获得imgAttr的一个参数,读取图像的高度,并提供给CT.

在第三部分中,CTRunDelegateCreate创建一个代理实例的引用,并绑定了回调函数和数据参数。

下一步是创建属性字典,而不是格式属性传递给代理实例。最后,在文本中创建了一个块空白的空间,稍后图片会渲染上去。

下一步,正如你期望的,给代理提供回调函数如下:

//inside MarkupParser.m, just above @implementation

/* Callbacks */
static void deallocCallback( void* ref ){
    [(id)ref release];
}
static CGFloat ascentCallback( void *ref ){
    return [(NSString*)[(NSDictionary*)ref objectForKey:@"height"] floatValue];
}
static CGFloat descentCallback( void *ref ){
    return [(NSString*)[(NSDictionary*)ref objectForKey:@"descent"] floatValue];
}
static CGFloat widthCallback( void* ref ){
    return [(NSString*)[(NSDictionary*)ref objectForKey:@"width"] floatValue];
}

ascentCallback,descentCallback和widthCallback只读取字典中各自的属性,并提供给CT. 而deallocCallback释放字典对象。

现在解析器能够处理img标签了,现在来调整CTView来渲染他们。我们需要一个方法把这些图片发送给视图,让我们把设置属性字符创和图片合并进一个方法,增加代码如下:

//CTView.h - inside @interface declaration as an ivar
NSArray* images;

//CTView.h - declare property for images
@property (retain, nonatomic) NSArray* images;

//CTView.h - add a method declaration
-(void)setAttString:(NSAttributedString *)attString withImages:(NSArray*)imgs;

//CTView.m - just below @implementation
@synthesize images;

//CTView.m - inside the dealloc method
self.images = nil;

//CTView.m - anywhere inside the implementation
-(void)setAttString:(NSAttributedString *)string withImages:(NSArray*)imgs
{
    self.attString = string;
    self.images = imgs;
}

现在CTView接受到了images的数组,我们把图片传递给视图并渲染他们。

定位到CoreTextMagazineViewController.m并找到”[(CTView*)self.view setAttString: attString];”并代替为如下代码:

[(CTView *)[self view] setAttString:attString withImages: p.images];

如果在MarkupParser类中找到attrStringFromMarkup:方法,将会在self.images属性中看到所有的图片数据。

渲染图片,必须知道渲染的位置。为了找到位置,必须考虑如下几点:

  • contentOffset 当内容滚动时
  • offset CTView的frame(frameXOffset, frameYOffset)
  • CTLine的原始坐标(CTLine可能在段落开始处有偏移)
  • CTRun的原始点与CTLine的原始点的距离

core_text_tutorial_for_ios_making_a_magazine_app

为了渲染图片,更新CTColumnView类如下:

//inside CTColumnView.h
//as an ivar
NSMutableArray* images;

//as a property
@property (retain, nonatomic) NSMutableArray* images;

//inside CTColumnView.m
//after @implementation...
@synthesize images;

-(id)initWithFrame:(CGRect)frame
{
    if ([super initWithFrame:frame]!=nil) {
        self.images = [NSMutableArray array];
    }
    return self;
}

-(void)dealloc
{
    self.images= nil;
    [super dealloc];
}

//at the end of drawRect:
for (NSArray* imageData in self.images) {
    UIImage* img = [imageData objectAtIndex:0];
    CGRect imgBounds = CGRectFromString([imageData objectAtIndex:1]);
    CGContextDrawImage(context, imgBounds, img.CGImage);
}

如上代码,我们增加了images属性,在其中保存栏目中出现的图片。为了避免声明另一个新类来保存图像数据,我们直接使用数组对象保存:

  1. UIImage的实例
  2. 图像的bounds, 例如图像在文本中的原始点和大小

现在增加计算图片位置的代码,并把它们赋予各自的文本栏:

//inside CTView.h
-(void)attachImagesWithFrame:(CTFrameRef)f inColumnView:(CTColumnView*)col;

//inside CTView.m
-(void)attachImagesWithFrame:(CTFrameRef)f inColumnView:(CTColumnView*)col
{
    //drawing images
    NSArray *lines = (NSArray *)CTFrameGetLines(f); //1

    CGPoint origins[[lines count]];
    CTFrameGetLineOrigins(f, CFRangeMake(0, 0), origins); //2

    int imgIndex = 0; //3
    NSDictionary* nextImage = [self.images objectAtIndex:imgIndex];
    int imgLocation = [[nextImage objectForKey:@"location"] intValue];

    //find images for the current column
    CFRange frameRange = CTFrameGetVisibleStringRange(f); //4
    while ( imgLocation < frameRange.location ) {
        imgIndex++;
        if (imgIndex>=[self.images count]) return; //quit if no images for this column
        nextImage = [self.images objectAtIndex:imgIndex];
        imgLocation = [[nextImage objectForKey:@"location"] intValue];
    }

    NSUInteger lineIndex = 0;
    for (id lineObj in lines) { //5
        CTLineRef line = (CTLineRef)lineObj;

        for (id runObj in (NSArray *)CTLineGetGlyphRuns(line)) { //6
            CTRunRef run = (CTRunRef)runObj;
            CFRange runRange = CTRunGetStringRange(run);

            if ( runRange.location <= imgLocation && runRange.location+runRange.length > imgLocation ) { //7
                CGRect runBounds;
                CGFloat ascent;//height above the baseline
                CGFloat descent;//height below the baseline
                runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL); //8
                runBounds.size.height = ascent + descent;

                CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL); //9
                runBounds.origin.x = origins[lineIndex].x + self.frame.origin.x + xOffset + frameXOffset;
                runBounds.origin.y = origins[lineIndex].y + self.frame.origin.y + frameYOffset;
                runBounds.origin.y -= descent;

                UIImage *img = [UIImage imageNamed: [nextImage objectForKey:@"fileName"] ];
                CGPathRef pathRef = CTFrameGetPath(f); //10
                CGRect colRect = CGPathGetBoundingBox(pathRef);

                CGRect imgBounds = CGRectOffset(runBounds, colRect.origin.x - frameXOffset - self.contentOffset.x, colRect.origin.y - frameYOffset - self.frame.origin.y);
                [col.images addObject: //11
                    [NSArray arrayWithObjects:img, NSStringFromCGRect(imgBounds) , nil]
                ]; 
                //load the next image //12
                imgIndex++;
                if (imgIndex < [self.images count]) {
                    nextImage = [self.images objectAtIndex: imgIndex];
                    imgLocation = [[nextImage objectForKey: @"location"] intValue];
                }
            }
        } 
        lineIndex++;
    }
}

让我们一步步的来解析

  1. CTFrameGetLines函数返回CTLine对象的数组;
  2. 获得frame中所有行的原始点,即所有文本行的左上角点;
  3. 加载下一张图片的数据属性,imgLocation保存图片的字符串位置;
  4. CTFrameGetVisibleStringRange获取在frame中渲染的的可见文本范围,然后遍历图像数组,找到当前frame要渲染的图片。
  5. 遍历每个文本行,并把每一行加载到”line”变量中;
  6. 遍历行中的每个run, 可以通过CTLineGetGlyphRuns获取;
  7. 检查图片是否在当前的run中,如果在,在此处处理图片的渲染;
  8. 通过CTRunGetTypographicBounds方法获取run的宽度和高度;
  9. 通过CTLineGetOffsetForStringIndex获取run的原始点、及其他偏移量;
  10. 根据文件名加载图片,并获取当前栏的区域,和图像最终渲染的区域;
  11. 创建保存UIImage和其frame的数组,并添加到CTColumnView的image list中;
  12. 最后,加载下一副图片,在文本行中继续遍历;

最后一步,是在CTView.m中找到”[content setCTFrame:(id)frame];”, 并增加如下代码:

[self attachImagesWithFrame:frame inColumnView: content];

现在,有了全部的工作代码,但是还没有内容来显示…

别担心,我准备了Zomie Monthly - 一本月刊,可以按如下步骤加载其内容:

  1. 在工程中删除test.txt文件;
  2. 加载并解压Zombie mag materials;
  3. 把解压后的文件拖拽到Xcode工程中,确保”Copy items into destination group’s folder (if needed)”勾选。

切换到CoreTextMagazineViewController.m,改变获取文件的代码如下:

NSString *path = [[NSBundle mainBundle] pathForResource:@"zombies" ofType:@"txt"];

然后,编译并运行,你就能看到Zombie 月刊杂志了.

core_text_tutorial_for_ios_making_a_magazine_app

最后,要使栏目中的文本对齐,即两端对齐,增加如下代码:

//inside CTView.m
//at the end of the setAttString:withImages: method

CTTextAlignment alignment = kCTJustifiedTextAlignment;

CTParagraphStyleSetting settings[] = {
    {kCTParagraphStyleSpecifierAlignment, sizeof(alignment), &alignment},
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, sizeof(                                       settings) / sizeof(settings[0]));
NSDictionary *attrDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
                                (id)paragraphStyle, (NSString*)kCTParagraphStyleAttributeName,
                                nil];

NSMutableAttributedString* stringCopy = [[[NSMutableAttributedString alloc] initWithAttributedString:self.attString] autorelease];
[stringCopy addAttributes:attrDictionary range:NSMakeRange(0, [attString length])];
self.attString = (NSAttributedString*)stringCopy;

这是段落样式的起点,请参照kCTParagraphStyleSpecifierAlignment的Apple文档。

When to use Core Text and why?

现在杂志app已经完成,你可能会问自己:”我为什么要使用Core Text,而不是UIWebView呢?”

是的, CT和UIWebView在各自领域内都非常有用。

别忘了UIWebView是一个成熟的web浏览器,如果只是用它来显示多行文本,只是大材小用。

例如在UI中有10个多颜色的标签,这意味消耗的内存10 Safaris。

请记住,UIWebView是一个浏览器,而Core Text 是一个有效率的文本渲染engine。

Where To Go From Here?

这里是基于上面向导创建的Core Text example project工程。

如果想基于本项目学习更多的Core Text知识,请读一下苹果的文档Core Text Reference Collection,并且你可以给本app增加如下属性:

  • 在解析中支持更多的标签;
  • 支持更多的run样式;
  • 支持更多的段落样式;
  • 支持自动的应用样式到文字,段落,句子的功能;
  • 支持调整字间距功能;

知道了怎么扩展解析器,这里,我再推荐两个链接:

你有任何的问题,评论,建议,欢饮加入讨论!