Edited at

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

More than 1 year has 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 の作成に骨が折れるので、何らかの手段で自動で作成されることをおすすめします。