处理 NSDateFormatter 区域设置“功能”的最佳方法是什么?
似乎 NSDateFormatter 有一个让你意想不到的“功能”:如果你执行一个简单的“固定”格式操作,例如:
NSDateFormatter* fmt = [[NSDateFormatter alloc] init];
[fmt setDateFormat:@"yyyyMMddHHmmss"];
NSString* dateStr = [fmt stringFromDate:someDate];
[fmt release];
那么它在美国和大多数语言环境中都可以正常工作,直到......有人他们的电话设置为 24 小时区域,将设置中的 12/24 小时开关设置为 12。然后上面开始将“AM”或“PM”添加到结果字符串的末尾。
(参见,例如 NSDateFormatter,我在做什么有问题还是这是一个错误?)
(请参阅https://developer.apple.com/library/content/qa/qa1480/_index.html)
显然苹果已经宣称这是“BAD”——破坏了设计,并且他们不会修复它。
规避显然是为特定区域(通常是美国)设置日期格式化程序的区域设置,但这有点混乱:
NSLocale *loc = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
[df setLocale: loc];
[loc release];
在 onesies-twosies 中还不错,但我正在处理大约十个不同的应用程序,第一个我看到有 43 个这种情况的实例。
那么,对于宏/重写的类/任何东西,有什么聪明的想法可以最大限度地减少更改所有内容的工作量,而不会使代码变得模糊吗? (我的第一直觉是用一个可以在 init 方法中设置区域设置的版本覆盖 NSDateFormatter。需要更改两行 - alloc/init 行和添加的导入。)
##Added
这是我想出的到目前为止——似乎适用于所有场景:
@implementation BNSDateFormatter
-(id)init {
static NSLocale* en_US_POSIX = nil;
NSDateFormatter* me = [super init];
if (en_US_POSIX == nil) {
en_US_POSIX = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
}
[me setLocale:en_US_POSIX];
return me;
}
@end
##Update 关于 OMZ 的提案,这就是我发现的 --
这是类别版本 -- h 文件:
#import <Foundation/Foundation.h>
@interface NSDateFormatter (Locale)
- (id)initWithSafeLocale;
@end
类别 m 文件:
#import "NSDateFormatter+Locale.h"
@implementation NSDateFormatter (Locale)
- (id)initWithSafeLocale {
static NSLocale* en_US_POSIX = nil;
self = [super init];
if (en_US_POSIX == nil) {
en_US_POSIX = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
}
NSLog(@"Category's locale: %@ %@", en_US_POSIX.description, [en_US_POSIX localeIdentifier]);
[self setLocale:en_US_POSIX];
return self;
}
@end
代码:
NSDateFormatter* fmt;
NSString* dateString;
NSDate* date1;
NSDate* date2;
NSDate* date3;
NSDate* date4;
fmt = [[NSDateFormatter alloc] initWithSafeLocale];
[fmt setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
dateString = [fmt stringFromDate:[NSDate date]];
NSLog(@"dateString = %@", dateString);
date1 = [fmt dateFromString:@"2001-05-05 12:34:56"];
NSLog(@"date1 = %@", date1.description);
date2 = [fmt dateFromString:@"2001-05-05 22:34:56"];
NSLog(@"date2 = %@", date2.description);
date3 = [fmt dateFromString:@"2001-05-05 12:34:56PM"];
NSLog(@"date3 = %@", date3.description);
date4 = [fmt dateFromString:@"2001-05-05 12:34:56 PM"];
NSLog(@"date4 = %@", date4.description);
[fmt release];
fmt = [[BNSDateFormatter alloc] init];
[fmt setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
dateString = [fmt stringFromDate:[NSDate date]];
NSLog(@"dateString = %@", dateString);
date1 = [fmt dateFromString:@"2001-05-05 12:34:56"];
NSLog(@"date1 = %@", date1.description);
date2 = [fmt dateFromString:@"2001-05-05 22:34:56"];
NSLog(@"date2 = %@", date2.description);
date3 = [fmt dateFromString:@"2001-05-05 12:34:56PM"];
NSLog(@"date3 = %@", date3.description);
date4 = [fmt dateFromString:@"2001-05-05 12:34:56 PM"];
NSLog(@"date4 = %@", date4.description);
[fmt release];
结果:
2011-07-11 17:44:43.243 DemoApp[160:307] Category's locale: <__NSCFLocale: 0x11a820> en_US_POSIX
2011-07-11 17:44:43.257 DemoApp[160:307] dateString = 2011-07-11 05:44:43 PM
2011-07-11 17:44:43.264 DemoApp[160:307] date1 = (null)
2011-07-11 17:44:43.272 DemoApp[160:307] date2 = (null)
2011-07-11 17:44:43.280 DemoApp[160:307] date3 = (null)
2011-07-11 17:44:43.298 DemoApp[160:307] date4 = 2001-05-05 05:34:56 PM +0000
2011-07-11 17:44:43.311 DemoApp[160:307] Extended class's locale: <__NSCFLocale: 0x11a820> en_US_POSIX
2011-07-11 17:44:43.336 DemoApp[160:307] dateString = 2011-07-11 17:44:43
2011-07-11 17:44:43.352 DemoApp[160:307] date1 = 2001-05-05 05:34:56 PM +0000
2011-07-11 17:44:43.369 DemoApp[160:307] date2 = 2001-05-06 03:34:56 AM +0000
2011-07-11 17:44:43.380 DemoApp[160:307] date3 = (null)
2011-07-11 17:44:43.392 DemoApp[160:307] date4 = (null)
电话 [将其设为 iPod Touch] 设置为英国, 12/24开关设置为12。两次结果有明显差异,我判断类别版本错误。请注意,类别版本中的日志正在执行(并且命中了代码中放置的停止点),因此这不仅仅是代码未得到使用的简单情况。
##一个奇怪的观察
稍微修改了类别实现:
#import "NSDateFormatter+Locale.h"
@implementation NSDateFormatter (Locale)
- (id)initWithSafeLocale {
static NSLocale* en_US_POSIX2 = nil;
self = [super init];
if (en_US_POSIX2 == nil) {
en_US_POSIX2 = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
}
NSLog(@"Category's locale: %@ %@", en_US_POSIX2.description, [en_US_POSIX2 localeIdentifier]);
[self setLocale:en_US_POSIX2];
NSLog(@"Category's object: %@ and object's locale: %@ %@", self.description, self.locale.description, [self.locale localeIdentifier]);
return self;
}
@end
基本上只是更改了静态区域设置变量的名称(以防与子类中声明的静态变量发生冲突)并添加了额外的 NSLog。但是看看 NSLog 打印的内容:
2011-07-15 16:35:24.322 DemoApp[214:307] Category's locale: <__NSCFLocale: 0x160550> en_US_POSIX
2011-07-15 16:35:24.338 DemoApp[214:307] Category's object: <NSDateFormatter: 0x160d90> and object's locale: <__NSCFLocale: 0x12be70> en_GB
2011-07-15 16:35:24.345 DemoApp[214:307] dateString = 2011-07-15 04:35:24 PM
2011-07-15 16:35:24.370 DemoApp[214:307] date1 = (null)
2011-07-15 16:35:24.378 DemoApp[214:307] date2 = (null)
2011-07-15 16:35:24.390 DemoApp[214:307] date3 = (null)
2011-07-15 16:35:24.404 DemoApp[214:307] date4 = 2001-05-05 05:34:56 PM +0000
如您所见,setLocale 根本没有打印。格式化程序的区域设置仍然是 en_GB。看起来类别中的 init 方法有些“奇怪”。
It seems that NSDateFormatter
has a "feature" that bites you unexpectedly: If you do a simple "fixed" format operation such as:
NSDateFormatter* fmt = [[NSDateFormatter alloc] init];
[fmt setDateFormat:@"yyyyMMddHHmmss"];
NSString* dateStr = [fmt stringFromDate:someDate];
[fmt release];
Then it works fine in the US and most locales UNTIL ... someone with their phone set to a 24-hour region sets the 12/24 hour switch in settings to 12. Then the above starts tacking "AM" or "PM" onto the end of the resulting string.
(See, eg, NSDateFormatter, am I doing something wrong or is this a bug?)
(And see https://developer.apple.com/library/content/qa/qa1480/_index.html)
Apparently Apple has declared this to be "BAD" -- Broken As Designed, and they aren't going to fix it.
The circumvention is apparently to set the locale of the date formatter for a specific region, generally the US, but this is a bit messy:
NSLocale *loc = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
[df setLocale: loc];
[loc release];
Not too bad in onesies-twosies, but I'm dealing with about ten different apps, and the first one I look at has 43 instances of this scenario.
So any clever ideas for a macro/overridden class/whatever to minimize the effort to change everything, without making the code to obscure? (My first instinct is to override NSDateFormatter with a version that would set the locale in the init method. Requires changing two lines -- the alloc/init line and the added import.)
##Added
This is what I've come up with so far -- seems to work in all scenarios:
@implementation BNSDateFormatter
-(id)init {
static NSLocale* en_US_POSIX = nil;
NSDateFormatter* me = [super init];
if (en_US_POSIX == nil) {
en_US_POSIX = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
}
[me setLocale:en_US_POSIX];
return me;
}
@end
##Update
Re OMZ's proposal, here is what I'm finding --
Here is the category version -- h file:
#import <Foundation/Foundation.h>
@interface NSDateFormatter (Locale)
- (id)initWithSafeLocale;
@end
Category m file:
#import "NSDateFormatter+Locale.h"
@implementation NSDateFormatter (Locale)
- (id)initWithSafeLocale {
static NSLocale* en_US_POSIX = nil;
self = [super init];
if (en_US_POSIX == nil) {
en_US_POSIX = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
}
NSLog(@"Category's locale: %@ %@", en_US_POSIX.description, [en_US_POSIX localeIdentifier]);
[self setLocale:en_US_POSIX];
return self;
}
@end
The code:
NSDateFormatter* fmt;
NSString* dateString;
NSDate* date1;
NSDate* date2;
NSDate* date3;
NSDate* date4;
fmt = [[NSDateFormatter alloc] initWithSafeLocale];
[fmt setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
dateString = [fmt stringFromDate:[NSDate date]];
NSLog(@"dateString = %@", dateString);
date1 = [fmt dateFromString:@"2001-05-05 12:34:56"];
NSLog(@"date1 = %@", date1.description);
date2 = [fmt dateFromString:@"2001-05-05 22:34:56"];
NSLog(@"date2 = %@", date2.description);
date3 = [fmt dateFromString:@"2001-05-05 12:34:56PM"];
NSLog(@"date3 = %@", date3.description);
date4 = [fmt dateFromString:@"2001-05-05 12:34:56 PM"];
NSLog(@"date4 = %@", date4.description);
[fmt release];
fmt = [[BNSDateFormatter alloc] init];
[fmt setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
dateString = [fmt stringFromDate:[NSDate date]];
NSLog(@"dateString = %@", dateString);
date1 = [fmt dateFromString:@"2001-05-05 12:34:56"];
NSLog(@"date1 = %@", date1.description);
date2 = [fmt dateFromString:@"2001-05-05 22:34:56"];
NSLog(@"date2 = %@", date2.description);
date3 = [fmt dateFromString:@"2001-05-05 12:34:56PM"];
NSLog(@"date3 = %@", date3.description);
date4 = [fmt dateFromString:@"2001-05-05 12:34:56 PM"];
NSLog(@"date4 = %@", date4.description);
[fmt release];
The result:
2011-07-11 17:44:43.243 DemoApp[160:307] Category's locale: <__NSCFLocale: 0x11a820> en_US_POSIX
2011-07-11 17:44:43.257 DemoApp[160:307] dateString = 2011-07-11 05:44:43 PM
2011-07-11 17:44:43.264 DemoApp[160:307] date1 = (null)
2011-07-11 17:44:43.272 DemoApp[160:307] date2 = (null)
2011-07-11 17:44:43.280 DemoApp[160:307] date3 = (null)
2011-07-11 17:44:43.298 DemoApp[160:307] date4 = 2001-05-05 05:34:56 PM +0000
2011-07-11 17:44:43.311 DemoApp[160:307] Extended class's locale: <__NSCFLocale: 0x11a820> en_US_POSIX
2011-07-11 17:44:43.336 DemoApp[160:307] dateString = 2011-07-11 17:44:43
2011-07-11 17:44:43.352 DemoApp[160:307] date1 = 2001-05-05 05:34:56 PM +0000
2011-07-11 17:44:43.369 DemoApp[160:307] date2 = 2001-05-06 03:34:56 AM +0000
2011-07-11 17:44:43.380 DemoApp[160:307] date3 = (null)
2011-07-11 17:44:43.392 DemoApp[160:307] date4 = (null)
The phone [make that an iPod Touch] is set to Great Britain, with the 12/24 switch set to 12. There's a clear difference in the two results, and I judge the category version to be wrong. Note that the log in the category version IS getting executed (and stops placed in the code are hit), so it's not simply a case of the code somehow not getting used.
##A curious observation
Modified the category implementation slightly:
#import "NSDateFormatter+Locale.h"
@implementation NSDateFormatter (Locale)
- (id)initWithSafeLocale {
static NSLocale* en_US_POSIX2 = nil;
self = [super init];
if (en_US_POSIX2 == nil) {
en_US_POSIX2 = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
}
NSLog(@"Category's locale: %@ %@", en_US_POSIX2.description, [en_US_POSIX2 localeIdentifier]);
[self setLocale:en_US_POSIX2];
NSLog(@"Category's object: %@ and object's locale: %@ %@", self.description, self.locale.description, [self.locale localeIdentifier]);
return self;
}
@end
Basically just changed the name of the static locale variable (in case there was some conflict with the static declared in the subclass) and added the extra NSLog. But look what that NSLog prints:
2011-07-15 16:35:24.322 DemoApp[214:307] Category's locale: <__NSCFLocale: 0x160550> en_US_POSIX
2011-07-15 16:35:24.338 DemoApp[214:307] Category's object: <NSDateFormatter: 0x160d90> and object's locale: <__NSCFLocale: 0x12be70> en_GB
2011-07-15 16:35:24.345 DemoApp[214:307] dateString = 2011-07-15 04:35:24 PM
2011-07-15 16:35:24.370 DemoApp[214:307] date1 = (null)
2011-07-15 16:35:24.378 DemoApp[214:307] date2 = (null)
2011-07-15 16:35:24.390 DemoApp[214:307] date3 = (null)
2011-07-15 16:35:24.404 DemoApp[214:307] date4 = 2001-05-05 05:34:56 PM +0000
As you can see, the setLocale simply didn't. The locale of the formatter is still en_GB. It appears that there is something "strange" about an init method in a category.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
呃!!
有时你会“啊哈!!”有时候,这更像是“呃!!”这是后者。在
initWithSafeLocale
类别中,“super”init
被编码为self = [super init];
。这会初始化NSDateFormatter
的 SUPERCLASS,但不会init
NSDateFormatter
对象本身。显然,当跳过此初始化时,
setLocale
“反弹”,可能是因为对象中缺少某些数据结构。将init
更改为self = [self init];
会导致发生NSDateFormatter
初始化,并且setLocale
很高兴再次。这是该类别的 .m 的“最终”来源:
Duh!!
Sometimes you have an "Aha!!" moment, sometimes it's more of a "Duh!!" This is the latter. In the category for
initWithSafeLocale
the "super"init
was coded asself = [super init];
. This inits the SUPERCLASS ofNSDateFormatter
but does notinit
theNSDateFormatter
object itself.Apparently when this initialization is skipped,
setLocale
"bounces off", presumably because of some missing data structure in the object. Changing theinit
toself = [self init];
causes theNSDateFormatter
initialization to occur, andsetLocale
is happy again.Here is the "final" source for the category's .m:
您可以创建一个带有附加初始化程序的
NSDateFormatter
类别,而不是子类化,该初始化程序负责分配区域设置以及可能的格式字符串,因此您在初始化后将拥有一个随时可用的格式化程序它。然后,您可以在代码中的任何位置使用
NSDateFormatter
,只需:您可能希望以某种方式为类别方法添加前缀以避免名称冲突,以防 Apple 决定在未来版本的操作系统中添加此类方法。
如果您始终使用相同的日期格式,您还可以添加返回具有某些配置的单例实例的类别方法(例如
+sharedRFC3339DateFormatter
)。但请注意,NSDateFormatter
不是线程安全的,当您从多个线程使用同一实例时,您必须使用锁或@synchronized
块。Instead of subclassing, you could create an
NSDateFormatter
category with an additional initializer that takes care of assigning the locale and possibly also a format string, so you'd have a ready-to-use formatter right after initializing it.Then you could use
NSDateFormatter
anywhere in your code with just:You might want to prefix your category method somehow to avoid name conflicts, just in case Apple decides to add such a method in a future version of the OS.
In case you're always using the same date format(s), you could also add category methods that return singleton instances with certain configurations (something like
+sharedRFC3339DateFormatter
). Be aware however thatNSDateFormatter
is not thread-safe and you have to use locks or@synchronized
blocks when you're using the same instance from multiple threads.我可以建议一些完全不同的东西吗,因为说实话,所有这些都有点像是掉进了兔子洞。
您应该使用一个设置了
dateFormat
的NSDateFormatter
并将locale
强制设置为en_US_POSIX
来接收日期(来自服务器/API) )。然后,您应该为 UI 使用不同的
NSDateFormatter
,您将设置timeStyle
/dateStyle
属性 - 这样您就不会拥有您自己显式设置了dateFormat
,从而错误地假设将使用该格式。这意味着 UI 是由用户偏好驱动的(上午/下午与 24 小时,以及根据用户选择正确格式化的日期字符串 - 来自 iOS 设置),而“进入”应用程序的日期始终会被正确“解析”为 < code>NSDate 供您使用。
May I suggest something totally different because to be honest all of this is somewhat running down a rabbit hole.
You should be using one
NSDateFormatter
withdateFormat
set andlocale
forced toen_US_POSIX
for receiving dates (from servers/APIs).Then you should be using a different
NSDateFormatter
for the UI which you will set thetimeStyle
/dateStyle
properties - this way you're not having an explicitdateFormat
set by yourself, thus falsely assuming that format will be used.This means UI is driven by user preferences (am/pm vs 24 hour, and date strings formatted correctly to user choice - from iOS settings), whereas dates that are "coming into" your app are always being "parsed" correctly to an
NSDate
for you to use.这是 swift 版本中该问题的解决方案。在 swift 中,我们可以使用扩展而不是类别。
所以,在这里我创建了 DateFormatter 的扩展,并在 initWithSafeLocale 内部创建了扩展
返回具有相关区域设置的 DateFormatter,在我们的例子中是 en_US_POSIX,除此之外还提供了几种日期形成方法。
斯威夫特4
使用说明:
Here is the solution for that problem in the swift version. In swift we can use extension instead of category.
So, Here I have created the extension for the DateFormatter and inside that initWithSafeLocale
returns the DateFormatter with the relevant Locale, Here in our case that is en_US_POSIX, Apart from that also provided couple of date formation methods.
Swift 4
usage description: