Dynamic PickerView with multiple components: Index out of range - swift

I have a PickerView with 3 components based on this code. The two picker to the right change their content based on the previous selection to the left.
Unfortunately the app crashes when I change a parent picker to another one with less rows than the previews one: 🛑 Fatal Error: Index out of range.
I tried to return the child picker to their first position after selecting another parent picker, but it didn't worked out. (Please see under didSelectRow
Do you have another idea?
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 3
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0 {
return countries.count
} else if component == 1 {
let selectedCountry = pickerView.selectedRow(inComponent: 0)
return countries[selectedCountry].cities.count
} else {
let selectedCountry = pickerView.selectedRow(inComponent: 0)
let selectedCity = pickerView.selectedRow(inComponent: 1)
return countries[selectedCountry].cities[selectedCity].posts.count
}
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0 {
return countries[row].name
} else if component == 1{
let selectedCountry = pickerView.selectedRow(inComponent: 0)
return countries[selectedCountry].cities[row].name
} else {
let selectedCountry = pickerView.selectedRow(inComponent: 0)
let selectedCity = pickerView.selectedRow(inComponent: 1)
return countries[selectedCountry].cities[selectedCity].posts[row]
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
pickerView.reloadAllComponents()
if component == 0 {
pickerView.selectRow(0, inComponent: 1, animated: true)
pickerView.selectRow(0, inComponent: 2, animated: true)
} else if component == 1 {
pickerView.selectRow(0, inComponent: 2, animated: true)
}
}

When you call reloadAllComponents() in your didSelectRow you haven't updated the selected rows yet so it's crashing.
Change the order around and it should be fine.
Reset the value of the 1st component of UIPickerView when component 0 is changed might be of interest here as well.
Note pickerView.reloadAllComponents() isn't strictly necessary, as pointed out in the post above. You could just reload the ones that need to change.
A suggestion as well: Your code would be easier to read/maintain if you used say an enum to organize your components so instead of if component == 0 you could have if componentType == .country (or better yet, a switch), for example.

Related

Creating 3 columns in UIPickerView, also using append function

This is another question of an app I am trying to develop for a small company. Essentially, I want to make it easier for people to find account numbers. The first column in the PickerView is essentially the department. The second will be the task that he/she is going to do, and the third will be the location. I've figured out how to visually make 3 columns, but i cannot figure out how to put it into the code. By that, I mean how to actually set values/selections for the third column. Furthermore, I'm having some issues trying to connect it with the .append function so that only certain options are presented with certain selections in the column. The append function isn't a necessity, but it would be nice to use it if possible.
This is the first app I have ever coded and I "trained myself", hence my amateur mistakes if any. Any help is appreciated.
import UIKit
cllass Account {
var account: String
var jlCode: [String]
var location: [[String]]
init(account:String, jlCode:[String], location:[[String]]) {
self.jlCode = jlCode
self.account = account
self.location = location
}
}
class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
#IBOutlet weak var pickerView: UIPickerView!
#IBOutlet weak var accountLbl: UILabel!
#IBOutlet weak var infoLbl: UILabel!
#IBOutlet weak var imageView: UIImageView!
var accounts = [Account]()
override func viewDidLoad() {
pickerView.delegate = self
pickerView.dataSource = self
infoLbl.text = """
test
"""
//A. PARKS - Tasks for account number 12345678910
accounts.append(Account(account: "12345678910", jlCode: ["task 1"], location: [["here"]]))
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 3
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0 {
return accounts.count
} else if component == 1 {
let selectedAccount = pickerView.selectedRow(inComponent: 0)
return accounts[selectedAccount].jlCode.count
} else if component == 2 {
let selectedAccount = pickerView.selectedRow(inComponent: 0)
let selectedjlCode = pickerView.selectedRow(inComponent: 1)
let location = pickerView.selectedRow(inComponent: 2)
return accounts[selectedAccount].jlCode[selectedjlCode].location.count
}
return 3
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0 {
return accounts[row].account
} else if component == 1 {
let selectedAccount = pickerView.selectedRow(inComponent: 0)
return accounts[selectedAccount].jlCode[row]
} else if component == 2 {
let selectedAccount = pickerView.selectedRow(inComponent: 0)
let selectedjlCode = pickerView.selectedRow(inComponent: 1)
let selectedLocation = pickerView.selectedRow(inComponent: 2)
return accounts[selectedAccount].jlCode[selectedjlCode].location[selectedLocation]
}
return nil
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
pickerView.reloadComponent(1)
let selectedAccount = pickerView.selectedRow(inComponent: 0)
let selectedjlCode = pickerView.selectedRow(inComponent: 1)
let account = accounts[selectedAccount].account
let jlCode = accounts[selectedAccount].jlCode[selectedjlCode]
accountLbl.text = "Account Number: \(account)\nJL Code: \(jlCode)"
}
}
It seems your titleForRow function is somewhat lacking. You are currently only serving component 0 (accounts) and... anything that is not accounts. You need to be more explicit and deliver the texts accordingly.
To help you on your way, and make it easier for you to comprehend what is happening might I suggest that you create these constants in the top of your class?
private let accountsComponent = 0
private let tasksComponent = 1
private let locationsComponent = 2
Then you should alter the titleForRow function to cater for all the above tasks:
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == accountsComponent {
return accounts[row].account
} else if component == tasksComponent {
let selectedAccount = pickerView.selectedRow(inComponent: accountsComponent)
return accounts[selectedAccount].jlCode[row]
} else if component == locationsComponent {
// THIS IS WHAT IS MISSING
// You need to return the location strings from this place.
}
return nil
}
You will need to return the appropriate strings from the if component == locationsComponent part...
[edit2]
OK, since you're just starting out I'll go the extra mile:
It's not obvious that you've grasped exactly what the numberOfRowsInComponent and titleForRow functions are for(?)
The numberOfRows tells the component how many values each component ("column") contains. The titleForRow is where you return the actual text-strings that should be displayed for each row in each component.
So, as I mentioned in my comments, create a Location struct:
struct Location {
var identifier: Int
var label: String
}
In your controller, create an array of this struct.
let locations: [Location] = [Location(identifier: 1, label: "Outdoors"),
Location(identifier: 2, label: "Indoor"),
Location(identifier: 3, label: "Narnia")]
Then your numberOfRows should lean on this array to return the appropriate number of rows for the location-component:
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
// Only including the parts to return the location-stuff
} else if component == 2 {
return locations.count
}
return 0 // This will never be reached as long as the numberOfComponents returns 3
}
and the titleForRow:
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
// Only including the parts to return the location-stuff
} else if component == 2 {
return locations[row].label
}
return nil
}

