あるいはDate
の時差の変換でハマった話しです。
海外対応を実装した方がターゲット層です。
概要
今回はiOSのDate
とDateFormatter
の仕様について話して行きたいと思います。
今週これだけで1, 2日ハマってしまいましたので備忘録も込めてまとめます。
ハマったことについて
例えば次のようなコードがあったとします。
簡単に言えばDate
のExtensionを作って日付の処理について簡単に扱う為に作った関数です。
extension Date {
var japaneseDeviceDateFormatter: DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")
dateFormatter.calendar = Calendar(identifier: .gregorian)
return dateFormatter
}
var deviceDateFormatter: DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = .current
dateFormatter.calendar = Calendar(identifier: .gregorian)
return dateFormatter
}
func getSampleLocalDate() -> String {
let localDate = Date()
let df = localDate.deviceDateFormatter
df.dateFormat = "yyyyMMddHHmm"
let testableDateString = df.string(from: localDate)
return testableDateString
}
}
関数の書き方とか確かにそういうところもツッコミたいかもしれませんが、
そこはグッと抑えてください。
テストの仕方
昔のシミュレータはタイムゾーンを変更できた記憶があるのですが、今のシミュレータはできないみたいです。
出来る方はこっそり教えてください。
実機で海外の時間に変更したい場合は
設定 > 一般 > 日付と時刻
の自動設定をオフにすると各国の時間帯に変更することができます。

これで各国の時間でデバッグすることができます。
テストコードを走らせる場合は実機をMacに繋げてXcodeでテストスキームで実機でビルドしないと駄目な気がします。
それではテストしていきます。
テストする仕様について
- 今回は海外在住のユーザーが日本のコンテンツを日本時刻で見たいというニーズに合わせて、海外時刻を日本時刻に変換する処理をテストしたいと思います。
- 海外時刻を関数に渡す時には
Date
が使えず"201905180110"
というような形の文字列
しか引数に渡すことができません。(例の文字列は2019年05月18日1:10
)
つまり、海外の日付の文字列
から日本時刻の文字列
ひいてはDate
を取得する関数が欲しいです
テスト失敗例その1
こちらは自分でテストを書いておきながらハマってしまったコードの例です。
import XCTest
@testable import DateSample
class DateSampleTests: XCTestCase {
func testBatPractice() {
let localDate = Date()
let localDateFormatter = localDate.deviceDateFormatter
localDateFormatter.dateFormat = "yyyyMMddHHmm"
guard let currentLocalDate = localDateFormatter.date(from: localDate.getSampleLocalDate()) else { return }
let currentDateString = localDateFormatter.string(from: currentLocalDate)
print("currentLocalDate:", currentLocalDate.description, ", currentDateString:", currentDateString)
let jpDateFormatter = localDate.japaneseDeviceDateFormatter
let jpDateString = jpDateFormatter.string(from: currentLocalDate)
guard let jpDate = jpDateFormatter.date(from: jpDateString) else { return }
print("jpDate:", jpDate.description, ", jpDateString:", jpDateString)
XCTAssertNotEqual(currentDateString, jpDateString)
}
}
これはテストが失敗します。
正解のテストコード
一見うまいこと行くように思われますが
let jpDateFormatter = localDate.japaneseDeviceDateFormatter
let jpDateString = jpDateFormatter.string(from: currentLocalDate) // ""
}
このステップで落ちてしまいます。
jpDateString
が生成されなくて落ちてしまうみたいです。
jpDate: 1999-12-31 15:00:00 +0000 , jpDateString:
Printing description of jpDateString:
""
正しくは次のコードになります。
import XCTest
@testable import DateSample
class DateSampleTests: XCTestCase {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
func testConvertToJapanese() {
let localDate = Date()
let localDateFormatter = localDate.deviceDateFormatter
localDateFormatter.dateFormat = "yyyyMMddHHmm"
guard let currentLocalDate = localDateFormatter.date(from: localDate.getSampleLocalDate()) else { return }
let currentDateString = localDateFormatter.string(from: currentLocalDate)
print("currentDate:", currentLocalDate.description, ", currentDateString:", currentDateString)
let jpDataFormatter = localDate.japaneseDeviceDateFormatter
jpDataFormatter.dateFormat = "yyyyMMddHHmm" // NEW
let jsDateString = jpDataFormatter.string(from: currentLocalDate)
guard let jpDate = jpDataFormatter.date(from: jsDateString) else { return }
print("jpDate:", jpDate.description, ", jpDateString:", jsDateString)
XCTAssertNotEqual(currentDateString, jsDateString)
}
}
// NEW
の部分が修正コードになります。
dateFormatterを日本用に新しく生成した場合は最初dateFormat
を指定しないとDate
からString
に日本時刻で変換できない仕様だということが分かりました。
当然と言えば当然なのですが焦っているときにExtensionで作られているクラスのテストでは思いつきません。
もしかすると既に気づいている人もいるかもしれませんが、海外用のDateFormatter
と日本用のDateFormatter
の関数が既にあったので僕は勘違いしてしまいました。
実はこんな回りくどい処理をする必要がないのです。
そもそも文字列などからDate
のインスタンスを生成した場合はUTC
基準なのです。
(この記事によると既に一番上の参考コードはバットプラクティスのオンパレードですがそこはスルーで)

