UIPickerView:コンポーネント内の表示内容を更新

今回はUIPickerViewを使用して、以下のことを実装していきます。

※今回はUIPickerViewを3つ使用して実装していきます。
下記はわかりにくいですが、実装画像になります。

f:id:SumJun-Blog:20220213154749p:plain:w200f:id:SumJun-Blog:20220213154951p:plain:w200

環境

Xcode:13.1
Swift:5.5.1

全体

まず全体のコードをお見せします。
Storyboardは使用せず、コードのみの実装になります。

//ViewController.swift:メイン画面

import UIKit

class ViewController: UIViewController {
    
    //MARK: - Property
    
    ///西暦の配列
    private let yearArray = ["2017", "2018", "2019", "2020", "2021"]
    ///月の配列
    private let monthArray = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
    ///日数の配列
    private var dayArray = [String]()
    ///西暦のピッカー
    private var yearPickerView = UIPickerView()
    ///月のピッカー
    private var monthPickerView = UIPickerView()
    ///日数のピッカー
    private var dayPickerView = UIPickerView()
    
    //MARK: - Initialization
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        setPicker()
        setCreateDayArray31()
    }
    ///ピッカーの初期化
    private func setPicker() {
        /*
         PickerView初期化
         各PickerViewのデリゲート指定
         */
        yearPickerView.delegate = self
        yearPickerView.dataSource = self
        monthPickerView.delegate = self
        monthPickerView.dataSource = self
        dayPickerView.delegate = self
        dayPickerView.dataSource = self
        //ピッカーの表示、配置
        let stackView = UIStackView(arrangedSubviews: [yearPickerView, monthPickerView, dayPickerView])
        view.addSubview(stackView)
        yearPickerView.anchor(width: UIScreen.main.bounds.size.width / 3, height: 450)
        stackView.axis = .horizontal
        stackView.distribution = .fillEqually
        stackView.spacing = 0
        stackView.anchor(centerY: view.centerYAnchor, centerX: view.centerXAnchor)
    }
    
    //MARK: - Method
    
    /*
     ■行数(要素数)の切り替えメソッド
     各月で異なる日数に対応するため(2月:28日(29日)、1月:31日など)
     ①配列内の日数を全て削除
     理由:例えば2月は28日分を生成して、3月に切り替えたとき、3月は31日分をそのまま生成してしまうと。28日のあとに31日分が追加で生成されてしまうため。
     ②最初の要素を用意。
     理由:for文は要素がある状態での処理が前提になるため。
     ③新たに生成した配列をピッカーに反映するために更新する。
     */
    ///31日分生成
    private func setCreateDayArray31() {
        dayArray.removeAll()
        dayArray.append("1")
        for day in 0 ..< 30 {
            let newDay = Int(dayArray[day])! + 1
            dayArray.append(String(newDay))
        }
        dayPickerView.reloadAllComponents()
    }
    ///30日分生成
    private func setCreateDayArray30() {
        dayArray.removeAll()
        dayArray.append("1")
        for day in 0 ..< 29 {
            let newDay = Int(dayArray[day])! + 1
            dayArray.append(String(newDay))
        }
        dayPickerView.reloadAllComponents()
    }
    ///28日分生成
    private func setCreateDayArray28() {
        dayArray.removeAll()
        dayArray.append("1")
        for day in 0 ..< 27 {
            let newDay = Int(dayArray[day])! + 1
            dayArray.append(String(newDay))
        }
        dayPickerView.reloadAllComponents()
    }
}

//MARK: - UIPickerViewDelegate, UIPickerViewDataSource
extension ViewController: UIPickerViewDelegate, UIPickerViewDataSource {
    
