在latex中调整hyphenation

用latex写英文论文的时候,可能会遇到断字符 (hyphenation) 在不该断开的地方断开的问题。因为英文单词长短不一,latex排版的时候为了让论文整体上看起来比较美观,可能会把落在行尾的单词从中断开,一部分留在当前这一行并且以一个短横线(-)也就是Hyphenation结尾,剩下的部分新起一行。

在英文文章的排版中,hyphenation是很重要的,特别是当行尾的单词很长的时候,如果不作断字,把单词都放在当前行就显得挤,新起一行就显得松。因为中文文章不存在这个问题,所以自己平时也没注意到。至于各个单词具体应该怎样断字,我还没有完全弄清楚,似乎也没有一个明确的规则,而且对于美式英语,英式英语也不尽相同。但是有一些简单考量一个断字点是不是正确的方法,比如,会不会造成歧义,是不是和单词的读音一致,或者是不是前/后缀之类的。

latex使用了处理断字的算法去自动的找断字的地方,而且latex会调整单词间距,使得文章看起来不会显得疏密不一致。大多数情况下,这些算法都工作得很好。但是因为断字的算法是根据某种规则来处理单词的断字,而不是依照人工事先标注的字典,所以,它仍然会出问题。或是在不该断的地方断开了,又或者是断开的地方太多了等等。在latex下可以通过调整参数和指定断字点来处理这些问题。

\hyphenpenalty=5000
\tolerance=1000

可以把这两个参数的调整加到tex文件里。hyphenpenalty的意思比较显而易见,这个值越大断字出现的就越少。tolerance越大,换行就会越少,也就是说,latex会把本该断开放到下一行的单词,整个儿的留在当前行。调这两个值就可以得到不一样的排版,有可能可以解决断字太多的问题。

如果遇到了断开的地方不对的情况,也可以手动来指定一个单词应该怎么断。

\hyphenation{hy-phen-a-tion}

这个命令告诉latex,hyphenation只能从标有短横线(-)的地方断开。

嗯,就是这些了。

橡皮鸭调试法

偶然在StackOverflow上看到Rubber duck debugging (wiki) 这个概念,有点儿意思,不过直译成橡皮鸭调试法好像比较弱啊。

按照wiki上的说法

传说中程序大师调试代码的时候会在桌上放上一只玩具橡皮鸭,调试的时候他会不断地,详细地,向鸭子解释自己的代码…

如果没有玩具小鸭子也可以考虑向其它东西倾诉..比如桌上的花花草草,键盘鼠标 (汗)。

好吧,正经地说这也是软件工程里的一个概念,一边阐述修改代码的意图一边做调试,就会更容易发现自己的错误。

类似的,有一种现象叫做Cone of Answers,不知道如何翻译这个词。这是一个常见的现象。你的朋友跑来问你一个问题,但是当他自己把问题说完,或者说到一半的时候就想出了答案走了,留下一脸茫然的你。是的,这个时候你就起到了那只橡皮鸭子的作用…

相似的概念还有不少,前面的wiki页面底部列出了好几个。总的来说,在你试图表述自己的想法的过程中,自然地在促使自己去整理思路,重新考虑问题。Thinking out loud 可能是一种不错的做法。

siri和语音对话系统

Siri最近是相当的火,朋友Sigma最近的一片文章也有讨论她用到的技术。
Siri本身是一个语音对话系统。从输入到输出,经过语音识别,对话系统到语音合成。
想起自己当年的毕业设计就是一个对话系统,再看看Siri,觉得自己的东西太naive了 :]

系统前后两端的技术已经比较成熟了。

嘈杂环境下的实时语音识别可能仍然是一个问题,
但是Siri的使用环境使得她不需要去考虑这两点。
第一,用户不太可能在很吵闹的地方对着手机喊叫,这看起来太傻了。
第二,用户不需要Siri随时都监听输入。
这样一来,输入方面,误识别率大大下降了。

