SwiftUI で UIPickerView を使う

# はじめに
　iPhone 標準の時計アプリのタイマーの時間設定のような Picker を使いたかったのですが SwiftUI の Picker ではそのような見た目にはできないようだったので、UIPickerView で実装することにしたのですが、SwiftUI と組み合わせる情報が見つかりませんでした。そんな中でなんとか実装できたので、記録を残しておきます。

　ちなみにいまさらながら初めての iPhone ネイティブアプリ開発でした。

![スクリーンショット 2021-03-08 11.23.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13938/b7275707-4106-bf8d-3b64-648c6fdd923e.png)

# 課題
　実装するにあたり以下が課題となりました。
1. SwiftUI と UIPickerView の連携
2. ラベルを固定する
3. Picker から値を受け渡す

これを踏まえて実装を順に紹介していきます。

# 実装
　実装を順に説明します。

## SwiftUI と UIPickerView の連携
　まず、ガワだけを示すと次のようなコードになります。

```swift
import SwiftUI

struct TimePickerView: UIViewRepresentable {
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: Context) -> UIPickerView {
        let picker = UIPickerView()
        picker.dataSource = context.coordinator
        picker.delegate = context.coordinator
        
        return picker
    }
    
    func updateUIView(_ uiView: UIPickerView, context: Context) {
    }
    
    class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
        var timerPickerView: TimePickerView

        init(_ view: TimePickerView) {
            self.timerPickerView = view
        }
        
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            <#code#>
        }
        
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            <#code#>
        }

        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            <#code#>
        }
    }
}

struct TimePickerView_Previews: PreviewProvider {
    static var previews: some View {
        TimePickerView()
    }
}
```

## Picker の値を選択できるようにする
　次に Picker の値を初期化して選択できるようにします。ただし、これだと問題があって時、分、秒のラベルも操作できてしまいます。これは次で修正をします。

```swift
import SwiftUI

struct TimePickerView: UIViewRepresentable {
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: Context) -> UIPickerView {
        let picker = UIPickerView()
        picker.dataSource = context.coordinator
        picker.delegate = context.coordinator
        
        return picker
    }
    
    func updateUIView(_ uiView: UIPickerView, context: Context) {
    }
    
    class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
        var timerPickerView: TimePickerView
        var dataList = [["0"], [""], ["0"], [""], ["0"], [""]]

        init(_ view: TimePickerView) {
            self.timerPickerView = view
            
            for i in 1...23 {
                dataList[0].append("\(i)")
            }
            for i in 1...59 {
                dataList[2].append("\(i)")
            }
            for i in 1...59 {
                dataList[4].append("\(i)")
            }
        }
        
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            return dataList.count
        }
        
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            return dataList[component].count
        }
        
        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            return dataList[component][row]
        }
    }
}

struct TimePickerView_Previews: PreviewProvider {
    static var previews: some View {
        TimePickerView()
    }
}
```

## ラベルを固定する
 ラベルを固定するために `UILabel` を生成して `UIPickerView` のサブビューとします。（個人的には他にもっと簡単なやり方があるのではないかと疑ってしまうのですが）

```swift
import SwiftUI

struct TimePickerView: UIViewRepresentable {
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: Context) -> UIPickerView {
        let picker = UIPickerView()
        picker.dataSource = context.coordinator
        picker.delegate = context.coordinator

        let fontSize:CGFloat = 20
        let labelWidth:CGFloat = picker.frame.size.width / CGFloat(picker.numberOfComponents)
        let y:CGFloat = (picker.frame.size.height / 2) - (fontSize / 2)

        for i in [1, 3, 5] {
            let label = UILabel()
            switch i {
            case 1:
                label.text = "時"
            case 3:
                label.text = "分"
            default:
                label.text = "秒"
            }
            label.frame = CGRect(x: labelWidth * CGFloat(i), y: y, width: labelWidth, height: fontSize)
            label.font = UIFont.systemFont(ofSize: fontSize, weight: .light)
            label.backgroundColor = .clear
            label.textAlignment = NSTextAlignment.center
            picker.addSubview(label)
        }
                
        return picker
    }
    
    func updateUIView(_ uiView: UIPickerView, context: Context) {
    }
    
    class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
        var timerPickerView: TimePickerView
        var dataList = [["0"], [""], ["0"], [""], ["0"], [""]]

        init(_ view: TimePickerView) {
            self.timerPickerView = view
            
            for i in 1...23 {
                dataList[0].append("\(i)")
            }
            for i in 1...59 {
                dataList[2].append("\(i)")
            }
            for i in 1...59 {
                dataList[4].append("\(i)")
            }
        }
        
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            return dataList.count
        }
        
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            return dataList[component].count
        }
        
        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            return dataList[component][row]
        }
    }
}

struct TimePickerView_Previews: PreviewProvider {
    static var previews: some View {
        TimePickerView()
    }
}
```

## Picker から値を受け渡す
　値を受け渡すために状態変数を用意します。ただし、アニメーションが終わったタイミングで値が確定されるということに注意が必要です。（アニメーションを中断して値を確定する方法をご存知の方がいれば教えてください）

```swift
import SwiftUI

struct TimePickerView: UIViewRepresentable {
    @Binding var hour: Int
    @Binding var minute: Int
    @Binding var second: Int
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: Context) -> UIPickerView {
        let picker = UIPickerView()
        picker.dataSource = context.coordinator
        picker.delegate = context.coordinator

        let fontSize:CGFloat = 20
        let labelWidth:CGFloat = picker.frame.size.width / CGFloat(picker.numberOfComponents)
        let y:CGFloat = (picker.frame.size.height / 2) - (fontSize / 2)

        for i in [1, 3, 5] {
            let label = UILabel()
            switch i {
            case 1:
                label.text = "時"
            case 3:
                label.text = "分"
            default:
                label.text = "秒"
            }
            label.frame = CGRect(x: labelWidth * CGFloat(i), y: y, width: labelWidth, height: fontSize)
            label.font = UIFont.systemFont(ofSize: fontSize, weight: .light)
            label.backgroundColor = .clear
            label.textAlignment = NSTextAlignment.center
            picker.addSubview(label)
        }
                
        return picker
    }
    
    func updateUIView(_ uiView: UIPickerView, context: Context) {
    }
    
    class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
        var timerPickerView: TimePickerView
        var dataList = [["0"], [""], ["0"], [""], ["0"], [""]]

        init(_ view: TimePickerView) {
            self.timerPickerView = view
            
            for i in 1...23 {
                dataList[0].append("\(i)")
            }
            for i in 1...59 {
                dataList[2].append("\(i)")
            }
            for i in 1...59 {
                dataList[4].append("\(i)")
            }
        }
        
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            return dataList.count
        }
        
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            return dataList[component].count
        }
        
        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            return dataList[component][row]
        }
        
        func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
            switch component {
            case 0:
                timerPickerView.hour = row
            case 2:
                timerPickerView.minute = row
            case 4:
                timerPickerView.second = row
            default: break
            }
        }
    }
}

struct TimePickerView_Previews: PreviewProvider {
    static var previews: some View {
        TimePickerView(hour: .constant(0), minute: .constant(0), second: .constant(0))
    }
}
```

# おわりに
　試行錯誤した割にはソースコードは大した分量ではなかったですね。同じようなことで困った方のお役に立てば幸いです。

# 参考
　実装にあたり、以下の記事を参考にさせていただきました。

https://qiita.com/SonoRi/items/68b14247989ea1c0c47d