Changing data of UIPickerView Swift

I was searching very much, but i did not find an answer for my question.
My app should change the data of one picker after the other picker was changed.
As you can see, in the titleForRow method, I want to set the data and I also know that this method is just called once, but I just do not know where and also how I should do it then.
I think, on the didSelectRow method I could set the data, too, but there I do not have a gleam how I should do it.
Here is my code, I hope this is helpful.
class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
var x = 0
#IBOutlet weak var leaguePicker: UIPickerView!
#IBOutlet weak var clubPicker: UIPickerView!
let league = ["Bundesliga", "Premier League"]
let club = [["ManCity", "Arsenal"],["Bayern", "Dortmund"]]
override func viewDidLoad() {
super.viewDidLoad()
leaguePicker.delegate = self
leaguePicker.dataSource = self
clubPicker.delegate = self
clubPicker.dataSource = self
}
func numberOfComponents(in picker: UIPickerView) -> Int {
return 1
}
func pickerView(_ picker: UIPickerView, numberOfRowsInComponent component: Int) -> Int
{
switch (picker){
case leaguePicker:
return league.count
case clubPicker:
return club.count
default:
return 1
}
}
func pickerView(_ picker: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
switch (picker){
case leaguePicker:
return league[row]
case clubPicker:
if (x==0){
print("titleForRow call.. x=0")
return club[0][row]
// this is just happening at the beginning
} else {
print("titleForRow call.. x=1")
return club[1][row]
// this does not happen
}
default: return "an error occurred"
}
}
func pickerView(_ picker: UIPickerView, didSelectRow row: Int, inComponent component: Int)
{
if (picker == leaguePicker) {
if (row==0){
//clubPicker.selectRow(0, inComponent: 0, animated: true)
x=0
} else {
x=1 // this call is working
//clubPicker.selectRow(0, inComponent: 1, animated: true)
}
}
picker.reloadAllComponents()
//this does not lead to call titleForRow
}
}
You need to reload the 2nd picker when the value in the 1st picker changes.
func pickerView(_ picker: UIPickerView, didSelectRow row: Int, inComponent component: Int)
{
if (picker == leaguePicker) {
x = row
clubPicker.reloadAllComponents()
}
}
You also need to fix the numberOfRowsInComponent:
func pickerView(_ picker: UIPickerView, numberOfRowsInComponent component: Int) -> Int
{
switch (picker){
case leaguePicker:
return league.count
case clubPicker:
return club[x].count
default:
return 1
}
}
And your titleForRow can be simplified:
func pickerView(_ picker: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
switch (picker){
case leaguePicker:
return league[row]
case clubPicker:
return club[x][row]
default: return "an error occurred"
}
}
I also suggest you renamed your x variable to something more useful such as selectedLeague.

