5

I have a view with three picker views in it. Two of the picker views have the same data, an array with the numbers 1 to 100. The third picker view has an array with a list of model railroad track manufacturers in it. I have tagged the picker views using a method I found on this site, but when I run the app, all three picker views have 1 to 100 as their data. I also control-dragged from all picker views to the yellow circle at the top of the view and clicked dataSource and delegate. How do I use multiple picker views with different data sources in one view? Also, in order to make the code run, I had to delete weak from all @IBOutlet statements relating to the picker views. Is this a bad thing to do? I am relatively new to code. Thanks.

Picker View Scene Screen Shot

import UIKit

class ViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate {
    //MARK: Properties

    @IBOutlet var layoutLengthPickerView: UIPickerView!
    @IBOutlet var layoutWidthPickerView: UIPickerView!
    @IBOutlet var trackPickerView: UIPickerView!

    override func viewDidLoad() {
        super.viewDidLoad()

        layoutLengthPickerView = UIPickerView()
        layoutWidthPickerView = UIPickerView()
        trackPickerView = UIPickerView()

        layoutLengthPickerView.tag = 0
        layoutWidthPickerView.tag = 1
        trackPickerView.tag = 2
    }

    let numbers = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "100"]

    let manufacturers = ["Atlas True Track", "Atlas Code 100", "Atlas Code 83", "Bachmann Nickel Silver", "Bachmann Steel Alloy", "Kato", "Life-Like Trains Code 100", "LIfe-Like Trains Power-Loc", "Peco Code 100", "Peco Code 83", "Peco Code 75", "Shinohara Code 100", "Shinohara Code 70", "Walthers"]

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

    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        if pickerView.tag == 0 {
            return numbers[row]
        } else if pickerView.tag == 1 {
            return numbers[row]
        } else if pickerView.tag == 2 {
            return manufacturers[row]
        }

        return ""
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        if pickerView.tag == 0 {
            return numbers.count
        } else if pickerView.tag == 1 {
            return numbers.count
        } else if pickerView.tag == 2 {
            return manufacturers.count
        }
        return 1
    }
}
rmaddy
  • 314,917
  • 42
  • 532
  • 579
TonyStark4ever
  • 848
  • 1
  • 9
  • 24
  • 1
    Get rid of the three lines in `viewDidLoad` that create new picker views. Those lines mess up the ones you created in your storyboard. – rmaddy May 25 '17 at 03:34
  • 1
    Also dont use `tag`, use something like `pickerView == layoutLengthPickerView` – Tj3n May 25 '17 at 03:40
  • You can also entertain having a single picker with multiple components. Sometimes having multiple pickers can result in a confusing UX. – Rob May 25 '17 at 04:37

3 Answers3

7

When dealing with multiple controls with delegates and data sources, you should consider avoiding view controller bloat (i.e., in the spirit of the single responsibility principle) by creating separate objects for the delegates of the multiple pickers. This keeps this logic out of the view controller, itself, and avoids single cumbersome UIPickerViewDataSource and UIPickerViewDelegate methods trying to service multiple pickers with hairy if-else or switch statements.

For example, here is a view controller that has outlets for two pickers, but rather than encumbering the view controller with code to manage the dataSource and delegate for these pickers, you can have separate objects for each picker, and all the view controller has to do is to say which delegate object will handle which picker:

class ViewController: UIViewController {

    @IBOutlet weak var namePicker: UIPickerView!
    @IBOutlet weak var numberPicker: UIPickerView!

    let namePickerDelegate = NamePickerDelegate()
    let numberPickerDelegate = NumberPickerDelegate()

    override func viewDidLoad() {
        super.viewDidLoad()

        namePicker.delegate = namePickerDelegate
        namePicker.dataSource = namePickerDelegate

        numberPicker.delegate = numberPickerDelegate
        numberPicker.dataSource = numberPickerDelegate
    }

    @IBAction func didTapButton(_ sender: Any) {
        let nameValue = namePicker.selectedRow(inComponent: 0)
        let numberValue = numberPicker.selectedRow(inComponent: 0)

        print("\(nameValue); \(numberValue)")
    }

}

The only trick is to make sure to keep a strong reference to those delegate objects, as shown above, because the picker, itself, only has weak references to its delegate, as is best practice.

And the implementation of the picker delegate methods is much cleaner:

class NamePickerDelegate: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
    let names = ["Mo", "Larry", "Curley"]

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

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

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

class NumberPickerDelegate: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
    let numbers: [String] = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .spellOut
        return (0 ..< 100).compactMap { formatter.string(for: $0) }  // use `flatMap` in Xcode versions prior to 9.3
    }()

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

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

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

Now, this is still, obviously, a simplified example, but the beauty is that as the code gets more complicated, the details are encapsulated within separate objects, rather than encumbering a single view controller with all the code.


If you want, you can have the view controller provide the list of strings to the delegate/data source object. In fact, that simplifies it because you need only one class for the picker delegate, and you just instantiate a different one for each picker:

class ViewController: UIViewController {

    let names = ["Mo", "Larry", "Curley"]

