我們希望有一種快速的一次性的解決方案,可以把數(shù)據(jù)格式化為一種易讀的格式。Foundation 框架中的就有 NSFormatter
可以很好地勝任這個(gè)工作。另外,在 Mac 上,Appkit 已經(jīng)內(nèi)建了 NSFormatter
的支持。
Foundation 框架中的 NSFormatter
是一個(gè)抽象類,它有兩個(gè)已經(jīng)實(shí)現(xiàn)的子類:NSNumberFormatter
與 NSDateFormatter
?,F(xiàn)在我們先跳過(guò)這些,來(lái)實(shí)現(xiàn)我們自己的子類。
如果你想了解更多的相關(guān)知識(shí),我推薦閱讀 NSHipster。
NSFormatter
除了拋出錯(cuò)誤,其它什么事也不做。我還不知道有人想要用這個(gè),當(dāng)然如果它對(duì)你有用,就去用它吧。
因?yàn)槲覀儾幌矚g錯(cuò)誤,我們?cè)诖藢?shí)現(xiàn)一個(gè) NSFormatter
的子類,它可以把 UIColor
實(shí)例轉(zhuǎn)換成可讀的名字。例如,以下代碼可以返回字符串“Blue”:
KPAColorFormatter *colorFormatter = [[KPAColorFormatter alloc] init];
[colorFormatter stringForObjectValue:[UIColor blueColor]] // Blue
NSFormatter
的子類化有兩個(gè)方法需要實(shí)現(xiàn):stringForObjectValue:
與 getObjectValue:ForString:errorDescription:
。我們先開始介紹第一個(gè)方法,因?yàn)檫@個(gè)方法更常用。第二個(gè)方法,就我所知,經(jīng)常用于 OS X 上,并且通常不是很有用,我們將稍后介紹。
首先,我們需要做些初始化的工作。由于沒(méi)有事先定義好的字典可以把顏色映射至名字,這些工作將由我們來(lái)完成。為了簡(jiǎn)化,這些工作將在初始化方法中完成:
- (id)init;
{
return [self initWithColors:@{
[UIColor redColor]: @"Red",
[UIColor blueColor]: @"Blue",
[UIColor greenColor]: @"Green"
}];
}
這里的 colors 是一個(gè)以 UIColor
實(shí)例為鍵,英語(yǔ)名為值的字典。大家可以自行地去實(shí)現(xiàn) initWithColors:
方法。當(dāng)然你也可以自行實(shí)現(xiàn),或者直接前往 Github repo 獲得答案。
由于我們這里只可以格式化 UIColor
實(shí)例對(duì)象,于是在方法 stringForObjectValue:
中的第一件事就是判斷傳入的參數(shù)類型是否是 UIColor
類。
- (NSString *)stringForObjectValue:(id)value;
{
if (![value isKindOfClass:[UIColor class]]) {
return nil;
}
// To be continued...
}
在判斷參數(shù)合法后,我們可以實(shí)現(xiàn)真正的邏輯了。我們的格式器中包含一個(gè) UIColor
對(duì)象為鍵,顏色名為值的字典。因此,我們只需要以 UIColor
對(duì)象為鍵找到對(duì)應(yīng)的值:
- (NSString *)stringForObjectValue:(id)value;
{
// Previously on KPAColorFormatter
return [self.colors objectForKey:value];
}
以上代碼是一個(gè)盡可能簡(jiǎn)單的實(shí)現(xiàn)。一個(gè)更高級(jí)(有用)的格式器應(yīng)該是在我們的顏色字典中沒(méi)有找到匹配的顏色時(shí),返回一個(gè)最接近的顏色。大家可以自行實(shí)現(xiàn),或是你不想花費(fèi)太多功夫,可以前往 Github repo。
我們的格式器也應(yīng)該支持反向格式化,即把字符串轉(zhuǎn)成實(shí)例對(duì)象。這是通過(guò) getObjectValue:forString:errorDescription:
方法實(shí)現(xiàn)。在 OS X 上,在使用 NSCell
時(shí)會(huì)經(jīng)常用到這個(gè)方法。
NSCell
有一個(gè) objectValue
屬性。默認(rèn)情況下,NSCell
會(huì)用 objectValue
的描述,但是它也可以選擇用一個(gè)格式器。在用 NSTextFieldCell
時(shí),用戶可以輸入值,作為程序員,我們可能期望 objedctValue
可以根據(jù)根據(jù)輸入的字符串轉(zhuǎn)成一個(gè) UIColor
實(shí)例。例如,用戶如果輸入“Blue”,我們需要返回一個(gè) [UIColor blueColor]
實(shí)例的引用。
實(shí)現(xiàn)反向格式化分為兩部分:一部分為當(dāng)格式器可以成功地把字符串轉(zhuǎn)成 UIColor
實(shí)例,另一部分當(dāng)其不能成功轉(zhuǎn)換。第一部分代碼如下:
- (BOOL)getObjectValue:(out __autoreleasing id *)obj
forString:(NSString *)string
errorDescription:(out NSString *__autoreleasing *)error;
{
__block UIColor *matchingColor = nil;
[self.colors enumerateKeysAndObjectsUsingBlock:^(UIColor *color, NSString *name, BOOL *stop) {
if([name isEqualToString:string]) {
matchingColor = color;
*stop = YES;
}
}];
if (matchingColor) {
*obj = matchingColor;
return YES;
} // Snip
這里可以做一些優(yōu)化,但是我們先不去做這些。以上方法會(huì)遍歷我們顏色字典里的每一個(gè)對(duì)象 ,當(dāng)一個(gè)顏色名字找到時(shí),則會(huì)返回其對(duì)應(yīng)關(guān)聯(lián)的 UIColor
實(shí)例對(duì)象的引用,同時(shí)返回 YES 告知調(diào)用者我們已經(jīng)成功地把字符串轉(zhuǎn)成了一個(gè) UIColor
實(shí)例對(duì)象。
現(xiàn)在處理第二部分:
if (matchingColor) {
// snap
} else if (error) {
*error = [NSString stringWithFormat:@"No known color for name: %@", string];
}
return NO;
這里,我們?nèi)绻荒苷业揭粋€(gè)匹配的顏色,我們會(huì)檢測(cè)調(diào)用者是否需要錯(cuò)誤信息,如果需要,則把錯(cuò)誤通過(guò)引用返回。這里檢查錯(cuò)誤很重要。如果你不這樣做,程序就會(huì) crash。同時(shí),我們也會(huì)返回 NO,告知調(diào)用者這次轉(zhuǎn)換失敗。
到現(xiàn)在,我們已經(jīng)建立了一個(gè)完全功能的 NSFormatter
的子類,當(dāng)然這只是對(duì)于生活在美國(guó)的英語(yǔ)使用者而言有用。
但相比全世界 71.3 億人,那才 3.19 億?;蛘哒f(shuō),你還有 96% 的潛在用戶。當(dāng)然你可以說(shuō):這些潛在用戶絕大部分都不是 iPhone 或 Mac 使用者,這么做有什么意思呢?這么想你就太掃興了。
NSNumberFormatter
與 NSDateFormatter
都有一個(gè) locale 屬性,它是 NSLocale
實(shí)例對(duì)象。我們現(xiàn)在來(lái)擴(kuò)展格式器以支持本地化,讓它可以根據(jù) local 屬性來(lái)返回對(duì)應(yīng)翻譯的名字。
首先,我們需要翻譯顏色名字字符串。有關(guān) genstring 與 *.lprojs 超出了本文的范圍。有很多文章討論這點(diǎn)。好了,不需要其它工作了,快要結(jié)束了。
接下來(lái)是本地化功能的實(shí)現(xiàn)。在獲取翻譯的字符串后,我們需要更新 stringForObejectValue:
方法。以前已經(jīng)使用過(guò) NSLocalizedString
的人可能已經(jīng)早早的把每一個(gè)字符串都用 NSLocalizedString
替換了。但是我們不會(huì)這么做。
我們現(xiàn)在處理的是一個(gè)動(dòng)態(tài)的 local,而 NSLocalizedString
只會(huì)查找當(dāng)前默認(rèn)的語(yǔ)言的翻譯。在99%的情況下,這種默認(rèn)的行為是你所想要的,但是我們會(huì)用格式化器的 locale 屬性來(lái)動(dòng)態(tài)查詢語(yǔ)言。
以下是 stringForObjectValue:
的新的實(shí)現(xiàn):
- (NSString *)stringForObjectValue:(id)value;
{
// Previously on... don't you hate these? I just watched that 20 seconds ago!
NSString *languageCode = [self.locale objectForKey:NSLocaleLanguageCode];
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:languageCode
withExtension:@"lproj"];
NSBundle *languageBundle = [NSBundle bundleWithURL:bundleURL];
return [languageBundle localizedStringForKey:name value:name table:nil];
}
上面的代碼還有可以重構(gòu)改進(jìn)的地方,但因?yàn)榘汛a都放在同一個(gè)地方可以方便閱讀,所以請(qǐng)大家多多包涵了。
首先,我們通過(guò) locale 屬性查找相應(yīng)的語(yǔ)言,之后通過(guò) NSBundle 找到對(duì)應(yīng)的語(yǔ)言代碼。最后,我們會(huì)讓 bundle 對(duì)英語(yǔ)名稱進(jìn)行翻譯。如果找不到對(duì)應(yīng)的翻譯,則會(huì)返回 name: 方法的參數(shù)(即英語(yǔ)名稱)。如上即是 NSLocalizedString
的具體實(shí)現(xiàn)。
同樣,我們也可以把顏色名稱轉(zhuǎn)成 UIColor
實(shí)例對(duì)象,當(dāng)然,我認(rèn)為這樣做是不值得的。我們當(dāng)前的實(shí)現(xiàn)適用于99%的情況。另外1%的情況是在 Mac 的 NSCell
上使用,而且你允許用戶輸入一個(gè)你試圖解析的顏色的名字,這所需要做的要比簡(jiǎn)單的 子類化 NSFormatter 復(fù)雜很多?;蛟S,你不應(yīng)該允許你的用戶通過(guò)文本輸入顏色值。NSColorPanel 在這里是一個(gè)更好的解決方案。
到目前為止,我們的格式器都按我們預(yù)期的工作。接下來(lái)讓我們做一個(gè)完全沒(méi)用的功能,只是示范一下我們可以這么做,你懂的。
格式器同時(shí)支持屬性化字符串。要不要支持它取決于你特定的應(yīng)用與其用戶界面。因此,你最好把這個(gè)功能做成可配置。
以下代碼就是將文本顏色設(shè)置為當(dāng)前正在格式化的顏色:
- (NSAttributedString *)attributedStringForObjectValue:(id)value
withDefaultAttributes:(NSDictionary *)defaultAttributes;
{
NSString *string = [self stringForObjectValue:value];
if (!string) {
return nil;
}
NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithDictionary:defaultAttributes];
attributes[NSForegroundColorAttributeName] = value;
return [[NSAttributedString alloc] initWithString:string attributes:attributes];
}
首先,我們?nèi)缰耙粯犹幚碜址缓髾z查格式化是否成功。然后我們把默認(rèn)的屬性值與前面設(shè)置的顏色屬性結(jié)合后,最終返回屬性化字符串。很容易,是嗎?
因?yàn)槌跏蓟瘍?nèi)建的格式器太慢了,所以通常需要對(duì)外給你的格式器提供一個(gè)便利的類方法。這個(gè)格式器應(yīng)該用默認(rèn)值與當(dāng)前的本地化環(huán)境。以下是格式器的實(shí)現(xiàn):
+ (NSString *)localizedStringFromColor:(UIColor *)color;
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
KPAColorFormatterReusableInstance = [[KPAColorFormatter alloc] init];
});
return [KPAColorFormatterReusableInstance stringForObjectValue:color];
}
除非你的格式器像 NSNumberFormatter
與 NSDateFormatter
一樣做一些瘋狂的事情 ,你可能不需要因?yàn)樾阅軉?wèn)題這么做。但是這樣做也可以讓使用格式器簡(jiǎn)單許多。
我們的顏色格式器現(xiàn)在可以把一個(gè) UIColor
實(shí)例格式成一個(gè)可讀的名字或是反過(guò)來(lái)也行。當(dāng)然還有放多有關(guān) NSFormatter
的事情沒(méi)有涉及。特別是在 Mac 上,因?yàn)樗?NSCell
相關(guān),你可以用更多高級(jí)的特性。例如當(dāng)用戶在編輯的時(shí),你可以對(duì)字符串做一些檢測(cè)。
我們的格式器還可以做更多自定義的事情。例如,在沒(méi)查找到一個(gè)你需要的顏色名字時(shí),我們可以返回給你最相近的顏色名字。有時(shí),你可能需要我們的格式器有一個(gè) Boolean 屬性來(lái)控制該功能。或許我們的屬性化字符串的格式化不是你想要的,并且應(yīng)該支持更多自定義操作。
就此,我們完成了一個(gè)非??煽康母袷狡鳌K械拇a(伴有 OS X 示例)都放在了 Github 上, 并且你也可以在 CocoaPods 上看到。如果你應(yīng)用需要此功能,可以將 "KPAColorFormatter" 放在你的 Podfile 中,開始使用它吧。