当把一些数据格式化成我们易懂的格式时,我们希望能有一种简单而快速的方案。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文件中。