當把一些資料格式化成我們易懂的格式時,我們希望能有一種簡單而快速的方案。Foundation框架中的NSFormatter就能很好的勝任這個工作。在Mac上,AppKit已經內建了對NSFormatter的支援。
內建的格式化程式
Foundation框架中的NSFormatter只是一個抽象類,它有兩個已經實現了的子類:NSNumberFormatter、NSDateFormatter。接下來,讓我們來實現自己的子類吧。如果你還想了解更多,請移步這裡。
介紹
NSFormatter除了丟擲錯誤,其它什麼也不幹。不知道有沒有程式設計師想要做這樣的工作。
沒有人會喜歡錯誤的,我們實現一個子類,它能夠將UIColor的例項物件變成一個人們更容易看得懂的名字。例如下面的程式碼,它會返回一個“blue”的字串:
1 2 |
KPAColorFormatter *colorFormatter = [[KPAColorFormatter alloc] init]; [colorFormatter stringForObjectValue:[UIColor blueColor]] // Blue |
子類化NSFormatter需要實現stringForObjectValue:和getObjectValue:forString:errorDescription:兩個方法。第一個方法最常用,第二個方法更多是在OSX上使用。
初始化
首先,我們需要做一些設定,先預定義顏色物件和顏色名字的對應關係。下面是一個簡單的實現:
1 2 3 4 5 6 7 8 |
- (id)init; { return [self initWithColors:@{ [UIColor redColor]: @"Red", [UIColor blueColor]: @"Blue", [UIColor greenColor]: @"Green" }]; } |
UIColor的例項物件作為字典的Key, 顏色的名字作為字典的Value。我想讓讀者去實現自己的initWithColors:方法。但是你想直接獲得答案的話,在這裡可以找到。
格式化物件的值
我們的方法只能格式化UIColor例項物件,所以做的第一件事就是判斷傳入的引數是否是UIColor類
1 2 3 4 5 6 7 |
- (NSString *)stringForObjectValue:(id)value; { if (![value isKindOfClass:[UIColor class]]) { return nil; } // To be continued... } |
在判斷引數合法之後,我們的真正邏輯就要開始了。在我們的類中,有一個包含顏色名字和UIColor例項物件的字典,我們只需要用UIColor例項物件做為Key來查詢。
1 2 3 4 5 6 |
- (NSString *)stringForObjectValue:(id)value; { // Previously on KPAColorFormatter return [self.colors objectForKey:value]; } |
上面的程式碼是一個簡單的實現。一個更有用的格式化程式應該是在我們的顏色字典裡面沒有找到匹配的顏色時,返回一個最相近的顏色。我將具體實現留給讀者,如果你還是想直接獲取答案,請移步這裡。
反向格式化
我們的格式化程式也應該支援從字串格式化成例項物件 。通過getObjectValue:forString:errorDescription:方法可以實現。在OSX上,在使用NSCell時會經常用到這個方法。
NSCell有一個objectValue的屬性, 它就可以作為一個格式化程式。在用NSTextFieldCell時,使用者輸入一個字串,作為程式設計師的我們可能期望objectValue屬性的值能夠根據輸入字串變成一個UIColor的例項物件。例如,使用者輸入“Blue”,我們應該返回一個[UIColor blueColor] 的例項變數的引用。
實現反轉格式化分為兩部分:一部分是從一個字串可以格式化成UIColor例項物件,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (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 } |
上面的程式碼還可以優化,但現在先不做。我們遍歷包含顏色的字典,當找到一個我們需要的顏色的名字時,會返回一個顏色物件的引用,同時也會返回一個YES,告知呼叫者已經成功把字串變成了UIColor例項物件。
1 2 3 4 5 6 7 |
if (matchingColor) { // snap } else if (error) { *error = [NSString stringWithFormat:@"No known color for name: %@", string]; } return NO; |
如果沒有找到匹配的顏色,我們會檢測呼叫者是否需要錯誤資訊,如果需要,則返回。錯誤檢測很重要,如果你不做檢測,程式很可能崩潰。同時,我們也會返回NO告訴呼叫者,轉換失敗。
本地化
到目前為止,我們的這個NSFormatter 子類可以幫助生活在美國講英文的人,但相比全世界71.3億人口,那才3.19億。換句話說,你還有大約96%的潛在使用者。當然了,你可能認為他們中的大多數都沒有iPhone或者Mac。
NSNumberFormatter 和NSDateFormatter 都有一個locale的屬性,它是一個NSLocale類的例項物件。接下來讓我們擴充套件一下格式化程式,讓它可以根據locale屬性返回對應翻譯的名字。
翻譯
首先,我們需要做的是翻譯顏色名字字串。關於getstring命令和“*.lprojs”檔案超出了本文的範圍。可以移步到這裡閱讀相關文章。
本地化的格式化
接下來是本地化功能的實現。在獲取翻譯字串後,我們需要更新stringForObjectValue來實現翻譯。那些用過NSLocalizedString的人已經早早的將每一個字串用NSLocalizedString替換了。但是我們不會這麼做。
NSLocalizedString只會找到當前預設語言的翻譯。因為是它是系統預設的,99%的情況下能滿足你的需求,但是我們會根據我們格式化程式的locale屬性來動態查詢語言。
下面是stringForObjectValue的最新實現
1 2 3 4 5 6 7 8 9 10 |
- (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]; } |
上面的程式碼還有改進的餘地,請大家多多包涵了。如果所有的程式碼都在一起,應該會更好閱讀。
首先,我們通過locale屬性獲取對應的語言,然後通過NSBundle找到對應的語言程式碼。最後我們會讓bundle對英文名字進行翻譯。如果沒有找到英文名字對應的翻譯,則會返回英文名字。如上正是NSLocalizedString的具體實現。
本地化的反向格式化
同樣,我們也可以將一個被翻譯的顏色名字變化成顏色例項物件,當然我認為這是不值得的。我們現在的版本適用於99%的情況。另外1%的情況是在Mac上,在NSCell類上使用該格式化程式,而且你允許使用者輸入一個你試圖解析的顏色名字;那麼你可能需要一個比NSFormatter 子類更復雜的物件來處理。或許你不應該允許使用者用文字輸入顏色字串。NSColorPanel是一個更好的方案。
屬性化字串
到目前為止,我們的格式化程式都按照我們期望的那樣工作著。接下來讓我們做一個完全無用的功能。格式化程式也支援屬性化字串。當然是否要用這個功能取決於應用的設定以及應用的使用者介面。因此,你最好把這個功能做成一個配置。
我們的示例是將文字顏色設定為我們正在格式化的顏色:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (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]; } |
首先,我們像之前一樣,格式化字串,在格式化成功後,我們會將之前的顏色屬性和預設屬性合併,最後返回屬性化字串,是不是很簡單。
便利
因為初始化內建的格式化程式是很慢的,所以一種普遍的做法就是對外暴露一個便利的類方法。格式化程式應該使用相同的預設值和本地化環境。下面是我們的實現:
1 2 3 4 5 6 7 8 9 |
+ (NSString *)localizedStringFromColor:(UIColor *)color; { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ KPAColorFormatterReusableInstance = [[KPAColorFormatter alloc] init]; }); return [KPAColorFormatterReusableInstance stringForObjectValue:color]; } |
除非你的格式化器像NSNumberFormatter 和NSDateFormatter那樣變態,否則你是不需要考慮效能問題的。
總結
現在我們的顏色格式化程式能夠將UIColor例項物件轉換成人們易懂的名字。當然NSFormatter 還可以幹很多事。特別是在Mac上,因為整合了NSCell, 有更多高階功能。例如當使用者編輯時,你可以對字串做一些檢測。
我們的格式化程式還可以做更多的自定義。例如,沒有查詢到一個你需要的顏色名字時,我們會返回給你最相近的顏色名字。我們可以暴露一個布林值屬性來控制該功能。或許我們的格式化程式不是你想要的,你也可以自己定義一個。
上面所有的程式碼在都放在了Github上,我實現的程式碼也加入到了CocoaPods中。如果你的App需要此功能,可以將KPAColorFormatter放到你的Podfile檔案中。