我很好奇的是Siri怎样做到和说话人无关的识别。
一般来说,如果期望识别系统有一个比较好的识别率,
都需要事先针对说话人对系统做一些训练。
对于不同的个人得到相应的特征偏移。
小词汇量的连续语音识别和命令式的语音识别对这方面要求不高,
但是大词汇量下的连续语音识别却是比较依赖事先训练。
难道是米国人的发音都很标准?

对话系统的核心是从文本到文本的这一段。
也就是从已经识别出来的,用户说的话,到Siri给出对应的反馈,这个过程。

这方面的研究也有很长的历史了。比如,很著名的图灵测试。
图灵测试里,机器的目的就是通过对话骗过裁判来相信它们是人类。
铜奖的标准是在文本对话上完成这个任务,银奖则需要语音上的完美模拟,
金奖就得面对面的自然交谈了。
目前还没有机器能达到银奖水平。

Siri,和图灵测试里的程序的不同之处在于她需要提供有用的服务。
如果Siri只是在跟你打哈哈,即使内容有趣,也不会有多大的用处。
所以Siri需要真正得理解对话的内容。
这就把人工智能多年以来的很多工作整合起来了,比如自然语言理解、专家系统、
甚至到逻辑推理。
这些才是人工智能的核心内容。

Siri和生活服务的整合方面则是语义网成功的应用。这方面我不太了解。
只是早先听说语义网在小的、定制的范围内有很成功的应用,
因为这一块需要比较大量的工程上的工作。
其实想法很好理解,而且大家都想过。
大家都曾经希望对话系统能够自发地去网上找缺失的信息。
但是机器没有办法直接消化处理搜索引擎的输出。机器需要信息按照机器能理解的方式去组织。
所以就有了语义网这个概念。网络上所有的信息都需要它的标签,机器可以理解的标签。
对于任何一个词,机器可以方便地找到相关的知识。
所以机器能知道”当我谈跑步时,我谈些什么” :]

我们可以隐隐感觉到语言理解是各个问题的核心,也是人工智能的初衷之一。
我们期望能和机器交谈。
当她们的外表越来越接近我们,她们似乎也应该有同样丰富的内心。
当我们向机器提出一个问题的时候,我们希望她能够给出满意的回答。
无论她通过什么手段。无论她叫什么名字。

静态链接库

静态链接库

几个例子,使用和建立静态库的时候的几种常见情景。

首先建几个文件
a.h

void testA();

a.c

#include "a.h"

void testA() {
    printf("A");
}

b.h

void testB();

b.c

#include "b.h"
#include "a.h"

void testB(){
    testA();
    printf("B");
}
  1. 得到目标文件
  2. $gcc -c a.c
    
    $gcc -c b.c
    
  3. 得到静态库文件
  4. $ ar -r libba.a a.o b.o
    
    $ ar -r liba.a a.o
    
    $ ar -r libb.a b.o
    

得到了库libba.a libb.a liba.a

这是最终需要完成的程序。
test.c

#include "b.h"
void main()
{
    testB();
}

假设我们没有a.c,b.c这两个源文件。
在这种情况下,为了编译test.c得到可执行文件,至少需要那些文件?

由于b.h中包含了a.h,所以两个头文件都是需要的。

  1. 用libba.a
  2. $ gcc test.c -o test -L. -lba
    

    我们需要的是testB()和testA()的实现,libba.a里显然有这些实现,所以是可行的。

  3. 用libb.a 和 liba.a
  4. $ gcc test.c -o test -L. -lb -la
    

如果一开始就没有a.c,怎么办?
假设liba.a是一个第三方的闭源库,我们只有liba.a和a.h
当然,为了得到test这个可执行文件,我们也可以使用上面第二种方式编译。

如果我们不是要得到test,而是要为其它人提供libb.a怎么办?
上面的libb.a只含有testB()的实现,而没有testA()的实现。
除非“其它人”也有liba.a,否则只用libb.a是没有办法使用testB()这个函数的。