Making a row (in a pickerview) not selectable after selected in a UItextField with same pickerview

I have been looking for answers but I couldn't find any clear solution. I am making a view controller with three UItextFields with same UIpickerViews. How can I make a specific row not selectable change color or disappear) if it is already selected in another UItextField?
Here's my code:
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return Array.count
}
func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
if "Shake" {
let attributesUnavailable = [NSForegroundColorAttributeName: UIColor.gray]
let attributedTitle = NSAttributedString(string: "Shake", attributes: attributesUnavailable)
return attributedTitle
}
let attributesAvailable = [NSForegroundColorAttributeName: UIColor.black]
let attributedTitle = NSAttributedString(string: Array[row], attributes: attributesAvailable)
return attributedTitle
}
public func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func donePicker() {
actionTextField1.resignFirstResponder()
actionTextField2.resignFirstResponder()
actionTextField3.resignFirstResponder()
}
#IBAction func save3(_ sender: AnyObject) {
if (behaviorTextField1.text?.isEmpty)! {
let behavior1Empty = UIAlertController (title: "Error", message: "Please fill-In Behavior Names and set corresponding Actions", preferredStyle: UIAlertControllerStyle.alert)
self.present(behavior1Empty, animated:true, completion:nil)
behavior1Empty.addAction(UIAlertAction(title: "Okay", style: UIAlertActionStyle.default, handler: { (action: UIAlertAction!) in
}))
}
}
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if Array[row] {
pickerView.selectRow(row + 1, inComponent: 0, animated: true)
}
}
if pickerView.tag == 1 {
actionTextField1.text = Array[row]
}
else if pickerView.tag == 2 {
actionTextField2.text = Array[row]
}
else {
actionTextField3.text = Array[row]
}
Help will be highly appreciated!
It not so simple like UITableView, but there is a nice approach which seems to be the same used when setting IOS Date at Settings.app.
First set the invalid row title attribute color to be gray, so the user will know this row is unavailable:
pickerView(_:attributedTitleForRow:forComponent:)
Example:
func pickerView(pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
if row_cant_be_selected {
let attributesUnavailable = [NSForegroundColorAttributeName: UIColor.grayColor()]
let attributedTitle = NSAttributedString(string: YOUR_TITLE_FOR_ROW, attributes: attributesUnavailable)
return attributedTitle
}
let attributesAvailable = [NSForegroundColorAttributeName: UIColor.blackColor()]
let attributedTitle = NSAttributedString(string: YOUR_TITLE_FOR_ROW, attributes: attributesUnavailable)
return attributedTitle
}
Then use delegate's method pickerView(_:didSelectRow:inComponent:) to know when users selected an unavailable row. In this method just call selectRow(_:inComponent:animated:) to "force" another row selection.
Example:
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if ROW_CANT_BE_SELECTED {
pickerView.selectRow(row + 1, inComponent: 0, animated: true)
}
}
IE: If row 1 is unavailable, on pickerView(_:didSelectRow:inComponent: just call pickerView.selectRow(row + 1, inComponent: 0, animated: true). This will make UIPickerView to skip to next element. Just watch out for last element.
Please pay attention to row + 1, if the current row its the last one, you should do row - 1 instead.
Other approach is to remove those unavailable elements on your data source. Just reload the other picker views data.

Two pickers in the same UIViewcontroller

I am making an app where a user has the option to use two UIPickers in the same view controller. How can this be done. I am wanting one picker to display beach names and another to display animals living at the beach.
Thanks for your help
Here's a quick guide to doing this:
1.Initialize pickers, and picker data sets in the class:
var pickerView1 = UIPickerView()
var pickerView2 = UIPickerView()
var pickerView1Data: [String] = ["Waikiki", "Long Beach", ...]
var pickerView2Data: [String] = ["Crab", "Seal", ...]
2.Set delegates, data sources, and tags (in viewDidLoad).
pickerView1.dataSource = self
pickerView1.delegate = self
pickerView1.tag = 1
pickerView2.dataSource = self
pickerView2.delegate = self
pickerView2.tag = 2
3.Set number of rows
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
var returnIndex: Int = 0
if pickerView.tag == 1 {
returnIndex = pickerView1Data.count
} else if pickerView.tag == 2 {
returnIndex = pickerView2Data.count
}
return returnIndex
}
4.Return data for each row
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
var returnRow: String!
if pickerView.tag == 1 {
returnRow = pickerView1Data[row]
} else if pickerView.tag == 2 {
returnRow = pickerView2Data[row]
}
return returnRow
}
5.Capture pickerView selection
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
// This method is triggered whenever the user makes a change to the picker selection.
if pickerView.tag == 1 {
beachTextField.text = pickerView1Data[row]
} else if pickerView.tag == 2 {
animalTextField.text = pickerView2Data[row]
}
}
Of course, this is on top of everything else you have to do to set up picker views, but this should be everything that needs to be taken care of for two picker views.

