[Swift]カレンダーは月曜始まりの地域があるから気をつけてねという話

![Simulator Screen Shot - iPhone 11 - 2020-06-08 at 19.28.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172991/42c9a6ba-3f3c-55a3-5c43-9375ad82d617.png)

こんな感じの自作カレンダーを搭載したアプリを2018年にリリースして、放置していました。
すると最近、イタリア語らしき言語で「日付と曜日が違うんだけど」といったニュアンスのレビューがつきました。
最初はLocalの設定とかに問題があるのかな程度に思っていたのですが、よくよく調べてみるとCalendarクラスを使った実装方法に問題があったため、2年間気付かなかった自戒の意味も込めてここで供養します:pray:

# 既存実装
カレンダーは`UICollectionView`を使って実装していて、曜日を表示しているセクションと日付を表示しているセクションの2セクション構成としています。
余談ですが、左右にスワイプで月が切り替わります。

`UICollectionView`を使ったカレンダー実装はたくさん記事があるのでここでは割愛させていただいて、今回問題になった部分のコードだけ掲載します。

### 曜日を用意する部分
`section0`で表示している曜日の文字列配列を用意している部分のコードです。

```Swift
let symbols = Calendar.current.shortStandaloneWeekdaySymbols
```

### 日付を用意する部分
`section1`で表示している日付の配列を用意している部分のコードです。

```Swift
    //月の配列
    func dateForCellAtIndexPathForDate(selectedDate: Date) -> [Date] {
        var dates = [Date]()
        let firstDate = firstDateOfMonth(date: selectedDate)
        let ordinalityOfFirstDay = Calendar.current.ordinality(of: .day, in: .weekOfMonth, for: firstDate)!
        
        let numberOfWeeks = Calendar.current.range(of: .weekOfMonth, in: .month, for: firstDate)!.count
        let number = numberOfWeeks * 7
        
        for i in 0..<number{
            let dateComponents = DateComponents()
            dateComponents.day = i - (ordinalityOfFirstDay - 1)
            let date = Calendar.current.date(byAdding: dateComponents, to: firstDate)!
            dates.append(date)
        }
        return dates
    }


    //月の初日を取得
    func firstDateOfMonth(date: Date) -> Date {
        var components = Calendar.current.dateComponents([.year, .month, .day], from: date)
        components.day = 1
        let firstDateMonth = Calendar.current.date(from: components)!
        return firstDateMonth
    }
```
*掲載用にここでは強制アンラップしています

内容を簡単にいうと

- `Calendar.current.shortStandaloneWeekdaySymbols`でデバイスの言語設定にあわせた日〜土の曜日配列を取得してそれを`section0`に表示
- `Calendar.current.ordinality(of: .day, in: .weekOfMonth, for: firstDate)`で該当月の第一週で1日が何番目かを取得して、そこから該当月の週数x7した数だけ1日ずつ進めて、該当月のカレンダーに表示する日付の配列を作成し、`section1`に表示

といった手順を行っていました。
![Simulator Screen Shot - iPhone 11 - 2020-06-09 at 00.10.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172991/7d2d423e-d20b-b447-9a13-918b1c37797d.png)


# 曜日がずれる
この状態で、端末の地域設定をイタリアに変更してみます。
![Simulator Screen Shot - iPhone 11 - 2020-06-09 at 00.17.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172991/c5fbbefe-e166-79f3-9a1d-30fbf8ca85ed.png)

日本 | イタリア
:-: | :-:
![Simulator Screen Shot - iPhone 11 - 2020-06-08 at 19.28.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172991/4c7797ef-d3ea-4f4b-90a4-bd4fb1f5037b.png) | ![Simulator Screen Shot - iPhone 11 - 2020-06-09 at 00.19.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172991/04b8d1f4-9bb4-53c9-aae9-4d761eb3e092.png)

**ズレてる！！**

# 原因
これはタイトルにある通り、イタリアがある欧州などの一部の地域ではカレンダーが月曜始まりである事が原因です。
今回のロジックで言うと、`Calendar.current.ordinality(of: .day, in: .weekOfMonth, for: firstDate)`の結果が異なる事が原因です。

実際に、2020年7月の結果を出力してみます。

日本 | イタリア
:-: | :-:
4 | 3

このように、1日ズレてしまいます。
なぜズレるのか、理由はどうやら`ordinality`のweekが絡むケースの計算ロジックで`firstWeekDay`というやつが使われているようなんです。
（詳細に興味がある方は[この辺](https://github.com/apple/swift-corelibs-foundation/blob/42bfad499c4e938156d5b49331c75f69cc92798c/CoreFoundation/Locale.subproj/CFCalendar.c)をご覧ください。）

この`firstWeekDay`は`Calendar.current.firstWeekday`で取得できるため、各地域設定で出力してみます。

日本 | イタリア
:-: | :-:
1 | 2

おぉ、違いますね！
これは週の始まりが何曜日かを示していて、`1`は日曜日で`2`は月曜日です。

# 対応
今回どのように対応したかというと、素直に月曜始まりの地域では曜日の並びを月曜始まりで表示する事としました。
そもそも`Calendar.current.shortStandaloneWeekdaySymbols`で`firstWeekDay`を考慮した配列を返してくれれば良かったのですが、現状そういうわけではないようなので、自分で対応します。。。

```Swift
    // 曜日の配列
    func shortWeekdaySymbols() -> [String] {
        var symbols = Calendar.current.shortStandaloneWeekdaySymbols
        let firstWeekday = symbols[Calendar.current.firstWeekday - 1]
        while symbols.first! != firstWeekday {
            let symbol = symbols.removeFirst()
            symbols.append(symbol)
        }
        return symbols
    }
```
現状は日曜始まりと月曜始まりしかないとは思っていますが、自分が未知の地域や今後文化の変化でどうなるか分からなかったので一応どの曜日始まりでも対応できるようにしておきました。

#  結果
日本 | イタリア
:-:|:-:
![Simulator Screen Shot - iPhone 11 - 2020-06-08 at 19.28.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172991/99e814f7-628c-42a0-8d28-bcfbb8a3f75c.png) | ![Simulator Screen Shot - iPhone 11 - 2020-06-09 at 01.24.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172991/163061d9-9d51-5ae3-bd19-a7388dba52ac.png)

**うまくいきました！**
他に良い方法があればぜひコメントお願いします:bow:

# 最後に
このカレンダーを実装したアプリは[こちら](https://apps.apple.com/jp/app/piary/id1357569770)です。
デザイン、機能などまだまだ改善の余地があるのでよろしければレビューお願いします！
ありがとうございました！！