那么是不是可以把liba.a链入libb.a?
对于静态库来说,是可以的。这也就是我们的解决办法。
-r 告诉Linker进行增量式链接,得到二进制目标文件,而不是最终的可执行文件。

$ ar -r libnba.a ba.o

这个libnba.a和前面通过

$ ar -r libba.a a.o b.o

得到的libba.a应该是一样的。

这时我们就可以把libnba.a提供给”其它人”了。

$ gcc test.c -o test -L. -lnba

可以得到test可执行文件。

[cocoa demo]从图片中选取Frame的小工具

和设计相比,开发者的好处是遇到不好用的App就可以牛B哄哄的说:
“这个不给力啊,等哪天有空的时候自己做一个吧”。
等到真正要做的时候就愣了,满脑子的按钮不知道怎么摆….

在学校的时候,做的东西对UI的要求很低,不求美观,只要基本功能有就行了。
更不要谈什么用户体验,是不是User-Friendly之类的了。
工作的时候就大不一样了,做prototype的时候界面可能很粗糙。
但是等到Designer的东西(一般管这个叫mockup)出来,感觉就完全不一样了。
作为码农,常常觉得自己没有审美观….

我认为完全重现mockup的效果是很重要的,尤其是一些细节。
功能上开发者可以发挥,但是在自己不擅长的领域还是不要胡闹了。

好了,扯远了….

其实只是因为最近想学mac上的开发,然后因为有以上需求,
所以做了一个非常简单的工具作为练习。
真的很简单….不过如果你恰好有同样的需求,我觉得它还是有帮助的。
至少作为一个cocoa入门的demo,还是挺好的。

GetFrame

就是从mockup里框取一块区域,得到经过转换坐标的Frame。
左上的Button是用来打开图片文件的,左下的是用来切换显示的。

代码在Github上: GetFrame
App下载链接: GetFrame.zip

mac下的图标格式是icns
用这个网站转换图标比较方便,推荐一下: http://iconverticons.com/

[iOS]delegate和protocol

今天上班和同事讨论工程怎么组织的时候涉及到这个话题。
iOS开发上对delegate使用广泛。
记在这里,如果有新人Google到了,希望能有点帮助。

protocol和delegate完全不是一回事,放在一起说,只是因为我们经常在同一个头文件里看到这两个word。

protocol和java里interface的概念类似,是Objective-C语法的一部分。
定义protocol如下

@protocol ClassADelegate

- (void)methodA;
- (void)methodB;

@end

那么就是定义了一组函数,这组函数放在一起叫作一个protocol,也就是协议。

函数是需要被实现的,所以如果对于class如下

@interface ClassB  {
}
@end

就叫作ClassB conform to protocol ClassADelegate,也就是说ClassB实现了这个协议,
也就是实现了这一组函数。

有了上面这个头文件,我们就可以放心作调用

ClassB *b = [[ClassB alloc] init];
[b methodA];
[b methodB];

而不用担心出现unrecognized selector sent to instance这种错误了。

所以protocol就是一组函数定义,是从类声明中剥离出来的一组定义。

id b = ...;
[b methodA];

这种用法也常见,b是一个id类型,它知道ClassADelegate这组函数的实现。

那么delegate是什么?其实和protocol没有关系。Delegate本身应该称为一种设计模式。
是把一个类自己需要做的一部分事情,让另一个类(也可以就是自己本身)来完成。
比如ClassC

@interface ClassC {
    id delegate;
}
@end

那么ClassC的实现(.m文件)里就可以用delegate这个变量了。
当然这里完全可以用其它名字而不是delegate。

我们也可以这样写

@interface ClassC {
    ClassB *delegate;
}
@end

这样我们知道了delegate是一个ClassB,它就可以提供ClassB里的方法。
可以把一部分ClassC里的工作放在ClassB里去实现。
这样的写法看起来是不是有点奇怪?或者应该写成这样?

