Search code examples
iosswiftuiviewuipickerviewtouch-event

UIPickerView did pass by row


I have this demo app:

As you can see, the alpha of the background is changing to black according to the value.

But the problem is that there is no smooth transition:

As you can see from the GIF, the background is only changing after the scrolling is over. And I don't want it to be like that.

this is my code:

class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {

    @IBOutlet weak var pickerView: UIPickerView!
    @IBOutlet weak var backView: UIView!
    
    let max = 100
    
    override func viewDidLoad() {
        super.viewDidLoad()

        pickerView.delegate = self
        pickerView.dataSource = self
    }
    
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return max + 1 // To include '0'
    }
    
    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
        
        let l = UILabel(frame: .zero)
        l.text = String(max - row)
        l.textColor = .white
        
        l.font = UIFont.preferredFont(forTextStyle: .title3)
        l.textAlignment = .center
        
        return l
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        backView.alpha = CGFloat(max - row) / 100
    }


}

I am giving the delegate UIView instead of String, because I had an idea: to test every time the location of each row is changing, and when the row is in the middle of the screen I should update the background. But unfortunately I don't know how to do that.

Maybe you can help? or perhaps suggest any other ideas?

Thank you!


Solution

  • The delegate method didSelectRow is only called when the rolling stops, so this is not the place you should update your alpha. UIPickerView has no delegate method which will notify you about the change during the rolling, however the UIPickerView will call your data source and delegate methods to get the title or in your case the view for a given row so it can be displayed as the user scrolls. So what you should do is just move your alpa changing logic there:

    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
        backView.alpha = CGFloat(max - row) / 100
    }
    

    Note that this delegate method will be called when the UIPickerView is loaded, so maybe you should disable the alpha changing until view is not layout out correctly(maybe viewDidAppear will do it).

    As the delegate method can sometimes behave unexpectedly(calling not just the next lines, but any line from the picker), we should store and also check if the row is just one step ahead or behind the last saved value, otherwise we should just ignore that. I made a simple demo to demonstrate how it works:

    import UIKit
    
    class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
        private let pickerView = UIPickerView()
        private var isPickerReady = false
        private var lastValue = 0
        private let max = 100
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            view.addSubview(pickerView)
            pickerView.translatesAutoresizingMaskIntoConstraints = false
    
            pickerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
            pickerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
            pickerView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
            pickerView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
    
    
            pickerView.delegate = self
            pickerView.dataSource = self
        }
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            isPickerReady = true
        }
    
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            return 1
        }
    
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            return max
        }
    
        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            // Do not update the value until the view is not loaded 
            // Only consider delegate methods which are one step ahead or behind the last value
            if row + 1 == lastValue && isPickerReady || row - 1 == lastValue && isPickerReady {
                lastValue = row
                view.alpha = CGFloat(max - lastValue ) / 100
            }
            return "Tiltle \(row)"
        }
    }