swift uipickerview second component selectrow not working

I have a pickerview in swift with two components. It works except for when I want to set the row in the second component with selectrow. This is when the user reloads the app and we already have a value set. The first component selectrow works.
Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
print("In viewDidLoad")
detailArmy.loadarmy(detailArmy, armymember: detailItem)
myGeneralPicker.dataSource = self
myGeneralPicker.delegate = self
// Set General Picker View to current value
let general = generalTypeInfo()
picker1Options = general.generalType()
print("Detail Item general: \(detailArmy.general)")
if detailArmy.general == "" {
myGeneralPicker.selectRow(0, inComponent: 0, animated: false)
picker2Options = general.generalInfo("None")
} else {
myGeneralPicker.selectRow(picker1Options.indexOf(detailArmy.general)!, inComponent: 0, animated: false)
picker2Options = general.generalInfo(detailArmy.general)
}
print("Detail Item general Type: \(detailArmy.generaltype) picker2Options: \(picker2Options)")
if detailArmy.generaltype == "" {
myGeneralPicker.selectRow(0, inComponent: 1, animated: false)
} else {
//myGeneralPicker.selectRow(picker2Options.indexOf(detailArmy.generaltype)!, inComponent: 1, animated: false)
myGeneralPicker.selectRow(4, inComponent: 1, animated: false)
}
}
//MARK: Delegates and data sources
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
//println("In numberOfComponenetsInPickerView pickerView: \(pickerView)")
return 2
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0 {
return picker1Options.count
} else {
return picker2Options.count
}
}
//Mark: Delegates
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
//var dicerow = dice[row]
print("titleForRow row: \(row) component: \(component)")
if component == 0 {
return String(picker1Options[row])
} else {
return String(picker2Options[row])
}
}
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if component == 0 {
generalpickinfo = picker1Options[row]
print("pickerView general selected: \(generalpickinfo)")
let general = generalTypeInfo()
picker2Options = general.generalInfo(picker1Options[row])
pickerView.reloadAllComponents()
myGeneralPicker.selectRow(0, inComponent: 1, animated: false)
} else {
generalpicktype = picker2Options[row]
print("pickerView general type selected: \(generalpicktype)")
}
}
func pickerView(pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusingView view: UIView?) -> UIView {
print("viewForRow row: \(row) component: \(component)")
let pickGeneralLabel = UILabel()
pickGeneralLabel.textAlignment = .Center
if component == 0 {
pickGeneralLabel.text = String(picker1Options[row])
} else {
pickGeneralLabel.text = String(picker2Options[row])
}
return pickGeneralLabel
}
class generalTypeInfo {
func generalType() -> [String] {
return ["None","C-in-C","Subordinate","Ally"]
}
func generalInfo(type: String) -> [String] {
if type == "None" {
return ["None"]
} else {
return ["None","Brilliant","Inert"]
}
}
func generalTypeIndex(type: String) -> Int {
let generalTypeDict = ["None": 0, "C-in-C": 1, "Subordinate": 2, "Ally": 3]
return generalTypeDict[type]!
}
func generalInfoIndex(type: String) -> Int {
let generalInfoDict = ["None": 0, "Brilliant": 1, "Inert": 2]
return generalInfoDict[type]!
}
}
In my code currently I have deliberately put a value of 4 in this line:
myGeneralPicker.selectRow(4, inComponent: 1, animated: false)
the line above has the correct code I want to run, but I put this in because it should crash the application since there isn't a 4th value. And it completely ignores this.
Anyone tell me what I'm doing incorrectly with this? Thanks.
So, ask a question, and then find out the answer..
The answer is to move the selectrow into the ViewDidAppear.
I found the solution here:
UIPickerView selectRow doesn't works
Thanks