@interface ClassC {
    ClassB *classB;
}
@end

…..

delegate没有了…
所以说其实delegate只是一种模式,大家约定俗成,当把自己内部一部分实现暴露给另外一个类去做的时候,就叫实际做事的类为delegate。

为什么会需要把内部实现提出来给另一个类做呢?
最常见的目的就是为了在隐藏实现的前提下,提供一个自定义的机会。
比如Apple提供的iOS SDK里就有众多的delegate,比如最常用的UITableView,
我们没法知道Apple怎么重用UITableViewCell,怎么处理UITableView里Cell的增加、删减,因为我们没有源码。
但是我们可以通过实现Delegate的方法来控制一个UITableView的一些行为。
UITableViewDataSource其实和delegate是一样一样的,只是由于意义不同换了个名字罢了。

protocol在此扮演了什么角色呢?
protocol是一种语法,它提供了一个很方便的、实现delegate模式的机会。
比如写UITableView的时候,Apple这么干
UITableView.m

- (void)doSomething {
    [self blahblah];
    
    [self.delegate guruguru];
   
    [self blahblah];
 }

delegate是我们写的类,这个类如果可以被传给UITableView做为其delegate,那唯一要求,就是它实现了

- (void)guruguru;

这个方法。

如果我们把这个方法定义在一个protocol里

@protocol XXXProtocol

- (void)guruguru;

@end

就说明了,UITableView需要的delegate是一个conform to XXXProtocol的类。
这就正好是

id

表达的意思。
无论具体的类是什么,它还有其它什么方法,只要它conform to这个protocol,
就说明它可以被传给UITableView,作为它的delegate。
那么Apple为了让我们知道这个protocol是delegate需要conform的protocol,
它就把XXXProtocol改成了UITableViewDelegate

这样我们看到protocol的名字里有Delegate,就知道这个protocol里的函数是用来做自定义(Customization)的了。

代码最终还是给人看的,公司里尤其如此。
大家都希望对方把事情讲得清晰易懂,如果在再有两句俗语或者行话那大家就很开心了 :]

贝塞尔曲线拟合

接上一篇Blog,这里用贝塞尔曲线来平滑多个点。

和样条插值不同,在多个点上作贝塞尔曲线的时候,曲线只穿过首尾两个点,中间的点都是作为控制点。

移动控制点,曲线也随之形变,可以造成一种拉扯的效果。在各种作图工具中,经常使用贝塞尔曲线来画曲线。一般的操作都是先画一条线段,然后可以通过拖动一个控制点来调整线段的弯曲程度。

作多点贝塞尔曲线只需要一个公式。所有的点的X值,被归一化到[0,1]区间内。
具体理论,可以参考这个页面Bézier curves。89年创建的,可有年头了。

这里还是贴代码吧。

首先需要得到X区间的总长度。

CGPoint startPt = [[_points objectAtIndex:0] CGPointValue];
CGPoint endPt = [[_points objectAtIndex:(self.pointCount - 1)] CGPointValue];
float amount = endPt.x - startPt.x;

然后就是曲线方程了,这个比样条插值要简单不少。
rank是指总的阶数,也就是实际的点数。这个函数表示n个点的贝塞尔曲线在x处的值。
这里的ux属于区间[0,1]

float (^bezierSpline)(int rank, float ux) = ^(int rank, float ux) {
        
    float p = 0.0f;
        
    for (int i = 0; i < rank; i++)
    {
        CGPoint pt = [[_points objectAtIndex:i] CGPointValue];
            
        p += pt.y * powf((1 - ux), (rank - i - 1)) * powf(ux, i) * (factorial(rank - 1) 
                  / (factorial(i) * factorial(rank - i - 1)));
    }
    
    return p;
};

