35

I have two UIPickerControllers in one view controller. I can get one to work, but when I add a second, my app crashes. Here is the code I use for one picker view:

import UIKit

class RegisterJobPosition: UIViewController, UIPickerViewDelegate {

    @IBOutlet weak var positionLabel: UILabel!

    var position = ["Lifeguard", "Instructor", "Supervisor"]

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    func numberOfComponentsInPickerView(PickerView: UIPickerView!) -> Int
    {
        return 1
    }

    func pickerView(pickerView: UIPickerView!, numberOfRowsInComponent component: Int) -> Int
    {
        return position.count
    }

    func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String!
    {
        return position[row]
    }

    func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {        
        positionLabel.text = position[row]
    }
}

Now, how can I get a second picker to work? Say my second picker view is called location (the other one is called position). I tried duplicating the code within the picker view methods for location but it doesn't work.

shim
  • 9,289
  • 12
  • 69
  • 108
dom999999
  • 447
  • 1
  • 7
  • 11
  • You'll need a way to distinguish between the two of them if your view controller is the sole delegate for handling user interaction. Can you post the code you're using to differentiate them? – andrewcbancroft Dec 24 '14 at 21:14
  • I just duplicate the code. For example, in the numberOfRowsInComponent method, I just put: return location.count – dom999999 Dec 24 '14 at 21:19
  • That part makes sense. But how are you programmatically figuring out which picker instance you're returning a count for? If you've got two picker instances sending messages to your single view controller (delegate), you need a way to identify the pickers so you can do appropriate branch logic and return the right count for the right picker. – andrewcbancroft Dec 24 '14 at 21:27
  • Ah....I wouldn't be able to set a "dummy" variable to tell the app which picker I want because I want both to be available at the same time.....how else could I do it? – dom999999 Dec 24 '14 at 21:31
  • I wrote an article on distinguishing between multiple UIActionSheets by using their tag property: http://bit.ly/DistinguishUIActionSheetsSwift -- While not exactly the same, the scenarios are similar. – andrewcbancroft Dec 24 '14 at 21:39
  • Thanks for the link...what you wrote makes sense so I'll pull out bits and pieces when I get a chance :) – dom999999 Dec 24 '14 at 21:48
  • My pleasure. If it still isn't working, try updating the question with more specifics on the error, and new code snippets with your branching logic. – andrewcbancroft Dec 24 '14 at 21:50

6 Answers6

79

Here is my solution:

  • in the storyboard, add two UIPickerView instances to your view
  • set the first picker's tag as 1 and set 2 for the second picker under the "Attributes Inspector"
  • control + drag from each picker to the top yellow view controller icon and choose dataSource. Repeat the same choosing delegate
  • add UIPickerViewDataSource and UIPickerViewDelegate to your view controller:

    class ViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate {
    
  • in your view controller class, create empty arrays for the pickers:

    var picker1Options = []
    var picker2Options = []
    
  • In viewDidLoad(), populate the arrays with your content:

    picker1Options = ["Option 1","Option 2","Option 3","Option 4","Option 5"]
    picker2Options = ["Item 1","Item 2","Item 3","Item 4","Item 5"]
    
  • implement the delegate and data source methods:

    func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        if pickerView.tag == 1 {
            return picker1Options.count
        } else {
            return picker2Options.count
        }
    }
    
    func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
        if pickerView.tag == 1 {
            return "\(picker1Options[row])"
        } else {
            return "\(picker2Options[row])"
        }
    }
    
shim
  • 9,289
  • 12
  • 69
  • 108
LAOMUSIC ARTS
  • 642
  • 2
  • 10
  • 15
  • 2
    Thanks for tag suggestion,which really help me alot using multiple picker.I got solved now. – Thiha Aung Jul 01 '15 at 03:55
  • This blows my mind off how difficult it is to fill multiple PickerViews in the same ViewController. On Android its super simple. – Lazar Kukolj Aug 02 '16 at 18:21
  • 5
    Lazar K that's a very hateful comment. It is not simpler nor more complex. It's just a different strategy. On Android you would (or should) use different adapters for different Spinners. Actually you can use different Data Sources and Delegates on iOS too. I would actually suggest this in an answer – Matei Suica Sep 07 '16 at 00:13
  • Rather than using tags, which are error prone, you can just have two picker view properties (eg `@IBOutlet var locationPickerView` and `positionPickerView`) in the class and check if `pickerView` in the delegate/data source methods is equal to either property. – shim Apr 05 '19 at 04:10
  • Similar to [Diavel's](https://stackoverflow.com/a/44320078/1032372) answer. – shim Apr 05 '19 at 04:18
30

Based on the information I have in the question, I'd say that you need to set up the data source & delegate methods to handle the ability to distinguish between which picker instance is calling them.

Using the tag property on the picker view is one strategy.

There should be some if/else or switch statements in the methods that have varying logic depending on whether it's the location or the position picker that's being referenced.

Matt
  • 179
  • 7
andrewcbancroft
  • 941
  • 1
  • 9
  • 11
  • 2
    Regarding the if/else statements, how would I even get a variable to become a value that tells the location or position picker to be used? – dom999999 Dec 24 '14 at 21:57
  • 1
    So, if you set the tag on the pickers in the storyboard, you can test it in the callback method. Do you see that the first parameter in the method is a pickerView? You can check pickerView.tag and perform the right logic based on the tag's correlation to your UI and your data array. – andrewcbancroft Dec 24 '14 at 22:21
12

I found this to work.

class SecondViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {

    @IBOutlet weak var textbox1: UILabel!
    @IBOutlet weak var textbox2: UILabel!

    @IBOutlet weak var dropdown1: UIPickerView!
    @IBOutlet weak var dropdown2: UIPickerView!

    var age = ["10-20", "20-30", "30-40"]
    var Gender = ["Male", "Female"]

    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        var countrows : Int = age.count
        if pickerView == dropdown2 {
            countrows = self.Gender.count
        }

        return countrows
    }

    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        if pickerView == dropdown1 {
            let titleRow = age[row]
             return titleRow
        } else if pickerView == dropdown2 {
            let titleRow = Gender[row]
            return titleRow
        }

        return ""
    }

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        if pickerView == dropdown1 {
            self.textbox1.text = self.age[row]
        } else if pickerView == dropdown2 {            
            self.textbox2.text = self.Gender[row]
        }
    }
}
McCygnus
  • 4,187
  • 6
  • 34
  • 30