    let numbers: [String] = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .spellOut
        return (0 ..< 100).compactMap { formatter.string(for: $0) }  // use `flatMap` in Xcode versions prior to 9.3
    }()

    @IBOutlet weak var numberPickerOne: UIPickerView!
    @IBOutlet weak var numberPickerTwo: UIPickerView!
    @IBOutlet weak var namePicker: UIPickerView!

    lazy var numberPickerOneDelegate: PickerDelegate = PickerDelegate(strings: self.numbers)
    lazy var numberPickerTwoDelegate: PickerDelegate = PickerDelegate(strings: self.numbers)
    lazy var namePickerDelegate:PickerDelegate = PickerDelegate(strings: self.names)

    override func viewDidLoad() {
        super.viewDidLoad()

        numberPickerOne.delegate   = numberPickerOneDelegate
        numberPickerOne.dataSource = numberPickerOneDelegate

        numberPickerTwo.delegate   = numberPickerTwoDelegate
        numberPickerTwo.dataSource = numberPickerTwoDelegate

        namePicker.delegate        = namePickerDelegate
        namePicker.dataSource      = namePickerDelegate
    }

}

class PickerDelegate: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
    let strings: [String]

    init(strings: [String]) {
        self.strings = strings
        super.init()
    }

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

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

    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return strings[row]
    }
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks. This works wonderfully. A few questions. For my `layoutLengthPickerView` and my `layoutWidthPickerView` classes, do I need to declare `numbers` in both classes? Or do I declare `numbers` once outside of both classes? Also, why is the `IBAction func didTapButton` method called if a picker view doesn't have buttons or any tappable objects? Finally, why do we call `NSObject` in all picker view delegate classes? Thanks. – TonyStark4ever May 25 '17 at 20:08
  • 1. You can change those classes so that you pass the values to be presented as a parameter that you capture in the `init` method. 2. The `didTapButton` was just an example of how the view controller could capture what values were selected in the picker views. You don't need that if you don't want. Do whatever you want. 3. The `UIPickerViewDelegate` and `UIPickerViewDataSource` protocols are `NSObjectProtocol`, so those objects have to be `NSObject` subclasses. It's just how UIKit protocols were implemented (for Objective-C compatibility). – Rob May 25 '17 at 20:14
  • That makes more sense. Can you explain 1 (how to put the array in an `init` method)? I am a beginner. Thanks. – TonyStark4ever May 25 '17 at 23:26
  • @W.Cook - See expanded answer re how you can pass data to the picker delegate object. – Rob May 26 '17 at 06:55
  • Thanks. That runs beautifully. – TonyStark4ever May 26 '17 at 15:20
  • To make things a little more complicated, I want to make the first two picker views (the ones with `numbers` as their delegates) to have 2 columns, one for feet, one for inches, but I still want the last picker view to only have one column, `manufacturers`. I have declared an `inches` array like I declared the `numbers` and `manufacturers` arrays. How do I make it so that the first two picker views have two columns, the first with `numbers` as its delegate, the second with `inches` as its delegate, and the last picker view has one column with `manufacturers` as its delegate? Thanks. – TonyStark4ever May 27 '17 at 05:25
  • Also, the `inches` array is just the numbers 1 to 12, so it essentially is just the first 12 members of the `numbers` array. Is it less complicated to just use the first 12 members of the `numbers` array than to use a new `inches` array? Or no? Thanks. – TonyStark4ever May 27 '17 at 05:31
  • Re two components in number picker, then go ahead and have a number picker delegate that returned `2` from `numberOfComponents`. Re one array vs two arrays, I'd probably have two, but you certainly could have one and override the `numberOfRowsInComponent` for the inches component. – Rob May 27 '17 at 14:13
  • So go back to your first answer and declare a class for each picker? Could I use one class/delegate for the first two pickers and one for the last picker since the first two are the same? And what is the code for the `numberOfRowsInComponent` and `titleForRow` for the two column pickers? I have read a lot of questions on this, and none of them make sense to me. Thanks. – TonyStark4ever May 27 '17 at 16:37
  • The code in `numberOfRowsInComponent` would be `if component == 0 { return numbers.count } else { return 12 }` or you can use ternary operator, `return component == 0 ? numbers.count : 12`. Basically, you're checking to see if you're in component 0 (the feet) or component 1 (the inches) and then returning the appropriate number. – Rob May 27 '17 at 17:02
3

If you IBOutlet the connections, you don't need the Tags. All IBOutlet should be weak, We generally use weak for IBOutlets (UIViewController's Childs).This works because the child object only needs to exist as long as the parent object does.

If you're using storyboard or Nib for UIPickerView, you don't need to do the allocation for UIPickerView.

Try this:

@IBOutlet weak var trackPickerView: UIPickerView!
@IBOutlet weak var layoutWidthPickerView: UIPickerView!
@IBOutlet weak var layoutLengthPickerView: UIPickerView!

override func viewDidLoad() {
    super.viewDidLoad()
    trackPickerView.delegate = self
    layoutWidthPickerView.delegate = self
    layoutLengthPickerView.delegate = self
}