下面很容易了,画图的时候步长为1,求得ux之后,带入方程得到点的y值,画曲线。

    [path moveToPoint:startPt];
    
    for (float curX = startPt.x; (curX - endPt.x) < 1e-5; curX += 1.0f)
    {
        float u = (curX - startPt.x) / amount;
        [path addLineToPoint:
            CGPointMake(curX, bezierSpline(self.pointCount, u))];
    }
    
    CGContextSetLineWidth(context, 1.0f);
    CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
    CGContextAddPath(context, path.CGPath);
    CGContextStrokePath(context);
}

和三次样条相比,由于不经过中间点,丢失细节比较多,平滑度更好。

平滑前
no bezier

三次样条
no bezier

贝塞尔曲线
no bezier

示例代码: Sample-CurveFit
和上个例子放在一个repo里。

参考: Wiki

三次样条插值和曲线拟合

很多东西不在手上用着就容易忘,尤其是书本知识。就弄这么个类别,叫作“书到用时方恨少”,来记录这些知识。

曲线拟合是一个“数值计算“中的一个基本内容。在实际的项目中,使用拟合的目的就是从有限个点得到一条平滑曲线。曲线本身也是由点构成的,所以如何从有限个点得到曲线上的其它点,就是插值所关注的内容。

插值的方法有很多,把这些个点逐个用直线段连起来也是一种插值。

样条插值是一种工业设计中常用的、得到平滑曲线的一种插值方法,三次样条又是其中用的较为广泛的一种。

Google 三次样条插值可以看得到不少材料,这里就不罗列公式了,直接看看在代码里,我们怎么做。

首先我们需要各个点的坐标,以x,y表示。

        const int len = [_points count];
        float x[len];
        float y[len];
        for (int i = 0; i < len; i++)
        {
            CGPoint p = [[_points objectAtIndex:i] CGPointValue];
            x[i] = p.x;
            y[i] = p.y;
        }

取变量x,y

从算法中可以得知,我们的目标是样条插值函数,这是一个分段函数,x最高次数为三次,在各个点二次连续可导以保证最终函数曲线的光滑性。
我们每两个点求一个三次函数,我们有n个点,那么这里就需要4(n-1)个方程。
目前我们有n个点的坐标,有n-2个连接点,有n个函数两次连续可导,这里有n+n-2+2*(n-2)共4n-6个方程,还差两个条件。
这里一般有三种处理方法,最方便的,也是我们这里使用的是自然三次样条,也就是在首尾两个点上二次导为0。

具体计算不在此列举了,根据算法构建一个方程组求一组中间值sx,左边是一个三对角矩阵。

        float h[len];
        float u[len];
        float lam[len];
        for (int i = 0; i < len-1; i++)
        {
            h[i] = x[i+1] - x[i];
        }    

        u[0] = 0;
        lam[0] = 1;
        for (int i = 1; i < (len - 1); i++)
        {
            u[i] = h[i-1]/(h[i] + h[i-1]);
            lam[i] = h[i]/(h[i] + h[i-1]);
        }
        
        float a[len];
        float b[len];
        float c[len];
            
        float m[len][len];
        for (int i = 0; i < len; i++)
        {
            for (int j = 0; j < len; j++)
            {
                m[i][j] = 0;
            }
            if (i == 0)
            {
                m[i][0] = 2;
                m[i][1] = 1;
                
                b[0] = 2;
                c[0] = 1;
            }
            else if (i == (len - 1))
            {
                m[i][len - 2] = 1;
                m[i][len - 1] = 2;
                
                a[len-1] = 1;
                b[len-1] = 2;
            }
            else
            {
                m[i][i-1] = lam[i];
                m[i][i] = 2;
                m[i][i+1] = u[i];
                
                a[i] = lam[i];
                b[i] = 2;
                c[i] = u[i];
            }
        }

求三对角矩阵,自下而上对角线上的参数是a,b,c

当然需要得到方程组右边的值

        float g[len];
        g[0] = 3 * (y[1] - y[0])/h[0];
        g[len-1] = 3 * (y[len - 1] - y[len - 2])/h[len - 2];
        for (int i = 1; i < len - 1; i++)
        {
            g[i] = 3 * ((lam[i] * (y[i] - y[i-1])/h[i-1]) + u[i] * (y[i+1] - y[i])/h[i]);
        }

