Help us understand the problem. What is going on with this article?

Swift でタイプセーフな区分値

More than 3 years have passed since last update.

やりたいこと

こんな感じでサーバーから取得した区分値を、 iOS なクライアント側で型安全に扱いたい。

response.json
{
  "gender_kbn": "00101",
  "prefecture_kbn": "00213"
}

ここでは下表のように、区分の種類によって上3桁が、区分によって下2桁が変わる区分値を扱います。

区分の種類 区分種コード
性別区分 001
都道府県区分 002
性別区分 区分コード
男性 01
女性 02

環境

Swift 3.1

あの頃はこう書いていた

叩き台のコード。

BadKbn.swift
class Constants {
    enum Kbn: String {
        // 性別
        case genderKbnMan = "00101"
        case genderKbnWoman = "00102"
        // 都道府県
        case prefectureKbnHokkaido = "00201"
        // ...

        var logicalName: String {
            switch self {
                // 性別
                case .genderKbnMan: return "男性"
                case .genderKbnWoman: return "女性"
                // 都道府県
                case .prefectureKbnHokkaido: return "北海道"
                // ...
            }
        }
    }
}

これだと、いくつかの不満がありました。

  • 文字列の比較をやめたい。
  • 型安全ではない。性別区分が入ることを期待する変数に、都道府県区分が入る可能性があることをコンパイル時にチェックできない。
MyProfileModel.swift
// モデル
struct MyProfileModel {
    // 性別区分
    let gender: String
    // 都道府県区分
    let prefecture: String

    init(jsonFromServer: [String: Any]) {
        // 型安全ではない
        self.gender = jsonFromServer["gender_kbn"] as? String ?? ""
        self.prefecture = jsonFromServer["prefecture_kbn"] as? String ?? ""

        switch self.gender {
            // 文字列の比較をやめたい。
            case Constants.Kbn.genderKbnMan: // ...
            case Constants.Kbn.genderKbnWoman: // ...
            default: // 何を書こう?
        }
    }
}

そこで

こんなプロトコルを用意し、各区分種を準拠させた。

KbnType.swift
// 各区分種が準拠するプロトコル
protocol KbnType: Equatable {
    // 全てのケース
    static var cases: [Self] { get }
    // 区分の種類を示す接頭詞
    static var prefix: String { get }
    // 区分を示す接尾詞
    var postfix: String { get }
    // 表示用論理名
    var logicalName: String { get }
}

extension KbnType {

    // 接頭詞 + 接尾詞 = 区分を一意に取得できるコード
    var code: String { return type(of: self).prefix + self.postfix }

    // コードで区分を取得
    static func get(byCode code: String?) -> Self? {
        guard let code = code else { return nil }
        return self.cases.filter { code == $0.code }.first
    }

    // MARK: - Equatable

    static func == (lhs: Self, rhs: Self) -> Bool {
        return lhs.postfix == rhs.postfix
    }

}

そのプロトコルに準拠させる形で各区分種の enum を作成

Kbn.swift
// 名前空間として構造体を使用した
struct Kbn {

    // 性別区分
    enum Gender: KbnType {

        case man
        case woman

        static var cases: [Gender] {
            return [
              .man,
              .woman
            ]
        }

        static var prefix: String { return "001" }

        var postfix: String {
            switch self {
            case .man: return "01"
            case .woman: return "02"
            }
        }

        var logicalName: String {
            switch self {
            case .man: return "男性"
            case .woman: return "女性"
            }
        }

    }

}

使ってみる

  • 文字列の比較から卒業できた。
  • 型安全になった。異なる区分種が入らないことを、コンパイル時に保証できるようになった。
  • たとえ区分種によって桁数が変わっても、桁数に依存していないので影響を受けない。
MyProfileModel.swift
struct MyProfileModel {
    // 性別区分
    let gender: Kbn.Gender?
    // 都道府県区分
    let prefecture: Kbn.Prefecture?

    init(jsonFromServer: [String: Any]) {
        // 異なる区分種の区分値が入る可能性がない
        self.gender = Kbn.Gender.get(byCode: jsonFromServer["gender_kbn"] as? String)
        self.prefecture = Kbn.Prefecture.get(byCode: jsonFromServer["prefecture_kbn"] as? String)

        if let gender = self.gender {
            // 文字列の比較が不要に
            switch gender {
                case .man:
                case .woman:
                // default: を書く必要がない。網羅性が高まった。
            }
        }

    }
}

さいごに

区分値の取扱い、割りとみんなやってそうなんだけど Swift で扱っている記事がみつからず。
もっと良い方法があれば教えてください。
区分種の enum の作成に骨が折れるので、何らかの手段で自動で作成されることをおすすめします。

KosukeOhmura
Swift, Ruby on Rails とかやってます。
https://www.kosukeohmura.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away