    ///各Pickerのコンポーネントの数(列数)
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    ///各コンポーネント(列)に並べる要素数(行数)
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        if pickerView == yearPickerView {
            return yearArray.count
        } else if pickerView == monthPickerView {
            return monthArray.count
        } else {
            return dayArray.count
        }
    }
    
    /*
     補足:ただ表示させるだけなら、デリメソtitleForRowでも良い
     ここでは、選択された行のみUI表示を変える処理に対応するために、デリメソviewForRowを使用している
     */
    ///各コンポーネントの表示設定
    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
        //コンポーネントに表示するUI設定
        let componentLabel = UILabel()
        componentLabel.textColor = .gray
        componentLabel.backgroundColor = .clear
        componentLabel.textAlignment = .center
        componentLabel.font = .systemFont(ofSize: 25, weight: .regular)
        //各コンポーネントをUILabelを使用して表示
        if pickerView == yearPickerView {
            componentLabel.text = yearArray[row]
            return componentLabel
        } else if pickerView == monthPickerView {
            componentLabel.text = monthArray[row]
            return componentLabel
        } else {
            componentLabel.text = dayArray[row]
            return componentLabel
        }
    }
    
    ///各コンポーネントの行が選択された時の処理(ドラムロールが止まったとき)
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        //各コンポーネントの選択された行の表示をカスタム
        let selectLabel = pickerView.view(forRow: row, forComponent: component) as? UILabel ?? UILabel()
        selectLabel.textColor = .systemBlue
        selectLabel.backgroundColor = .clear
        selectLabel.textAlignment = .center
        selectLabel.font = .systemFont(ofSize: 25, weight: .regular)
        
        //選択した月毎に選択できる日数を変える
        if pickerView == monthPickerView {
            switch monthArray[row] {
            case "2": //2月
                setCreateDayArray28()
            case "4", "6", "9", "11": //4、6、9、11月
                setCreateDayArray30()
            default: //上記以外の月
                setCreateDayArray31()
            }
        }
    }
}
//View_Extension.swift:各UIの配置設定

import UIKit

extension UIView {
    ///Viewの配置設定
    /// - Parameters:
    ///   - top:基準となる上方向の対象物を設定
    ///   - bottom:基準となる下方向の対象物を設定
    ///   - left:基準となる左方向の対象物を設定
    ///   - right:基準となる右方向の対象物を設定
    ///   - centerY:指定した対象物内におけるY軸による位置を設定
    ///   - centerX:指定した対象物内におけるX軸による位置を設定
    ///   - width:幅の設定
    ///   - height:高さの設定
    ///   - topPadding:基準となる上方向の対象物との距離を設定
    ///   - bottomPadding:基準となる下方向の対象物との距離を設定
    ///   - leftPadding:基準となる左方向の対象物との距離を設定
    ///   - rightPadding:基準となる右方向の対象物との距離を設定
    func anchor(top: NSLayoutYAxisAnchor? = nil,
                bottom: NSLayoutYAxisAnchor? = nil,
                left: NSLayoutXAxisAnchor? = nil,
                right: NSLayoutXAxisAnchor? = nil,
                centerY: NSLayoutYAxisAnchor? = nil,
                centerX: NSLayoutXAxisAnchor? = nil,
                width: CGFloat? = nil,
                height: CGFloat? = nil,
                topPadding: CGFloat = 0,
                bottomPadding: CGFloat = 0,
                leftPadding: CGFloat = 0,
                rightPadding: CGFloat = 0) {
        
        self.translatesAutoresizingMaskIntoConstraints = false
        
        if let top = top {
            self.topAnchor.constraint(equalTo: top, constant: topPadding).isActive = true
        }
        
        if let bottom = bottom {
            self.bottomAnchor.constraint(equalTo: bottom, constant: -bottomPadding).isActive = true
        }
        
        if let left = left {
            self.leftAnchor.constraint(equalTo: left, constant: leftPadding).isActive = true
        }
        
        if let right = right {
            self.rightAnchor.constraint(equalTo: right, constant: -rightPadding).isActive = true
        }
        
        if let centerY = centerY {
            self.centerYAnchor.constraint(equalTo: centerY).isActive = true
        }
        
        if let centerX = centerX {
            self.centerXAnchor.constraint(equalTo: centerX).isActive = true
        }
        
        if let width = width {
            self.widthAnchor.constraint(equalToConstant: width).isActive = true
        }
        
        if let height = height {
            self.heightAnchor.constraint(equalToConstant: height).isActive = true
        }
    }
}


コンポーネントで選択された行だけUI表示を変える

