【Swift5】 NSNumberのBoolとIntの判定(Alamofire)
背景
Alamofireを使用してAPI通信を行うときに、
parameter(body)を[String:Any]に変換して渡してあげる必要がありました。
そのときに
- Codableでエンコードしてdata型に変換
- JsonSerializationを使用してdata→[String:Any]に変換
を行えば、簡単に変更ができます。
例えば以下の例です。
struct Todo: Codable {
let id: String
let title: String
let number: Int
}
let todo: Todo = Model(id: "ssss", title: "hogehoge", number: 8)
let data: Data = JSONEncoder().encode(todo)
let parameter: [String:Any] = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
print(parameter)
// jsonに変換
// ["id": "ssss", "title": "hogehoge", "number": 8]
このようにModelがStringやIntだけであれば上記の流れで変換は可能です。
問題は以下のようにBool型が入ったときになります。
struct Todo: Codable {
let id: String
let title: String
let number: Int
let isDone: Bool
}
let todo: Todo = Model(id: "ssss", title: "hogehoge", number: 8, isDone: true)
let data: Data = JSONEncoder().encode(todo)
let parameter: [String:Any] = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
print(parameter)
// jsonに変換
// ["id": "ssss", "title": "hogehoge", "number": 8, "isDone": 1]
// isDoneがfalseなら0,trueなら1に変換されてしまう
上記のようにBool型がInt型のように変換されてしまうため、元の値がBoolだったのかIntだったのかが判別できないという問題にぶつかりました。
解決策
結論から言うと、一度上記の流れで変換したあとに
CFBooleanGetTypeID
とCFGetTypeID
を取得しその値を比較することで、元の値がBoolだったのかIntだったかを判定しました。
コードとしては以下のような判定になります。
func isBoolNumber(number: NSNumber) -> Bool {
let boolID = CFBooleanGetTypeID()
let numID = CFGetTypeID(number)
// 元の値がboolだとboolIDとnumIDが異なる値になる
return numID == boolID
}
こちらが成り立つ理由としては、JsonSerializeで変換後の値に差異があるためです。
上記の変換後のnumberとisDoneの型と値に注目すると、
number: NSNumber 8
isDone: NSNumber YES
という風に元がBool値の場合,
trueの時はNSNumber YES
falseの時はNSNumber NO
のようになります。
こちらの違いを利用すると上記のように各種IDを取得すると値が異なるので
判定をかけることができます。
全体コード
自分の場合dictionaryという変数をmodelに用意して、変換を行うクロージャーを定義しました。
struct Todo: Codable {
let id: String
let title: String
let number: Int
let isDone: Bool
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { data in
if let datas = data as? [String: Any] {
return convertNSNumberToBool(value: datas)
}
return data as? [String: Any]
}
}
// Boolの場合はBoolに変換を行う
func convertNSNumberToBool(value: [String: Any]) -> [String: Any] {
let array = value.mapValues { value -> Any in
// 値がNSNumberの場合は判定の関数を行う
if value is NSNumber {
if isBoolNumber(number: value as! NSNumber) {
return value as! Int == 0 ? false : true
}
// jsonの階層は2階層以上の場合に対応
} else if value is [String: Any] {
return convertNSNumberToBool(value: value as! [String: Any])
}
return value
}
return array
}
// NSNumberがboolかintか判定を行う
func isBoolNumber(number: NSNumber) -> Bool {
let boolID = CFBooleanGetTypeID()
let numID = CFGetTypeID(number)
return numID == boolID
}
}
上記のやり方だと
- Codableでエンコード
- JsonSerializeでjsonに変換
- jsonのデータをmap関数で一つ一つを判定し、代入し直す
のように一度変換したものを無理矢理値を直しているため、あまりスマートなやり方ではないです。
いろいろ調べましたが、上記の方法でないとできなかったので
もっとスマートなやり方があればぜひ教えて下さい・・。
Discussion