下面就是求解这个方程组了,对于三对角矩阵,使用追赶法

        //< Solve the Equations
        float p[len];
        float q[len];

        p[0] = b[0];
        for (int i = 0; i < len - 1; i++)
        {
            q[i] = c[i]/p[i];
        }

        for (int i = 1; i < len; i++)
        {
            p[i] = b[i] - a[i]*q[i-1];
        }
        
        float su[len];
        float sq[len];
        float sx[len];
        
        su[0] = c[0]/b[0];
        sq[0] = g[0]/b[0];
        for (int i = 1; i < len - 1; i++)
        {
            su[i] = c[i]/(b[i] - su[i-1]*a[i]);
        }

        for (int i = 1; i < len; i++)
        {
            sq[i] = (g[i] - sq[i-1]*a[i])/(b[i] - su[i-1]*a[i]);
        }
        
        sx[len-1] = sq[len-1];
        for (int i = len - 2; i >= 0; i--)
        {
            sx[i] = sq[i] - su[i]*sx[i+1];
        }        

求得了参数,现在就得到分段插值函数了。这种函数实际上有两个参数。
一个是所在的段位,这个是整数。另一个才是自变量。
在Objective-C里用block做一个临时的函数很是方便。

        double (^func)(int k, float vX) = ^(int k, float vX) {
            double p1 =  (ph[k] + 2.0 * (vX - px[k])) * ((vX - px[k+1]) * (vX - px[k+1])) * py[k] / (ph[k] *ph[k] * ph[k]);
            double p2 =  (ph[k] - 2 * (vX - px[k+1])) * powf((vX - px[k]), 2.0f) * py[k+1] / powf(ph[k], 3.0f);
            double p3 =  (vX - px[k]) * powf((vX - px[k+1]), 2.0f) * psx[k] / powf(ph[k], 2.0f);
            double p4 =  (vX - px[k+1]) * powf((vX - px[k]), 2.0f) * psx[k+1] / powf(ph[k], 2.0f);
            return p1 + p2 + p3 + p4;
        };

到此为止,得到了曲线的函数。当然在实际画曲线的时候,我们仍然是逐个点画,
取点的时候取得密集一些就形成曲线了。这里delta取为1。

        for (int i = 0; i < [_points count]; i++)
        {
            CGPoint pt = [[_points objectAtIndex:i] CGPointValue];
            if (i == 0)
            {
                CGPathMoveToPoint(path, NULL, pt.x, pt.y);
            }
            else
            {
                CGPoint curP = [[_points objectAtIndex:i-1] CGPointValue];
                float delta = 1.0f;
                for (float pointX = curP.x; fabs(pointX - pt.x) > 1e-5f; pointX += delta)
                {
                    float pointY = func(i-1, pointX);
                    CGPathAddLineToPoint(path, NULL, pointX, pointY);
                }
            }
        }

拟合前

拟合后

示例代码在GitHub上: Sample-CurveFit

Jon Morrow: How to Quit Your Job…

看到这类文章,总是感到很震撼,或许是因为看到了一种不同的,理想中的生活吧。
HN上的一篇文章就是这么一个励志故事。

Jon Morrow本来和我们一样,住在租来小房间里,对自己的生活不满意,总是期盼自己能有更多的时候做自己的喜欢的事情,有更多的时间陪陪家人。然后,像我们大多数人一样,他没有做什么,只是想想。直到一次车祸发生。

由于腿部14处骨折,在接下来的几个月里,Jon能做的只是忍受痛苦,还有思考自己的人生、理想和事业。他无法放弃这些,于是他辞职了。很爽快的卖掉了自己几乎所有的东西,开始写Blog。改变的过程听起来简单,但却是需要坚持的意志力。一段全身心投入事业的时间之后,Jon的事情有了起色。自己的网站每天有2000多个访问者,也应邀做了一家知名网站的编辑。

