0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SwiftUI で UIPickerView を使う

Last updated at Posted at 2021-03-03

はじめに

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

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

スクリーンショット 2021-03-08 11.23.22.png

課題

 実装するにあたり以下が課題となりました。

  1. SwiftUI と UIPickerView の連携
  2. ラベルを固定する
  3. Picker から値を受け渡す

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

実装

 実装を順に説明します。

SwiftUI と UIPickerView の連携

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

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

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 のサブビューとします。(個人的には他にもっと簡単なやり方があるのではないかと疑ってしまうのですが)

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 から値を受け渡す

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

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))
    }
}

おわりに

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

参考

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

0
3
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
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?