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