然后呢?Jon开始环视四周,考虑是不是要离开自己住的这个该死的地方。“你有没有在某一天醒来之后,发现自己其实很厌恶你住的这个地方?天气很差,邻居唠叨,你不愿意请朋友到家里来,因为它看起来一团糟”。于是,在换了工作之后,Jon离开了自己的城市。

Jon的新办公室在墨西哥的马萨特兰,度假胜地。“我在写这些的时候,我就坐在我的阳台上,看着海豚从海里跃起⋯⋯“

是不是觉得,太爽了吧!我也是这么想的 :]

但是到此为止,Jon的故事其实还不完整。

Jon患有一种严重的疾病,从颈部一下都不能行动。疾病的疗养费用是很高的,美国的医疗制度会给予Jon资助,但是做为获得资助的条件,要求Jon每月收入自己只能保留一定数量,其余上交。这当时不是很舒服的事情了,于是Jon开始自己筹钱。一开始就放弃资助是有很大风险的。当然,这个故事有个好的结局,Jon通过帮助别人,比如教人写Blog,给一些公司提供咨询等等,有了不菲的,足以应付医疗费的收入。

很酷!不是吗?

是不是该做点什么了?嗯,我想,我们还是先冷静冷静吧,励志故事总是容易让人冲动的,但是改变却不应该是一时之勇。
毕竟我们能看到的故事都是有一个好结局的,那些随便辞掉的工作而毫无建树的家伙们是不会出来给我们讲故事的。

冷静一下,如果自己内心真的渴望改变,这些改变的动力是可以积攒的。直到有一天开始行动,凭借多年以来积蓄的力量,我们才能走的更远。

UITableViewCell的背景

Saved Blog

UITableViewCell是一个很常用的View,通常我们都是直接使用它。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    static NSString *cellIdentifier = @"CellIdentifier";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell)
    {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier] autorelease];
    }
    
    cell.textLabel.text = [NSString stringWithFormat:@"Line: %d", indexPath.row];
    
    return cell;
}

得到这个效果:

现在我们给tableViewCell加上点背景色:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    static NSString *cellIdentifier = @"CellIdentifier";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell)
    {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier] autorelease];
    }
    
    cell.textLabel.text = [NSString stringWithFormat:@"Line: %d", indexPath.row];
    // cell.backgroundColor = [UIColor blueColor];
    cell.contentView.backgroundColor = [UIColor blueColor];
    return cell;
}

我们不应该直接使用cell.backgroundColor。Cell本身是一个UIView,我们所看到的部分其实只是它的一个Subview,也就是cell.contentView。所以,如果直接改变cell本身的背景色,依然会被cell.contentView给覆盖,没有效果。

不过,通过cell.contentView.backgroundColor来改变背景色还不是最好的Practice. 如果通过

tableView.editing = YES;

进入Edit模式,就会出现问题。

Cocoa提供的按钮背景色为透明。因为ContentView被移开,下面是tableView的颜色,已经不是cell的一部分了。

所以,最好的方式应该是通过cell.backgroundView来改变cell的背景。按照文档说明,backgroundView始终处于cell的最下层,所以,将cell里的其它subview背景设为[UIColor clearColor],以cell.backgroundView作为统一的背景,应该是最好的方式。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    static NSString *cellIdentifier = @"CellIdentifier";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell)
    {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier] autorelease];
    }
    
    cell.textLabel.text = [NSString stringWithFormat:@"Line: %d", indexPath.row];
    cell.textLabel.backgroundColor = [UIColor clearColor];
    
    UIView *backgrdView = [[UIView alloc] initWithFrame:cell.frame];
    backgrdView.backgroundColor = [UIColor blueColor];
    cell.backgroundView = backgrdView;
    [backgrdView release];
    
    return cell;
}

效果: