【Swift】いろんな型に対応した万能な編集画面を作りたかった

# はじめに
題名の通り。　
KeyPathを指定すると、型に応じて適切な編集画面を出してくれるSwifUIのViewを作りたかった。　
こんな感じの構造体があった時に 
```swift 
struct Item {
    var title: String
    var count: Int
    var color: UIColor
    var date: Date?
}

var item = Item(
    title: "こんにちは",
    count: 0,
    color: .red,
    date: Date()
)
```

こんな感じで、itemとkeyPathを指定すると、適切な編集UIに振り分けてくれる感じ 
```swift 
EditValueView(item, keyPath: \.title)
EditValueView(item, keyPath: \.count)
EditValueView(item, keyPath: \.color)
EditValueView(item, keyPath: \.date)
```






今回、自分が考えた、型とその対応する編集画面のUIを列挙していく。　

# 本文 
## 型と編集画面　
### String
`TextEditor`(`UITextView`)をそのまま利用した　
<img src="https://user-images.githubusercontent.com/50244599/197402681-7e3c4ec8-f7c3-4ad7-9e31-8e3cd270342f.png" width=414px>


### Bool　
`Toggle`(`UISwitch`)をそのまま利用した　
<img src="https://user-images.githubusercontent.com/50244599/197402668-973d18c4-9f87-4f2f-9e6c-77072b4a8db6.png" width=414px>

### Date 
`DatePicker`をgraphicalスタイルで利用した　
<img src="https://user-images.githubusercontent.com/50244599/197402673-414f5b2d-9031-4ad3-81de-300d85e5ad56.png" width=414px>
 
### Color, UIColor, NSColor, CGColor, CIColor 
`ColorPicker`を利用した。　
`ColorPicker`はSwiftUIの`Color`と`CGColor`にしか対応していないので、その他の型は一度`CGColor`に変換して表示している。　
<img src="https://user-images.githubusercontent.com/50244599/197402671-8a224878-ab39-4471-b072-cbb19a2d38b9.png" width=414px>

### Enum(CaseIterable)
せっかく選択肢を全て取得できるので、`Picker`を採用した。
<img src="https://user-images.githubusercontent.com/50244599/197402679-c6be841f-02ca-4db6-81ba-5e5e4893058d.png" width=414px>

### Enum(CaseIterable & RawRepresentable)
せっかくrawValueも取得できるならと、一緒に表示することにした。 
<img src="https://user-images.githubusercontent.com/50244599/197402678-dc8547ec-add7-436c-8cba-44d950f0d676.png" width=414px>


### Codable
jsonとの相互変換が可能なので、これを利用してString形式で編集できるようにした。　
ただ、型のプロパティを知らないと、どんなKeyが存在してどんな形式でJSONを入力すれば良いかわからないので、ヒントとしてプロパティの情報を`Mirror`により取得して表示するようにしている　
(正確にはCodingKeyの実装によっては間違っている場合もある。)
<img src="https://user-images.githubusercontent.com/50244599/197402669-5fe684df-cbbe-4945-b89e-264e00fed733.png" width=414px>


### Int, Double, Array, Dictionary...
`Codable`に対応したことによって、自動的にこれらにも対応できた。

|  Int  |  Double  |  Array  |  Dictionary  |
| ---- | ---- | ---- |  ----  |
|  ![Int](https://user-images.githubusercontent.com/50244599/197402680-eb91f16f-52db-441a-b923-706889c256f8.png)  |  ![Double](https://user-images.githubusercontent.com/50244599/197402677-cb2a90ca-58fa-4d2d-8459-fa2539836c36.png)  |  ![Array](https://user-images.githubusercontent.com/50244599/197402664-fce3326c-824d-4853-9a5b-47903ccdf470.png)  |  ![Dictionary](https://user-images.githubusercontent.com/50244599/197402675-d1dd4bdb-6135-4c45-89f9-2f640daf9f3d.png)  |





## 実装
こんな感じでGenericsを用いて定義し、Valueの型に応じて適切な編集画面を出し分ける。

```swift 
public struct EditValueView<Root, Value>: View {

    var body: some View {}

}
```
また、Optionalの扱いについて、今回は以下のように出来るだけinitializerで吸収して無視するように実装した。

```swift 
@_disfavoredOverload
public init(_ target: Root, key: String, keyPath: WritableKeyPath<Root, Value>) {
    // 省略
}

public init(_ target: Root, key: String, keyPath: WritableKeyPath<Root, Optional<Value>>) where Value: DefaultRepresentable {
    // 省略
}

public init(_ target: Root, key: String, keyPath: WritableKeyPath<Root, Optional<Value>>, defaultValue: Value) {
    // 省略
}
```
`DefaultRepresentable`プロトコルはデフォルト値をもつ型であるということを示すために作成した　
```swift 
public protocol DefaultRepresentable {
    static var defaultValue: Self { get }
}
```

### 型判定　
こんな感じのゴリ押し実装で分岐している　
```swift 
    @ViewBuilder
    var editor: some View {
        switch $value {
        case let v as Binding<String>:
            TextEditor(text: v)
                .border(.black, width: 0.5)
            
        case let v as Binding<Bool>:
            Toggle(key, isOn: v)
                .padding()
                .border(.black, width: 0.5)
            
        case _ where (value as? NSNumber) != nil:
            CodableEditorView($value, key: key, isValidType: $isValidType, textStyle: .single)
            
        case let v as Binding<Date>:
            DateEditorView(v, key: key)
            
        case let v as Binding<Color>:
            ColorEditorView(v, key: key)

        case let v as Binding<CGColor>:
            ColorEditorView(v, key: key)

        case let v as Binding<NSUIColor>:
            ColorEditorView(v, key: key)

        case let v as Binding<CIColor>:
            ColorEditorView(v, key: key)
            
        case _ where Value.self is any CaseIterable.Type:
            CaseIterableEditor($value, key: key)
                .border(.black, width: 0.5)
            
        case _ where Value.self is any Codable.Type:
            CodableEditorView($value, key: key, isValidType: $isValidType)
            
        default:
            Text("this type is currently not supported.")
        }
    }

```

# さいごに　
今回作成したViewはライブラリとして[ここ](https://github.com/p-x9/EditValueView)に供養してあります。　
[p-x9/EditValueView](https://github.com/p-x9/EditValueView) 

今回記事に載せたコード以外でも各所で色々な工夫してあるので是非リポジトリ覗いてみてください。　
そして改善案とかプルリクください。

実際に[このリポジトリ](https://github.com/p-x9/AppContainer)とかで利用しています。　

ありがとうございました。