ということはDateを作成した時点で既にTimeZone
の概念が入っているのでTimeZone
を変更するだけでいいのです。
正解のテストコード (その2)
以上をもって上のテストコードをもっとスマートに書くなら例えば次のように変更すればいいのです。
import XCTest
@testable import DateSample
class DateSampleTests: XCTestCase {
func testConvertToJapaneseBestPractice() {
let localDate = Date()
let localDateFormatter = localDate.deviceDateFormatter
localDateFormatter.dateFormat = "yyyyMMddHHmm"
guard let currentLocalDate = localDateFormatter.date(from: localDate.getSampleLocalDate()) else { return }
let currentDateString = localDateFormatter.string(from: currentLocalDate)
print("currentDate:", currentLocalDate.description, ", currentDateString:", currentDateString)
localDateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo") // TimeZone を変える
let jsDateString = localDateFormatter.string(from: currentLocalDate)
guard let jpDate = localDateFormatter.date(from: jsDateString) else { return }
print("jpDate:", jpDate.description, ", jpDateString:", jsDateString)
XCTAssertNotEqual(currentDateString, jsDateString)
}
}
// TimeZone を変える
のように既に作成したlocalDateFormatter
のTimeZoneを日本時刻に変更すればわざわざ日本用のDateFormatter
のインスタンスを作り直す必要がないのです。
言われて見れば「確かに!」と思う内容なのですが今まで使いまわしてタイムゾーンを変更する機能を作ったことがありませんでしたので拍子抜けしてしまいました。
そもそも海外の時刻を文字列
で持ってそれを日本の時刻の文字列
に変換する意味ってないやん、というツッコミもあるかもしれませんが仮にそういう境遇に陥ってしまった場合は今日の記事が参考になるかと思います。
1週間前にこの記事を読んでいたらこの1週間は徹夜する必要がなかったぐらいです。
DateFormatter について
元々はNSDateFormatter
ですがSwiftの登場によってDateFormatter
に呼び方が変わりました。
Date()クラスを修飾したいに使うクラスになります。
初期宣言
let dateFormatter = DateFormatter()
だいたいみんなが実装に困る部分の設定は
- calendar
- locale
- timezone
かなと思います。
calendar
設定は一般 > 言語と地域 > 暦法
から変更できます。

Left align | Right align |
---|---|
gregorian | グレゴリオ歴 |
japanese | 和暦 |
chinese | 中国暦 |
indian | インド暦 |
みたいな感じになります。
iOS アプリ開発 全アプリ共通でやっておきたいUIまわりのテストが詳しいです。
和暦は24時間対応の際にiPadとかでバグの温床になるケースが多いのでgregorian
を指定することが多いです。
japanese
の方が設定は簡単ですが長期的運用を考えた場合はこちらがオススメです。
let dateFormatter = DateFormatter()
dateFormatter.calendar = Calendar(identifier: .gregorian)
locale
文字列 から 日付 へ変換する際にどのロケールで変換するのかっていう設定で使います。
- ja_JP
- ja
- en_US_POSIX
とかのオプションがあります。
表についてはこちらのページが一覧になります。
一般的にはja_JP
かen_US_POSIX
で設定することが多いです。
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "ja_JP")
timezone
これが今回の記事のメインテーマになったプロパティです。
iPhoneの実機の表示時刻で各国の時間に合わせる時に使うオプションになります。
表については
こちらが参考になります。
海外対応や時差の計算で使うと思います。
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone(identifier: "GMT") // 標準時間
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo") // 日本時間
dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul") // ソウル時間
dateFormatter.timeZone = .current // 実機での設定時間
とりあえずここまでの情報や参考ページを読み込めばDateとDateFormatterとの関係性について深く理解できると思います。