Swift日期处理(Date、Calendar、DateComponents、DateFormatter、Locale、TimeZone)

所属分类:ios | 发布于 2022-12-01 09:08:32

最近做的东西要用到周日历,发现SwiftUI里面没有现成的周日历可以选择,然后又在油管发现一个大神自己写了月日历的界面,给了我思路,然后就想着自己实现一个周日历,像日历这种,肯定和时间和日期密切相关,所以又得学习时间和日期相关的知识。

打开文档学了之后才发现,Swift的时间和日期还是挺复杂的,想用的东西都得自己封装实现,这就要求对Swfit时间和日期处理这块的基础知识相当扎实才行,最开始看了很多文档都一头雾水,知道看到了某个大神写的一篇Swfit日期处理的文章,才茅塞顿开,因为它上面的一张配图,让我对Swift时间日期的处理有了整体的思路,有了思路之后,剩下的就是去理解和实践。

大神的那张图,我改进了一下,自己制作了一张,如下:

Swift日期处理主要要用到三个类型(String、Date、DeteComponents),而这三个类型转换又要用到两个类型作为转换桥梁(DateFormatter、Calendar)。下面简单描述下这三个类型和两个转换桥梁:

三个类型

1、String:基础库的String类型,主要用来显示。

2、Date:Swift提供的日期和时间操作类型,Swift把日期和时间都封装到了Date类型里面。

3、DateComponents:包含时间组成成分描述的类型。

两个转换桥梁

1、DateFormatter:用做Date和String相互转换。

2、Calendar:用做Date和DateComponents相互转换。

 

换种思路,从DateFromatter和Calendar这两个转换桥梁开始学习

DateFormatter(日期格式化)

DateFormatter对象默认地区为设备所在地区,默认格式为".none"

1、DateFormatter

DateFormatte的主要方法

// 日期转换成字符串
open func string(from date: Date) -> String
// 字符串转换成日期
open func date(from string: String) -> Date?
// 按照Style将日期本地化显示
open class func localizedString(from date: Date, dateStyle dstyle: DateFormatter.Style, timeStyle tstyle: DateFormatter.Style) -> String
open class func dateFormat(fromTemplate tmplate: String, options opts: Int, locale: Locale?) -> String?

DateFormatter的主要属性

open var dateFormat: String!
open var dateStyle: DateFormatter.Style
open var timeStyle: DateFormatter.Style
open var locale: Locale!
open var generatesCalendarDates: Bool
open var formatterBehavior: DateFormatter.Behavior
open var timeZone: TimeZone!
open var calendar: Calendar!
open var isLenient: Bool
open var twoDigitStartDate: Date?
open var defaultDate: Date?
open var eraSymbols: [String]!
open var monthSymbols: [String]!
open var shortMonthSymbols: [String]!
open var weekdaySymbols: [String]!
open var shortWeekdaySymbols: [String]!
open var amSymbol: String!
open var pmSymbol: String!

DateFormatter.Style的定义

extension DateFormatter {
    public enum Style : UInt, @unchecked Sendable {
        case none = 0
        case short = 1
        case medium = 2
        case long = 3
        case full = 4
    }
}

2、Locale

Locale封装了有关语言、文化、技术惯例和标准的信息。Locale封装的信息示例包括用于数字的小数点、分隔符和日期格式化符号等。尽管可以使用多种语言环境,但通常使用与当前用户关联的语言环境。

3、Date

 

4、DateInterval

DateInterval以[startDate,endDate]的形式表示日期间隔的闭区间。起始日期和结束日期可以相同,此时持续时间为0。DateInterval不支持反向,即开始日期晚于结束日期。

通常调用Calendar的dateInterval获取日期间隔。

5、Date转String:dateFormatter.string(from date: Date)

Date转String有两种方式,要么设置dateFormat属性,要么设置dateStyle或timeStyle,因为DateFormatter的默认格式为"空",即".none"

5.1、设置dateFormatter属性进行转换

let df1 = DateFormatter()
df1.dateFormat = "yyyy-MM-dd HH:mm:ss"
let dateStr1 = df1.string(from: Date.now)
print("\(dateStr1)")
// 2022-12-01 10:26:34

5.2、设置dateStyle或timeStyle属性进行转换

let df2 = DateFormatter()
df2.dateStyle = DateFormatter.Style.long
df2.timeStyle = DateFormatter.Style.short
let dateStr2 = df2.string(from: Date.now)
print("\(dateStr2)")
// December 1, 2022 at 10:32 AM

5.3、手动设置地区

let df3 = DateFormatter()
df3.dateStyle = DateFormatter.Style.long
df3.timeStyle = DateFormatter.Style.short
df3.locale = Locale(identifier: "zh_Hans_CN")
let dateStr3 = df3.string(from: Date.now)
print("currentLocale: \(Locale.current)", "\(dateStr3)")
// currentLocale: en_US (current) 2022年12月1日 10:43
// 默认locale是en_US,我们手动设置locale为zh_Hans_CN,这里可以看到locale影响显示格式,这就是apple做的本地化。

5、Date转String(本地化显示):localizedString(from date: Date, dateStyle dstyle: DateFormatter.Style, timeStyle tstyle: DateFormatter.Style) -> String

let localDateStr = DateFormatter.localizedString(from: Date.now, dateStyle: .short, timeStyle: .long)
print(localDateStr)
// 12/1/22, 12:42:28 PM GMT+8

这个例子显示的格式还不是我们熟悉的显示格式,这是因为当前的Locale设置的是en_US。详情见后面的测试。

6、String转Date:dateFormatter.date(from string: String)

string转date,需要先设置转换格式,再按转换格式设置时间,就可以调用date进行转换,示例如下

// 设置转换格式
let df4 = DateFormatter()
df4.dateFormat = "MMM dd, yyyy zzz"
// 按照转换格式设置时间
let dateStr4 = "Feb 01, 2022 GMT"
// 进行转换
let date4 = df4.date(from: dateStr4)
print(date4!)
// 2022-02-01 00:00:00 +0000

 

Calendar(日历)

Calendar封装了有关时间系统的信息,其中定义了年的开始和长度等。它提供有关日历的信息,并支持日历计算,例如获取符合条件的Date或DateComponents等。

1、Calendar

 

Calendar.Identifier的定义:

public struct Calendar : Hashable, Equatable, ReferenceConvertible, Sendable {
		public enum Identifier : Sendable {
        case gregorian
        case buddhist
        case chinese
        case coptic
        case ethiopicAmeteMihret
        case ethiopicAmeteAlem
        case hebrew
        case iso8601
        case indian
        case islamic
        case islamicCivil
        case japanese
        case persian
        case republicOfChina
        case islamicTabular
        case islamicUmmAlQura
    }
}

Calendar.Component的定义:

public struct Calendar : Hashable, Equatable, ReferenceConvertible, Sendable {
    public enum Component : Sendable {
        case era
        case year
        case month
        case day
        case hour
        case minute
        case second
        case weekday
        case weekdayOrdinal
        case quarter
        case weekOfMonth
        case weekOfYear
        case yearForWeekOfYear
        case nanosecond
        case calendar
        case timeZone
    }
}

2、DateComponents

DateComponents是以可扩展的结构化方式封装日期的组件。它通过提供日期的部分来指定日期:时,分,秒,日,月,年等。它还可以用于指定持续时间,例如5小时16分钟。
可通过Calendar来将DateComponents转化为Date或将Date转化为DateComponents。

2.1、初始化

public init(calendar: Calendar? = nil, timeZone: TimeZone? = nil, era: Int? = nil, year: Int? = nil, month: Int? = nil, day: Int? = nil, hour: Int? = nil, minute: Int? = nil, second: Int? = nil, nanosecond: Int? = nil, weekday: Int? = nil, weekdayOrdinal: Int? = nil, quarter: Int? = nil, weekOfMonth: Int? = nil, weekOfYear: Int? = nil, yearForWeekOfYear: Int? = nil)

2.2、主要属性

