【Swift】Encodableで可変パラメータを追加してみる

# はじめに
この記事はand factory.inc Advent Calendar 2022 13日目の記事です。
昨日は @hagmon の [huskyのpre-commitがSourceTreeからだと失敗する問題の解決方法](https://qiita.com/hagmon/items/f7b8f36d15ff9d166b3a) でした。

関わってるプロジェクトで「ボタンのタップイベントのパラメータで可変パラメータを送ってほしい」という要件がありました。
要件としてはそこまであるあるなものではなく、ニッチな要件になるかなと思いますが、　Encodableを駆使して割と簡単にできましたので、まとめます。

# Encodableに可変パラメータ追加
今回の方法は簡単にいうとCodingKeyを作って、encode時に考慮するやり方でもしかしたら他に方法があるかもしれません。
では、普通のEncodableクラスに可変パラメータを追加していきましょう。

## 一般的なEncodableのクラス
idをリクエストパラメータとして持つEncodableクラスだと以下のように簡単に作成できますね。

```TestRequest.swift
struct TestRequest: Encodable {
    let id: Int
}
```

このクラスを可変パラメータに対応したクラスに変貌させていきましょう。

### カスタムCodingKeyの追加
**キー名を外から貰って作成できる**カスタムCodingKeyを作成します。
encodeのタイミングで利用しますので、idはそのまま使える所ですが、CodingKeyとして定義します。

```TestRequest+CustomCodingKey.swift
// MARK: - CustomCodingKey
extension TestRequest {
    
    private struct CustomCodingKey: CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        
        var intValue: Int?
        init?(intValue: Int) { 
            return nil 
        }
        
        static let id = CustomCodingKey(stringValue: "id")! // idは固定パラメータなので、決め打ち
    }
}
```

### 可変パラメータを受け取る
`TestRequest`クラスに可変パラメータの定義を追加して、初期化時に入れられるようにする
値はStringのみ考慮だが、Cast駆使してAny型もできると思います。

```TestRequest.swift
struct TestRequest: Encodable {
    let id: Int
    let variableParameters: [String: String] // 可変パラメータ定義 ※今回、値はStringのみを考慮する
}
```

### encodeを実装
encodeで先程作成したCodingKeyを用いる

```TestRequest+encode.swift

// MARK: - encode
extension TestRequest {
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CustomCodingKey.self)
        try container.encode(id, forKey: .id)
        for (key, value) in variableParameters {
            if let parameterKey = CustomCodingKey(stringValue: key) {
                try container.encode(value, forKey: parameterKey) // 値の追加
            }
        }
    }
}
```

## 完成
以下で完成になります。
あとはTestRequest作成時にvariableParametersを渡せばOKです。

```TestRequest.swift
struct TestRequest: Encodable {
    let id: Int
    let variableParameters: [String: String]
}

// MARK: - CustomCodingKey
extension TestRequest {
    
    private struct CustomCodingKey: CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        
        var intValue: Int?
        init?(intValue: Int) { 
            return nil 
        }
        
        static let id = CustomCodingKey(stringValue: "id")!
    }
}

// MARK: - encode
extension TestRequest {
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CustomCodingKey.self)
        try container.encode(id, forKey: .id)
        for (key, value) in variableParameters {
            if let parameterKey = CustomCodingKey(stringValue: key) {
                try container.encode(value, forKey: parameterKey)
            }
        }
    }
}
```

# まとめ
できたのはいいものの、結局下記の観点より、保守性が良くないと思ってます。

- 固定パラメータ追加時に追記箇所が多い。
    - CustomCodingKey,encodeの箇所で追記が必要
- 使用してる箇所自体のリクエストパラメータはリクエストクラスみただけではわからない。

個人的には、このようなAPI仕様はAPI自体を見直すべきかなと考えていますが、
プロジェクトの性質上難しい部分もあり、検討が必要なこともあるかと思いますので、誰かのお力になれれば幸いです。