iOS 时间字符串format跟随系统语言地区

前言:

公司App做了个时间的改动。以前显示出来的时间使我们自己format出来的,也就是说,时间样式是我们自己定义的。比如,年份在月份前面,月份在日子前面。但是不同国家地区的人,他们有自己的日期显示习惯,

有的是:日/月/年

有的是:月/日/年

有的是:年-月-日

有的是:年.月.日.

各种顺序分隔符都有。为了符合当地人习惯,我们不再指定显示格式,而是根据手机系统的格式来显示。

需求:在App里显示的时间样式,根据用户的系统样式即可。也就是说,系统把时间显示成什么样,我们也显示成什么样。但是我们有选择地显示,有些地方只要 年月日,有些地方要 年月日时分秒,有些地方只要 时分。样式仍旧按照系统的来。

代码地址:https://gitee.com/yuency/Autolayout
示例代码类名 【LocalizeDateToPhoneSystemViewController】

部分示例截图:


示例.png

直接上结论代码,Command + C,V

func localizeDate(date: Date, destinationTimeZone: TimeZone? = TimeZone.current, destinationTemplate: DateTemplate) -> String {
    guard let destinationTimeZone = destinationTimeZone else {
        return "时区错误"
    }
    let formatter = DateFormatter()
    
    var formatTemplate = destinationTemplate.getTemplate()
    
    //对于一个没有国际化的App, 获取手机当前是 12小时制 还是 24小时制 "locale:" 参数一定要使用 Locale(identifier: "en_US_POSIX")
    if let aH = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale(identifier: "en_US_POSIX")),
       formatTemplate.contains("HH"),
       aH.contains("a") {
        formatTemplate = formatTemplate.replacingOccurrences(of: "HH", with: "hh")
    }
    
    //对于一个没有国际化的App, 经过实验, 用Locale.current这个方法拿到的语言地区不正确, 比如我设置的是 "简体中文-法国", Locale.current拿到的却是上一次设置的"英语-法国"
    //所以在这里使用 Locale.preferredLanguages 来拿到系统设置里的 "首选语言顺序"
    let locale = Locale(identifier: Locale.preferredLanguages.first ?? "en_US")
    
    //用下面这个方法就可获得根据系统语言地区得到的格式化样式
    //fromTemplate: 传一个临时的样式,告诉DateFormatter你需要哪些时间字段, 如: "yyyyMMdd", "MMddHHss"等, 无需携带格式.
    formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: formatTemplate, options: 0, locale: locale)
    
    formatter.timeZone = destinationTimeZone
    formatter.calendar = Calendar(identifier: .gregorian)
    //注意, 这里就不能写成 Locale(identifier: "en_US_POSIX")啦. 因为手机调整到 12/24小时制的时候, 在中文显示情况下, "AM/PM" 不会转换成 "上午/下午"
    //所以, 这里就和template一样,写成相同的语言地区就可以了
    formatter.locale = locale
    let dateString = formatter.string(from: date)
    return dateString
}

输出打印示例:
20210929134455 ->  13:44:55
20210929134455 ->  29/09/2021
20210929134455 ->  29/09/2021 13:44
20210929134455 ->  29/09/2021 13:44:55

2021-09-29 13:44:56 ->  13:44:56
2021-09-29 13:44:56 ->  29/09/2021
2021-09-29 13:44:56 ->  29/09/2021 13:44
2021-09-29 13:44:56 ->  29/09/2021 13:44:56

2021-09-29T13:44:57Z ->  13:44:57
2021-09-29T13:44:57Z ->  29/09/2021
2021-09-29T13:44:57Z ->  29/09/2021 13:44
2021-09-29T13:44:57Z ->  29/09/2021 13:44:57

2022-09-29 07:44:53 +0000 ->  15:44:53
2022-09-29 07:44:53 +0000 ->  29/09/2022
2022-09-29 07:44:53 +0000 ->  29/09/2022 15:44
2022-09-29 07:44:53 +0000 ->  29/09/2022 15:44:53

完整的测试代码如下:

import UIKit

class LocalizeDateToPhoneSystemViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.white
        
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 16)
        label.backgroundColor = .yellow
        label.textAlignment = .left
        label.numberOfLines = 0
        view.addSubview(label)
        label.snp.makeConstraints { make in
            make.left.equalTo(10)
            make.right.equalTo(-10)
            make.centerY.equalToSuperview()
        }
        
        label.text = """
                    不同语言地区组合时间格式化示例 (24小时制)
                    
                    zh-Hans_GL, 简体中文_格林兰
                    2021年09月29日 13:44:55
                    
                    en_GL, 英语_格林兰
                    2022-09-29, 14:28:42
                    
                    zh-Hans_FR, 简体中文_法国
                    29/09/2022 14:13:17
                    
                    en_FR, 英语_法国
                    29/09/2022 at 14:14:09
                    
                    zh-Hans_HR, 简体中文_克罗地亚
                    29. 09. 2022. 14:15:38
                    
                    en_HR, 英语_克罗地亚
                    29.09.2022., 14:20:32
                    
                    zh-Hans_MX, 简体中文_墨西哥
                    29/09/2022 14:24:41
                    
                    zh-Hans_MV, 简体中文_马尔代夫
                    2022年09月29日 14:25:43
                    
                    en_MV, 英语_马尔代夫
                    29-09-2022 14:26:18
                    
                    fi_FI, 芬兰语_芬兰
                    29.09.2022 klo 15.21.18
                                        
                    """
        //Locale.current 由两个部分组成, 为: "语言_地区" 用下划线分开语言和地区
        //Locale.current 这个取系统语言不是很准确,有时候把语言更换了,但是这里仍然没有改变,地区倒能改变
        print("当前的语言地区: \(Locale.current), 当前的时区: \(TimeZone.current)")
        print("用首选项 \(Locale.preferredLanguages)")
        
        
        print("\n\n")
        var dateString = "20210929134455"
        print("\(dateString) ->  \(localizeDate(dateString: dateString, dateFormat: .simple, destinationTemplate: .HHmmss))")
        print("\(dateString) ->  \(localizeDate(dateString: dateString, dateFormat: .simple, destinationTemplate: .yyyyMMdd))")
        print("\(dateString) ->  \(localizeDate(dateString: dateString, dateFormat: .simple, destinationTemplate: .yyyyMMddHHmm))")
        print("\(dateString) ->  \(localizeDate(dateString: dateString, dateFormat: .simple, destinationTemplate: .yyyyMMddHHmmss))")
        
        print("\n\n")
        dateString = "2021-09-29 13:44:56"
        print("\(dateString) ->  \(localizeDate(dateString: dateString, dateFormat: .normal, destinationTemplate: .HHmmss))")
        print("\(dateString) ->  \(localizeDate(dateString: dateString, dateFormat: .normal, destinationTemplate: .yyyyMMdd))")
        print("\(dateString) ->  \(localizeDate(dateString: dateString, dateFormat: .normal, destinationTemplate: .yyyyMMddHHmm))")
        print("\(dateString) ->  \(localizeDate(dateString: dateString, dateFormat: .normal, destinationTemplate: .yyyyMMddHHmmss))")
        
        print("\n\n")
        dateString = "2021-09-29T13:44:57Z"
        print("\(dateString) ->  \(localizeDate(dateString: dateString, dateFormat: .utc, destinationTemplate: .HHmmss))")
        print("\(dateString) ->  \(localizeDate(dateString: dateString, dateFormat: .utc, destinationTemplate: .yyyyMMdd))")
        print("\(dateString) ->  \(localizeDate(dateString: dateString, dateFormat: .utc, destinationTemplate: .yyyyMMddHHmm))")
        print("\(dateString) ->  \(localizeDate(dateString: dateString, dateFormat: .utc, destinationTemplate: .yyyyMMddHHmmss))")
        
        print("\n\n")
        let date = Date()
        print("\(date) ->  \(localizeDate(date: date, destinationTemplate: .HHmmss))")
        print("\(date) ->  \(localizeDate(date: date, destinationTemplate: .yyyyMMdd))")
        print("\(date) ->  \(localizeDate(date: date, destinationTemplate: .yyyyMMddHHmm))")
        print("\(date) ->  \(localizeDate(date: date, destinationTemplate: .yyyyMMddHHmmss))")
        
        // tongjigezhogngeshi()
    }
    
    /// 重要!
    func localizeDate(date: Date, destinationTimeZone: TimeZone? = TimeZone.current, destinationTemplate: DateTemplate) -> String {
        guard let destinationTimeZone = destinationTimeZone else {
            return "时区错误"
        }
        let formatter = DateFormatter()
        
        var formatTemplate = destinationTemplate.getTemplate()
        
        //对于一个没有国际化的App, 获取手机当前是 12小时制 还是 24小时制 "locale:" 参数一定要使用 Locale(identifier: "en_US_POSIX")
        if let aH = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale(identifier: "en_US_POSIX")),
           formatTemplate.contains("HH"),
           aH.contains("a") {
            formatTemplate = formatTemplate.replacingOccurrences(of: "HH", with: "hh")
        }
        
        //对于一个没有国际化的App, 经过实验, 用Locale.current这个方法拿到的语言地区不正确, 比如我设置的是 "简体中文-法国", Locale.current拿到的却是上一次设置的"英语-法国"
        //所以在这里使用 Locale.preferredLanguages 来拿到系统设置里的 "首选语言顺序"
        let locale = Locale(identifier: Locale.preferredLanguages.first ?? "en_US")
        
        //用下面这个方法就可获得根据系统语言地区得到的格式化样式
        //fromTemplate: 传一个临时的样式,告诉DateFormatter你需要哪些时间字段, 如: "yyyyMMdd", "MMddHHss"等, 无需携带格式.
        formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: formatTemplate, options: 0, locale: locale)
        
        formatter.timeZone = destinationTimeZone
        formatter.calendar = Calendar(identifier: .gregorian)
        //注意, 这里就不能写成 Locale(identifier: "en_US_POSIX")啦. 因为手机调整到 12/24小时制的时候, 在中文显示情况下, "AM/PM" 不会转换成 "上午/下午"
        //所以, 这里就和template一样,写成相同的语言地区就可以了
        formatter.locale = locale
        let dateString = formatter.string(from: date)
        return dateString
    }
    
    /// 重要!
    func localizeDate(dateString: String, dateFormat: DateFormat, destinationTimeZone: TimeZone? = TimeZone.current, destinationTemplate: DateTemplate) -> String {
        guard let destinationTimeZone = destinationTimeZone  else {
            return "时区错误"
        }
        let formatter = DateFormatter()
        formatter.dateFormat = dateFormat.getFormat()
        
        //1, 格式化为0时区, 针对服务器返回的时间统一处理为0时区, 那么可以写成GMT, 后面再看
        //   formatter.timeZone = TimeZone(abbreviation: "GMT")
        
        //2, 这里是不管啥时间串,直接格式化为东八区 (拿着手机跑到美国,看到的依然是东八区)
        //   formatter.timeZone = TimeZone(secondsFromGMT: 8)
        
        //3, 如果对时间字符串没有什么要求,可以按照当前时区格式化, 东八就东八, 东十就东十, 取决于手机
        formatter.timeZone = TimeZone.current
        
        formatter.calendar = Calendar(identifier: .gregorian)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        guard let gmtDate = formatter.date(from: dateString) else {
            return "格式化错误, 时间串:\(dateString), 格式串:\(formatter.dateFormat ?? "")"
        }
        let convertDateString = localizeDate(date: gmtDate, destinationTimeZone: destinationTimeZone, destinationTemplate: destinationTemplate)
        return convertDateString
    }
    
    /// 额外研究
    func qitajieshao() {
        //语言和地区,会互相影响最终的时间格式.
        //如果是一个有国际化的APP,那么有可能出现其他的形式,如,月份使用了英文 February 或者 中文 月
        //要让时间格式只收到地区影响,就要在 DateFormatter.dateFormat(fromTemplate: formatTemplate, options: 0, locale: NSLocale.current) 这一句里指定地区
        print("\n\n")
        let diqu = NSLocale.current.identifier.components(separatedBy: "_").last ?? "" //通过分隔符把最后的地域拿到
        let yuyan = "en"
        let yuyandiqu = "\(yuyan)_\(diqu)" //就硬再前面拼上语言,合成一个完整的 语言地域串
        //这个 `fromTemplate` 是可以带上格式的,但是会被忽略掉
        let fixedFormat = DateFormatter.dateFormat(fromTemplate: "yyyy MM dd HH mm ss", options: 0, locale: Locale(identifier: yuyandiqu))
        print("硬凑的语言地区: \(yuyandiqu),  修正后的格式: \(fixedFormat ?? "错误")")
    }
    
    /// 额外研究
    func tongjigezhogngeshi() {
        
        print("\n\n")
        print("")
        print("\n\n")
        
        var dic = [String: [String]]()
        
        //带有下划线的,认为是语言+地区的组合, 实际上对于下面的功能来说, 过滤与否, 结果条数都一样, 59条格式
        //var yuyandiquArray = Locale.availableIdentifiers.filter { string in
        //    string.contains("_")
        //}
        
        let yuyandiquArray = Locale.availableIdentifiers
        
        for s in yuyandiquArray {
            let fixedFormat = DateFormatter.dateFormat(fromTemplate: "yyyy MM dd HH mm ss", options: 0, locale: Locale(identifier: s)) ?? "错误"
            if dic.keys.contains(fixedFormat) == false {
                dic[fixedFormat] = [String]()
            }
            dic[fixedFormat]?.append(s)
        }
        
        print("取得格式化结果 \(dic.keys.count) 条")
        
        for (key, array) in dic {
            print("\n")
            print("所用格式: \(key)")
            print("所在地区: \(array)")
        }
        
        print("\n\n")
        let sortFormat = dic.keys.sorted { a, b in
            a.count > b.count
        }
        print("格式化长短排序 \(sortFormat)") //马其顿稳居第一
    }
}

enum DateTemplate: String {
    case HHmmss
    case yyyyMMdd
    case yyyyMMddHHmm
    case yyyyMMddHHmmss
    
    func getTemplate() -> String {
        switch self {
        case .HHmmss:
            return "HHmmss"
        case .yyyyMMdd:
            return "yyyyMMdd"
        case .yyyyMMddHHmm:
            return "yyyyMMddHHmm"
        case .yyyyMMddHHmmss:
            return "yyyyMMddHHmmss"
        }
    }
}

enum DateFormat: String {
    case normal
    case utc
    case simple
    
    func getFormat() -> String {
        switch self {
        case .normal:
            return "yyyy-MM-dd HH:mm:ss"
        case .utc:
            return "yyyy-MM-dd'T'HH:mm:ss'Z'"
        case .simple:
            return "yyyyMMddHHmmss"
        }
    }
}

感谢以下iOS玩家的文章:
iOS15.4 NSDateformatter 12小时制日期格式问题及解决
iOS DateFormatter dateFormat fromTemplate 中允许的格式说明符是什么?
苹果官方
swift DateFormatter

结语

又是这么一个小小的时间。

你可能感兴趣的:(iOS 时间字符串format跟随系统语言地区)