public var calendar: Calendar?
public var timeZone: TimeZone?
public var era: Int?
public var year: Int?
public var month: Int?
public var day: Int?
public var hour: Int?
public var minute: Int?
public var second: Int?
public var nanosecond: Int?
public var weekday: Int?
public var weekdayOrdinal: Int?
public var quarter: Int?
public var weekOfMonth: Int?
public var weekOfYear: Int?
public var yearForWeekOfYear: Int?
public var isLeapMonth: Bool?
public var date: Date? { get }

3、Date转DateComponents

Calendar提供了两个重载函数来用于实现Date转DateComponents

3.1、

dateComponents(in timeZone: TimeZone, from date: Date) -> DateComponents

示例代码如下:

let calendar = Calendar.current
let dc1 = calendar.dateComponents(in: TimeZone(secondsFromGMT: 7*3600)!, from: Date.now)
print(dc1.year!, dc1.month!, dc1.day!)
// 2022 12 1

3.2、

dateComponents(_ components: Set<Calendar.Component>, from date: Date) -> DateComponents

示例代码如下:

let dc2 = calendar.dateComponents([.year, .month, .day], from: Date.now)
print(dc2.year!, dc2.month!, dc2.day!)
// 2022 12 1

4、DateComponents转Date

// 创建DateComponents对象
var dc = DateComponents()
// 手动设置DateComponents的各个时间成分
dc.year = 2022
dc.month = 12
dc.day = 2
dc.hour = 11
dc.minute = 35
dc.second = 15
// 创建calendar对象
let cal4dc = Calendar.current
// 进行转换
let dateFromDc = cal4dc.date(from: dc)
print(dateFromDc!)
// 2022-12-02 03:35:15 +0000

我们发现打印出来的时候比预期时间少了8个小时。

 

继续测试把时区设置成零时区,输出时间和预期一致

dc.timeZone = TimeZone(secondsFromGMT: 0*3600)
let cal0 = Calendar.current
let dateForDc0 = cal4dc.date(from: dc)
print(dateForDc0!)
// 2022-12-02 11:35:15 +0000

把时区设置成东一区,输出时间比预期少了一个小时

dc.timeZone = TimeZone(secondsFromGMT: 1*3600)
let cal1 = Calendar.current
let dateForDc1 = cal1.date(from: dc)
print(dateForDc1!)
//2022-12-02 10:35:15 +0000

把时区设置成东二区,输出时间比预期少了两个小时

dc.timeZone = TimeZone(secondsFromGMT: 2*3600)
let cal2 = Calendar.current
let dateForDc2 = cal2.date(from: dc)
print(dateForDc2!)
//2022-12-02 09:35:15 +0000

把时区设置成西一区,输出时间比预期多了一个小时

dc.timeZone = TimeZone(secondsFromGMT: -1*3600)
let calW1 = Calendar.current
let dateForDcW1 = calW1.date(from: dc)
print(dateForDcW1!)
//2022-12-02 12:35:15 +0000

最终结论:设置了时区,时间向对应时区反方向走了对应小时。

现象解释:当我们设置了时间2022-12-02 12:35:15,系统默认使用当前设备的时区(东八区),于是components认为我们输入的时间是2022-12-01 12:35:15,但是Date对象本身使用的时区是零时区,于是在转换为Date对象时,会将东八区时间转换成零时区时间,而零时区时间比东八区时间慢了八个小时,于是减去八小时得到零时区时间:2022-12-02 03:35:15 +0000

解决办法:要显示当地时间有两种办法

1、把Date对象转换为DateComponents对象,转换时将时区重新设置为对应的时区。

2、用extension给对象对象扩展一个转换成当地时间的方法,使用时直接调用即可:

extension Date {
    // Convert UTC (or GMT) to local time
    func toLocalTime() -> Date {
        let timezone    = TimeZone.current
        let seconds     = TimeInterval(timezone.secondsFromGMT(for: self))
        return Date(timeInterval: seconds, since: self)
    }
}

实例:

print(dateFromDc!.toLocalTime())

 

日期计算

日期计算主要用到Calendar提供的方法

1、计算两个Date的差值:dateComponents(_ components: Set<Calendar.Component>, from start: Date, to end: Date) -> DateComponents