Diavel Rider
  • 351
  • 3
  • 6
2

My background is in Android but my answer is very OOP. I would suggest creating different classes to implement the DataSource and Delegate like this:

class PositionDataSourceDelegate : NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
   var position = ["Lifeguard", "Instructor", "Supervisor"]
   var selectedPosition : String?

    func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
       return 1
    }

    func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
      return position.count
    }

    func pickerView(pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
       return position[row]
    }

    func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
       selectedPosition = position[row]
    }
}

and then another one for the Location:

class LocationDataSourceDelegate : NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
   var location = ["Up", "Down", "Everywhere"]
   var selectedLocation : String?

    func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
        return 1
    }

    func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return location.count
    }

    func pickerView(pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
         return location[row]
    }

    func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        selectedLocation = location[row]
   }
}

then in your RegisterJobPosition you need to create an instance of each:

let positionDSD = PositionDataSourceDelegate()
let locationDSD = LocationDataSourceDelegate()

and assign them to the pickers like this:

positionPicker.dataSource = positionDSD
positionPicker.delegate = positionDSD
locationPicker.dataSource = locationDSD
locationPicker.delegate = locationDSD

and you can access the selected position and location using:

positionDSD.selectedPosition 
locationDSD.selectedLocation

Hope this helps you and others and I'm also hoping for some constructive comments of why this is not "swifty"

Matei Suica
  • 859
  • 1
  • 7
  • 17
  • 1
    It's a good OO solution, but it doesn't actually work in practice—I'm an iOS newcomer, but in trying to solve exactly this problem all day, and it appears to me that a UIPickerViewDelegate/DataSource must also be a UIViewController. A plain NSObject doesn't work. – Fishbreath Mar 20 '17 at 21:01
  • 1
    `UIPickerViewDelegate` and `UIPickerViewDataSource` can be applied to any `NSObject`. Just tested it out and it seems to work fine. See no reason why these particular protocols wouldn't work for non-UIViewControllers, as that goes against how the SDK is designed. Breaking out data sources into their own classes is a great idea that not enough iOS developers are doing. – shim Apr 05 '19 at 04:15
  • 1
    You can take Matei's suggestion one step further if you are using Storyboards.Drag an "Object" onto your VC and give it type PositionDataSourceDelegate.Create an outlet in your VC code connected to this object (Open assistant window next to storyboard and CTRL drag from Storyboard to VC code. You can call the outlet positionDSD ). Connect Picker's datasource and delegate methods to the object. Finally delete the let positionDSD =.. and positionPicker.dataSource =... positionPicker.delegate = – greg Mar 29 '21 at 14:01
1

I think the biggest issue and different to Java is that Java easily allow for attributes to be passed through the constructor. e.g. you could declare class LocationDataSourceDelegate as generic and call it genericDataSourceDelegate and make the constructor accept and Array public genericDataSourceDelegate (String data[]) and be able to make one class where could just simply create objects of. You just instantiate it and pass location the constructor like genericDataSourceDelegate (location)

The problem with your model you will have to create as many delegate classes in one program which is a strain to your compiler.

1

You should NEVER use tags!!!!!

The best way for me was:

  1. Create an enum, for example MyPickerViewType
    enum MyPickerViewType {
        case first
        case second
    }
  1. Create CustomPickerView class that conforms to UIPickerView and add type parameter
    class CustomPickerView: UIPickerView {
        let type: MyPickerViewType
    
        init(type: MyPickerViewType) {
            self.type = type
            super.init(frame: .zero)
        }
    
        required init?(coder: NSCoder) {
            fatalError("init(coder:) is not supported")
        }
    }
  1. Initialise custom pickerViews in UIViewController
    private lazy var firstPickerView: TimeLogCardPickerView = {
        let pickerView = TimeLogCardPickerView(type: .first)
        pickerView.delegate = self
        pickerView.dataSource = self
        return pickerView
    }()
    
    private lazy var secondPickerView: TimeLogCardPickerView = {
        let pickerView = TimeLogCardPickerView(type: .second)
        pickerView.delegate = self
        pickerView.dataSource = self
        return pickerView
    }()
  1. Usage
    extension ViewController: UIPickerViewDelegate, UIPickerViewDataSource {
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            return 1
        }
    
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            let customPickerView = pickerView as? CustomPickerView

            switch customPickerView?.type {
            case .first:
                return 2
            case .second:
                return 5
            }
        }
    
        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            let customPickerView = pickerView as? CustomPickerView

            switch customPickerView?.type {
            case .first:
                return firstTitleArray[row]
            case .second:
                return secondTitleArray[row]
            }
        }
    }