日本語 Codable Enums

この記事は [iOS Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ios) の 6日目の記事です。

Codable[^1] は、Swift 4.0 (3.2) で登場し、今年リリースされた Swift 4.1 / 4.2 のアップデートによって、さらに使いやすくなりました。

この記事では、実際に今年iOSアプリ開発で遭遇したケースを元に[^2]、 enum、 Codable など、 Swift らしい機能を使って JSON を簡潔に扱う例を紹介します。

## 想定ケース

「やるべき宿題（タスク）を表示してくれるリスト」を表示する画面を作ります。

API からは以下のような、タスクの配列が返ってきます。

```js
{
    [
    id: 1,  // タスクID
    subject_type: 2, // 教科タイプ
    books: [ ... ] ... // やるべき本とかとか
    ], ...
}
```

宿題には、教科のタイプ（国語、英語とか）が含まれます。

これを、リストの各要素に表示する画面を作ります。教科ごとに色が決まっていて、リストの要素の背景や、文字などに使用します。

また、教科のリストを選んで設定画面も作ります。

まとめると以下のようになります。

* 課題の配列を、リストに表示する画面がある
* 教科タイプ (`subject_type`) は、整数型で返ってくる
* 教科の情報 (教科名, 色など) があり、クライアントで持っている
* 教科タイプ (`subject_type`) をユーザーに選んでもらって、 post してもらう画面がある

JSON のパースには Codable を使うとよさそうですね。使ってみましょう。

## 実装例

以下のような型を定義しました。

```swift
struct Task: Codable {
    let id: Int
    let books: [Book]
    let subjectType: SubjectType
}

struct Book: Codable {
    let id: Int
    let title: Int
    ...
}

enum SubjectType: Int, Codable, CaseIterable, CaseStringConvertible {
    case 数学
    case 英語
    case 倫理
    case 簿記・会計
    case 生物基礎

    var color: UIColor {
        switch self {
        case .数学: return .blue
        case .英語: return .red
        case .倫理: return .yellow
        case .簿記・会計: return .brown
        case .生物基礎: return .green
        }
    }
}
```

注目してほしいのは `SubjectType` です。 `Codable` で、`CaseIterable` で、 `CaseStringConvertible` な Int の enum になっています。らぶるらぶる。

`subjectType` は単なる Int で指定しても実装できてしまうのですが、enum な型にするのには以下のようなメリットがあります。

* 他の型でも `subjecType` が登場するとき、それらが同一のものであることを示せる (型にするメリット)
* switch 文で使うとき、 `default:` を使わなければ case が増えたときにビルドエラーになり、もれなく列挙できる (enum を使うメリット)

Int の enum は、何も指定しない場合は 0 から値が割り当てられ、 `subject_type` の値が 0 なら数学、 1 なら英語に map されます。特定の値を特定の case としたいときには数値を指定することもできます。

```swift
    case 数学 = 5
    case 英語
```

ちなみに、JSONのほうのキーが snake_case になっている場合は、`JsonDecoder` に `keyDecodingStrategy` を指定することで camelCase に変換してくれます（Swift 4.1〜)。

```swift
let jsonDecoder = JsonDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
```

これ以外にも、テクニックというと大げさですが、簡潔に扱うために工夫している部分があるので、ここでは3つほど紹介します。

## テクニック1: 日本語をつかう

これは早速好みの分かれそうな方法ですが、case の名前に日本語を使っています。

日本語を使うことにはもちろんデメリットもあります。そのため、辞書を使っていい感じの英語に英訳できる場合は、英語を使うことも多いのですが、今回のような例だと…

```swift
    case 数学
    case 英語
    case 倫理
    case 簿記・会計
    case 生物基礎
```

律儀に英訳していくのは結構たいへんではないでしょうか？（特に「簿記・会計」とか…）

この場合は、 case の名前に、無理せず日本語をそのまま使うことで、可読性や保守性を上げることができるケースになるのではないかと思います（そして、後述する副次的な効果もあります）。

都道府県名なども日本固有の名前なので、日本語にしてしまうのがおすすめの例です。

```swift
enum Prefecture {
    case 北海道
    case 青森県
    case 岩手県
    ...
}
```

日本語命名は便利ですが使う場所によってはリスクもあり、たとえば Swift4.2 では、 Storyboard に紐付いている ViewController の名前などに指定していると、インスタンス化に失敗することがあります。しかし enum の case 名であれば、まず問題ないと思います。