计算两个字符串格式的日期的天数差

let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"

let calendar = Calendar.current

let startDate = formatter.date(from: "2022-11-29 12:35:10")
let endDate = formatter.date(from: "2022-12-01 15:40:05")

let daysDiff = calendar.dateComponents([.day, .hour, .minute, .second], from: startDate!, to: endDate!)
print(daysDiff, daysDiff.day!, daysDiff.hour!)
// day: 2 hour: 3 minute: 4 second: 55 isLeapMonth: false  2 3

2、计算两个DateComponents的差值:dateComponents(_ components: Set<Calendar.Component>, from start: DateComponents, to end: DateComponents) -> DateComponents

let dcDiff = calendar.dateComponents([.hour, .minute, .second], from: DateComponents(hour: 10), to: DateComponents(hour: 15, minute: 20, second: 15))
print(dcDiff)
// hour: 5 minute: 20 second: 15 isLeapMonth: false

3、获取指定日期对应日历组件的值:component(_ component: Calendar.Component, from date: Date) -> Int

let nowHour = calendar.component(.hour, from: Date.now)
print(nowHour)
// 14

4、获取指定日期的开始日期:startOfDay(for date: Date) -> Dat

let startOfDay = calendar.startOfDay(for: Date.now)
print(startOfDay)
// 2022-11-30 16:00:00 +0000
// 备注,看到返回的Date类型,就是想到Date对象使用的零时区,所以显示时间会比预期少8个小时

5、日期是否匹配日期组件:date(_ date: Date, matchesComponents components: DateComponents) -> Bool

print(calendar.date(Date.now, matchesComponents: DateComponents(month: 12, day: 02)))
// false
print(calendar.date(Date.now, matchesComponents: DateComponents(month: 12, day: 01)))
// true

6、通过日历组件获取一个相对日期的新日期:date(byAdding component: Calendar.Component, value: Int, to date: Date, wrappingComponents: Bool = false) -> Date?

let dForAdd = Date.now
print(dForAdd)
//2022-12-01 07:23:40 +0000
let dnForAdd = calendar.date(byAdding: .day, value: 1, to: dForAdd)
print(dnForAdd!)
//2022-12-02 07:23:40 +0000
let dn2ForAdd = calendar.date(byAdding: .hour, value: 3, to: dForAdd)
print(dn2ForAdd!)
//2022-12-01 10:23:40 +0000
// wrappingComponents,当组件值超过最大限制,wappingCompenents为true时会减去最大值,为false时,
let dn3ForAdd = calendar.date(byAdding: .hour, value: 26, to: dForAdd)
print(dn3ForAdd!)
//2022-12-02 09:23:40 +0000,wrappingComponents默认为false,结果为当前时间+26小时
let dn4ForAdd = calendar.date(byAdding: .hour, value: 26, to: dForAdd, wrappingComponents: true)
print(dn4ForAdd!)
//2022-12-01 09:23:40 +0000,设置wrappingComponents为true,结果为当前时间+(26-24)小时

7、通过日期组件获取一个相对日期的新日期:date(byAdding components: DateComponents, to date: Date, wrappingComponents: Bool = false) -> Date?

print(dForAdd)
//2022-12-01 07:31:27 +0000
let dn5ForAdd = calendar.date(byAdding: DateComponents(hour: 26), to: dForAdd)
print(dn5ForAdd!)
//2022-12-02 09:31:27 +0000
let dn6ForAdd = calendar.date(byAdding: DateComponents(hour: 26), to: dForAdd, wrappingComponents: true)
print(dn6ForAdd!)
//2022-12-01 09:31:27 +0000

终于学到它俩了,目前为止主要是想了解这两个方法,在那个大神写月日历的代码中见过这俩方法,写到这里,觉得自己算是搞懂了Swift的日期处理的大部分内容了,接下来就是自由发挥时间。

文哥博客(https://wenge365.com)属于文野个人博客,欢迎浏览使用

联系方式:qq:52292959 邮箱:52292959@qq.com

备案号:粤ICP备18108585号 友情链接