let numbers = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "100"]

let manufacturers = ["Atlas True Track", "Atlas Code 100", "Atlas Code 83", "Bachmann Nickel Silver", "Bachmann Steel Alloy", "Kato", "Life-Like Trains Code 100", "LIfe-Like Trains Power-Loc", "Peco Code 100", "Peco Code 83", "Peco Code 75", "Shinohara Code 100", "Shinohara Code 70", "Walthers"]

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

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

    if pickerView == layoutWidthPickerView || pickerView == layoutLengthPickerView {
        return numbers[row]
    }

    return manufacturers[row]
}

func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {

    if pickerView == layoutWidthPickerView || pickerView == layoutLengthPickerView {
        return numbers.count
    }

    return manufacturers.count

}

Output:

Screenshot

Sam
  • 1,765
  • 11
  • 82
  • 176
Imad Ali
  • 3,261
  • 1
  • 25
  • 33
1

As an alternate way of doing that you can use only one picker view and to do the same thing.

@IBOutlet weak var inputBank: UITextField!    
@IBOutlet weak var inputBranch: UITextField! // Those 2 are the fields I need to set the picker view


@IBOutlet weak var inputAccountNumber: UITextField! // this is an extra textField

private var availableBankDetails: AvailableBankDetails?
private var pickerBanks: [String]?
private var pickerBranches: [String]?
private var pickerView: UIPickerView

private var profileDomain: ProfileDomain

required init?(coder aDecoder: NSCoder) {
    self.profileDomain = ProfileDomain()
    self.pickerView = UIPickerView()
    super.init(coder: aDecoder)
}

override func viewDidLoad() {
    super.viewDidLoad()
    self.pickerView.delegate = self
    createBankPickerView()
    createBranchPickerView()
}

private func loadAvailableBankDetails(completion: @escaping (Bool) -> ()) {
    profileDomain.getAvailableBanks { (boolResponse, jsonResponseBody) in
        if boolResponse {
            self.availableBankDetails = AvailableBankDetails(availableBankDetails: jsonResponseBody)
            self.pickerBanks = self.availableBankDetails!.getAvailableBankDetails()
            completion(true)
        } else {
            self.view.makeToast(jsonResponseBody["message"].stringValue, duration: 3, position: .bottom, title: "Error")
            completion(false)
        }

    }
}

private func loadAvailableBranchDetails(bankName: String) -> [String] {
    pickerBranches = availableBankDetails?.getAvailableBranches(bank: bankName)
    return (availableBankDetails?.getAvailableBranches(bank: bankName))!
}

private func createBankPickerView() {
    loadAvailableBankDetails { (boolResponse) in
        if boolResponse {
            self.pickerView.selectRow(1, inComponent: 0, animated: true)
            self.inputBank.inputView = self.pickerView
            self.pickerView.backgroundColor = UIColor.lightGray
            self.createToolbar(inputBankOrBranch: self.inputBank)
        } else {
            print("error")
        }
    }

}

private func createBranchPickerView() {
    self.pickerView.selectRow(1, inComponent: 0, animated: true)
    self.inputBranch.inputView = self.pickerView
    self.pickerView.backgroundColor = UIColor.lightGray
    self.createToolbar(inputBankOrBranch: self.inputBranch)
}

func createToolbar(inputBankOrBranch: UITextField) {
  let toolbar = UIToolbar()
  toolbar.sizeToFit()
  toolbar.tintColor = UIColor.darkGray
  toolbar.backgroundColor = UIColor.blue
  let doneButton = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(self.closePickerView))
  toolbar.setItems([doneButton], animated: false)
  toolbar.isUserInteractionEnabled = true
  inputBankOrBranch.inputAccessoryView = toolbar
}

@objc func closePickerView() {
    view.endEditing(true)
}

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

func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
    if (inputBank.isFirstResponder) {
        return pickerBanks!.count
    } else{
        if pickerBranches == nil {
            return 0
        }
        return pickerBranches!.count
    }
}

func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
    if (inputBank.isFirstResponder){
        return pickerBanks![row]
    } else {
        return pickerBranches![row]
    }
}

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
    if inputBank.isFirstResponder {
        inputBank.text =  pickerBanks![row]
        inputBranch.text = ""
        self.pickerBranches = loadAvailableBranchDetails(bankName: inputBank.text!)
    } else {
        if pickerBranches == nil {
            inputBranch.text = ""
        } else {
            inputBranch.text = pickerBranches![row]
        }
    }        
}

func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
    return 100.0
}

func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
    return 60.0
}

func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {


    var label:UILabel

    if let v = view as? UILabel{
        label = v
    }
    else{
        label = UILabel()
    }

    label.textColor = UIColor.black
    label.textAlignment = .left
    label.font = UIFont(name: "Helvetica", size: 16)



    if inputBank.isFirstResponder {
        label.text = pickerBanks![row]
    } else {
        label.text = pickerBranches![row]
    }

    return label
}
Wimukthi Rajapaksha
  • 961
  • 1
  • 11
  • 23