## テクニック2: `String(describing:)` をつかう

`SubjectType` は `CaseStringConvertible` というユーザー定義のプロトコルに適合していました。これは、 `String(describing: self)` [^3]を返すプロトコルです。

```swift
protocol CaseStringConvertible { }

extension CaseStringConvertible {
    var string: String {
        return String(describing: self)
    }
}

hoge.string // それ自身を表現する文字列
```

`String(describing: self)` は、それ自身を表現する文字列を返してくれます。たとえば、 Int の enum のインスタンスを渡したときは、case の名前になります。print 関数にインスタンスを与えたときに出力される内容は、これと同じです。

case名を日本語にしたいのはこれを使いたいからでもあります。UIの表示名に合わせておくことで、 UIに表示すべき名前を `インスタンス名.string` で簡潔に取り出すことができます。

```swift
subjectLabel.text = task.type.string // 生物基礎 とかが返ってくる
task.type.rawValue // Int そのものの値が欲しいとき
```

また、色を返す変数を用意していたので、同じように `.color` でシンプルに取り出すことができます。

```swift
    var color: UIColor {
        switch self {
        case .数学: return .blue
        case .英語: return .red
        case .倫理: return .yellow
        case .簿記・会計: return .brown
        case .生物基礎: return .green
        }
    }
}
...

cell.subjectView.backgroundColor = task.type.color
```

これでリストの要素を簡潔に表現することができそうです。

## テクニック3: CaseIterable をつかう

Swift 4.2 で、`CaseIterable` というプロトコルが増えました[^4]。これに適合させた enum は、 `.allCases` で case のコレクションが取れるようになりました。

```swift
public protocol CaseIterable {
    /// A type that can represent a collection of all values of this type.
    associatedtype AllCases : Collection where Self.AllCases.Element == Self
 
    /// A collection of all values of this type.
    public static var allCases: Self.AllCases { get }
}
```

たとえばユーザーに `SubjectType` を選択してもらうリストを作りたい場合、 `.allCases` を使って表示するべき内容を指定することができます。

```swift
func collectionView(_ collectionView: UICollectionView,
                    cellForItemAt indexPath: IndexPath)
    -> UICollectionViewCell {
    let cell = collectionView
        .dequeueReusableCell(with: MyCell.self, for: indexPath)
    cell.setUp(title: SubjectType.allCases[indexPath.row].string)
}

func collectionView(_ collectionView: UICollectionView,
                    didSelectItemAt indexPath: IndexPath) {
    self.selectedType = SubjectType(rawValue: indexPath.row)!
    collectionView.reloadData()
}
```

便利ですね。

`SubjectType` は Codable (= JSON から変換できるし、JSON に変換可能)なので、選ばれた教科をそのまま post することができます。これで教科を選択する画面も作れそうです。

なお、 `CaseIterable` は Swift4.2 で追加されたプロトコルですが、Swift 4.2 以前でも、以下のように自分で定義して使うことができます[^5]。

```swift
public protocol EnumEnumerable { associatedtype Case = Self }
public extension EnumEnumerable where Case: Hashable {
    private static var iterator: AnyIterator<Case> {
        var n = 0
        return AnyIterator {
            defer { n += 1 }
            let next = withUnsafePointer(to: &n) {
                UnsafeRawPointer($0).assumingMemoryBound(to: Case.self).pointee
            }
            return next.hashValue == n ? next : nil
        }
    }
    public static var allCases: [Case] {
        return Array(self.iterator)
    }
}
```

## まとめ

Codable に適合した Enum を使うことで、 JSON を返す API とのやりとりを簡潔に Swift らしく表現できます。

また、以下と組み合わせることによって、さらに簡潔に表現できる場合があります。

* case の名前は無理に英訳せず、UI上の表示名を合わせられないか検討する
* `String(describing:)` を使う
* `CaseIterable` を使う

2019年も enum と Codable を使ってたのしくコーディングしていきましょう！ 🎉

[^1]: https://developer.apple.com/documentation/swift/codable
[^2]: 厳密には実際のユースケースやコードではなく、説明のために再編成しています。
[^3]: https://developer.apple.com/documentation/swift/string/2427941-init
[^4]: https://developer.apple.com/documentation/swift/caseiterable
[^5]: 参考： https://qiita.com/sgr-ksmt/items/c1c6a91324cf12151256