選択された行だけ表示内容を変えるには、コンポーネントに含まれる全ての行(要素)をUIViewで表示させる必要があるので、まずはデリメソのviewForRowメソッドを使用してコンポーネントに含まれる要素をUILabelで表示する処理を書きます。

///各コンポーネントの表示設定
    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
        //コンポーネントに表示するUI設定
        let componentLabel = UILabel()
        componentLabel.textColor = .gray
        componentLabel.backgroundColor = .clear
        componentLabel.textAlignment = .center
        componentLabel.font = .systemFont(ofSize: 25, weight: .regular)
        //各コンポーネントをUILabelを使用して表示
        if pickerView == yearPickerView {
            componentLabel.text = yearArray[row]
            return componentLabel
        } else if pickerView == monthPickerView {
            componentLabel.text = monthArray[row]
            return componentLabel
        } else {
            componentLabel.text = dayArray[row]
            return componentLabel
        }
    }

UILabelの設定をしたら、各ピッカーに反映させるために、ピッカーの場合分けで処理を書いていきます。
次はデリメソdidSelectRow内で「pickerView.view(forRow: row, forComponent: component)」を使用して、デリメソviewForRowで実装したUILabelと連動させます。あとはお好みでのカスタムになります。

//各コンポーネントの選択された行の表示をカスタム
        let selectLabel = pickerView.view(forRow: row, forComponent: component) as? UILabel ?? UILabel()
        selectLabel.textColor = .systemBlue
        selectLabel.backgroundColor = .clear
        selectLabel.textAlignment = .center
        selectLabel.font = .systemFont(ofSize: 25, weight: .regular)


コンポーネント(ピッカー)の選択された行(表示内容)で、別コンポーネント(別のピッカー)の表示内容を切り替える

同じくデリメソdidSelectRow内に処理を書いていきます。

//選択した月毎に選択できる日数を変える
        if pickerView == monthPickerView {
            switch monthArray[row] {
            case "2": //2月
                setCreateDayArray28()
            case "4", "6", "9", "11": //4、6、9、11月
                setCreateDayArray30()
            default: //上記以外の月
                setCreateDayArray31()
            }
        }

まずはピッカーのif文で月のUIPickerViewを指定し、その中でswitch文による「月」の場合分けをし、各月ごとにsetCreateDayArray〜〜メソッド(dayArray配列に1ヶ月分の日数を生成する処理)で日数を生成し、日数のピッカーに反映させます。
ここで以下のことがポイントになります。

  • ①新たに配列要素を生成する際は、既存の要素を削除しないと「既存要素+新要素」になってしまうこと
  • ②reloadAllComponentsをし忘れると、配列の変更が反映されないこと
    ①に関しては、今回は配列内の要素を全て削除して新たに要素を生成する処理を書いておりますが、要素を指定して削除したり、付け足したりする処理の方が楽かもしれません。
///31日分生成
    private func setCreateDayArray31() {
        dayArray.removeAll()
        dayArray.append("1")
        for day in 0 ..< 30 {
            let newDay = Int(dayArray[day])! + 1
            dayArray.append(String(newDay))
        }
        dayPickerView.reloadAllComponents()
    }
    ///30日分生成
    private func setCreateDayArray30() {
        dayArray.removeAll()
        dayArray.append("1")
        for day in 0 ..< 29 {
            let newDay = Int(dayArray[day])! + 1
            dayArray.append(String(newDay))
        }
        dayPickerView.reloadAllComponents()
    }
    ///28日分生成
    private func setCreateDayArray28() {
        dayArray.removeAll()
        dayArray.append("1")
        for day in 0 ..< 27 {
            let newDay = Int(dayArray[day])! + 1
            dayArray.append(String(newDay))
        }
        dayPickerView.reloadAllComponents()
    }


これで完了です。

未解決な部分

下記画像のように月の切替後の日数のピッカーを確認すると、reloadAllComponentsで再初期化されたためか、選択されてもその行のみのUI表示が反映されておりません。UIPickerViewの初期化状態で選択されている行は、選択されていない認識になっております。ここの解決方法に関しては今のところ見つかっておりませんので、他で調べてみてください。

f:id:SumJun-Blog:20220213162620p:plain:w250
以上になります。最後まで読んでいただきありがとうございます。