iOSの設定アプリで日付と時刻の時間帯を変更すると、日付表示がおかしくなる問題の対応

# 概要
日付を表示するような画面は、iOS端末の時間帯設定によって日付がずれて表示される場合があるので、その対処法を説明します。

# まとめ
- Dateを日付として表示する際は、特定のタイムゾーンで表示したい場合は目的のタイムゾーンを設定しましょう。
- ISO8601タイムゾーンなし文字列からDateに変換する際はタイムゾーンが明示的に設定されるようにしましょう。

# 例
クーポンの有効期限を表示するアプリがあるとします。
この有効期限はWeb APIなどからISO8601の日付文字列を取得して、その内容を表示するものとします。
このクーポンは、日本時間の2021年7月31日23時59分59秒までが有効期限だとします。

## 通常表示
Web APIからは、`2021-07-31T23:59:59+09:00` という文字列が返ってきましたので、これをISO8601DateFormatterでDateに変換し、DateFormatterで文字列に変換します。
当初の思惑通り、有効期限は7月31日と表示されます。


<img width="200" alt="7月31日" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/17001e55-153b-3d94-5d75-6dd5f5c59b05.png">


```swift
func expireDate(iso8601String: String) -> String {
    let iso8601DateFormatter = ISO8601DateFormatter()
    guard let date = iso8601DateFormatter.date(from: iso8601String) else { fatalError() }
    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .long
    dateFormatter.locale = .init(identifier: "ja-JP")
    return dateFormatter.string(from: date)
}


// Web APIからのレスポンスを変換
let dateString = expireDate(iso8601String: "2021-07-31T23:59:59+09:00")
print(dateString)
```


## 時間帯を変更する
しかし、iOSの設定アプリで日付と時刻の時間帯を`ブリスベン、オーストラリア`に変更を行うと、同じコードでも有効期限は8月1日と表示されます。

<img width="200" alt="8月1日" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/30ee2eb2-ed5f-fd07-6f71-f45ddb0162cd.png">

これは、オーストラリアの時差が日本よりも1時間の時差があるため、2021年7月31日23時59分59秒の一時間後、つまり2021年8月1日0時59分59秒という認識をしてしまうため、この表示が起きてしまいます。

これを防ぐには、DateFormatterのtimeZoneに、今回表示したい日付、つまり日本時間を設定する必要があります。

```swift
func expireDate(iso8601String: String) -> String {
    let iso8601DateFormatter = ISO8601DateFormatter()
    guard let date = iso8601DateFormatter.date(from: iso8601String) else { fatalError() }
    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .long
    dateFormatter.timeZone = .init(identifier: "Asia/Tokyo") // ←日本時間で表示を行う
    dateFormatter.locale = .init(identifier: "ja-JP")
    return dateFormatter.string(from: date)
}

```

## 日付だけのISO8601文字列
ISO8601の文字列は、必ずしもタイムゾーンを持つわけではなく、タイムゾーンを持たない日付だけの文字列の場合もあります。
例えばWeb APIからタイムゾーンを持たない、`2021-07-31` という文字列が返ってくるとします。
この場合にiOS端末の時間帯を`ホノルル、米国`に変更すると、DateFormatterのtimeZoneに何も設定をしていないと、有効期限は7月30日と表示されます。

<img width="200" alt="7月30日" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/c063e2cd-e9c1-1edf-3ac9-848351317a2d.png">


先ほどと同じくdateFormatter.timeZoneに日本時間のtimeZoneを設定すれば一見うまくいっているように見えますが、DateのtimeIntervalSinceReferenceDateとしては2021年7月31日に9時間足された形になるので、時間も表示すると2021年7月31日9時0分となってしまいます。

この場合、Dateを生成するISO8601DateFormatterにも基準となるTimeZoneを設定することで、日付表示を正しく行うことができます。

```swift
func expireDate(iso8601String: String) -> String {
    let timeZone = TimeZone(identifier: "Asia/Tokyo") // ←基準のタイムゾーン
    let iso8601DateFormatter = ISO8601DateFormatter()
    iso8601DateFormatter.formatOptions = .withFullDate // ←日付形式を処理するためのオプション
    iso8601DateFormatter.timeZone = timeZone // ← ISO8601DateFormatterにタイムゾーンを指定
    guard let date = iso8601DateFormatter.date(from: iso8601String) else { fatalError() }
    print(date.timeIntervalSinceReferenceDate)
    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .long
    dateFormatter.timeZone = timeZone // ← DateFormatterにもタイムゾーンを指定
    dateFormatter.locale = .init(identifier: "ja-JP")
    return dateFormatter.string(from: date)
}

let dateString = expireDate(iso8601String: "2021-07-31")
print(dateString) // 2021年7月31日0時0分

```