LoginSignup
6
5

More than 3 years have passed since last update.

Codableでレスポンス形式のゆらぎを吸収するPrice型をつくる

Last updated at Posted at 2019-08-24

はじめに

アプリで利用するAPIの種類が増えてくると、APIによってAPIレスポンスの形式が異なるという課題が発生します。

例えばこのような価格を返す複数のAPIがあったとします。

Int型で返す
{ "price": 10000 }
String型(価格フォーマットなし)で返す
{ "price": "10000" }
String型(価格フォーマットあり)で返す
{ "price": "10,000円" }

特に複数APIのマッシュアップをするような大規模サービスでは、ドメイン毎にコード規約やレスポンス形式の考え方が異なるため、同じ価格を表現するレスポンスでもこのようなことが発生しがちです。これらのレスポンス形式のゆらぎを吸収するPrice型をCodableでつくってみます。

Price型をつくる

上記の課題を解決するために、以下のようなPrice型を定義してみます。
Price型はDecodableに準拠し、init(from decoder: Decoder) throwsメソッドを実装することでデコード処理をカスタマイズしています。

Price型の実装
struct Price: RawRepresentable, Decodable {
    typealias RawValue = Int

    let rawValue: Int

    init?(rawValue: Self.RawValue) {
        self.rawValue = rawValue
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        // String型からInt型に変換する
        // 後述するextractNumber関数を用いて文字列から数字のみを取り出す
        if let stringValue = try? container.decode(String.self), let intValue = Int(stringValue.extractNumber) {
            self.rawValue = intValue
            return
        }

        let intValue = try container.decode(Int.self)
        self.rawValue = intValue
    }
}

文字列から数字のみを取り出すextractNumber関数は以下のように実装しています。

文字列から数字のみを取り出すextractNumber関数
extension String {
    var extractNumber: String {
        return trimmingCharacters(in: CharacterSet.decimalDigits.inverted)
            .replacingOccurrences(of: ",", with: "")
    }
}

Price型を利用してデコードする

以下のようなJSONレスポンスを想定します。

JSONレスポンス
{
    "price1": 1000,
    "price2": "2000",
    "price3": "3,000円",
    "price4": "¥4,000"
}

以下のように、レスポンスの価格の表現形式によらず、Price型としてデコードができます。

レスポンスの価格の表現形式によらず、Price型としてデコード
let jsonString = """
{
    "price1": 1000,
    "price2": "2000",
    "price3": "3,000",
    "price4": "¥4,000"
}
"""
let data = jsonString.data(using: .utf8)!

// レスポンスの価格の表現形式によらず、Price型としてデコード
struct Item: Decodable {
    let price1: Price
    let price2: Price
    let price3: Price
    let price4: Price
}

do {
    let item = try JSONDecoder().decode(Item.self, from: data)
    print(item)
} catch {
    print(error)
}

UI表示のためにさらに便利にする

せっかくPrice型という独自型を定義したので、価格表示のための文字列を返すロジックを実装します。ここでは、価格は3桁区切りのカンマをつけ、〜円の形式で表示するというドメインロジックがあると想定します。

Int型を拡張子し、3桁区切りのカンマを付与した文字列を返すwithCommaプロパティを実装します。それを用いて、Price型にformattedStringプロパティを実装します。

extension Int {
    /// 3桁区切りのカンマを付与した文字列を返す
    var withComma: String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        return formatter.string(from: NSNumber(value: self)) ?? "\(self)"
    }
}

struct Price: RawRepresentable, Decodable {
    /// 3桁区切りのカンマをつけ、〜円の形式にした文字列
    var formattedString: String {
        return rawValue.withComma + "円"
    }

    // 〜中略〜
}

formattedStringを用いることで、以下のように価格表示のフォーマットが簡単にできます。

print(item.price1.formattedString) // "1,000円"
print(item.price2.formattedString) // "2,000円"
print(item.price3.formattedString) // "3,000円"
print(item.price4.formattedString) // "4,000円"
6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5