UIPickerView selecting row in wrong component (exorcism required?) - ios

I don't know what I've done to anger the gods but my UIPickerView is behaving abnormally. It has 2 components, for month and year values, and selecting a value on the right component (year) causes the left component (month) to change value as well.
I initially create the UIPickerView using the following code:
pickerView = UIPickerView(frame: ...)
pickerView.delegate = self
pickerView.dataSource = self
Then I implement some simple delegate / data source methods to handle the picker view, as follows:
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if component == 0 {
expirationMonth = months[row]
} else {
expirationYear = years[row]
}
expirationTextField?.text = "\(expirationMonth ?? "")/\(expirationYear?.substring(from: 2) ?? "")"
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0 {
return months[row]
} else {
return years[row]
}
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0 {
return months.count
} else {
return years.count
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 2
}
Additionally, the UIPickerView data source method numberOfComponents is being called even when I don't set the picker view's data source.
I have no idea what's causing this behavior. I've tried pretty much everything and nothing is stopping it from changing both column's values. Does anyone have any ideas to fix this?

Turns out it's a bug in the iOS simulator, because when running it on my iPhone it worked like a charm. I thought I was going crazy there for a second!
I'm going to file a bug complaint in the morning...Good luck to anyone else with this issue.

Related

UIPickerView where selected 1st component decides contents of 2nd component is out of sync

I have encountered some synchronisation/graphic update problems with my UIPickerView.
I want a view with 2 components, where the content of the second component depends on the selected row of the first component.
My code is inspired from: Swift UIPickerView 1st component changes 2nd components data
However, while it seems to work, sometimes (not every time) there are some visual problems, as seen on the screenshots below. (on the second screenshot, you can see that the rows of the second component are not really correct, and are a mix of the rows from the first and the second component)
Here is the code:
import UIKit
class AddActivityViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
#IBOutlet weak var typePicker: UIPickerView!
var pickerData: [(String,[String])] = []
override func viewDidLoad() {
super.viewDidLoad()
self.typePicker.delegate = self
self.typePicker.dataSource = self
pickerData = [("sport",["bike", "run", "soccer", "basketball"]),
("games",["videogame", "boardgame", "adventuregame"])]
// not sure if necessary
typePicker.reloadAllComponents()
typePicker.selectRow(0, inComponent: 0, animated: false)
// pickerData = [("sport",["bike", "run", "soccer"]),
// ("games",["videogame", "boardgame", "adventuregame"])]
}
// number of columns in Picker
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 2
}
// number of rows per column in Picker
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
print("function 1 called")
if component == 0 {
return pickerData.count
} else {
let selectedRowInFirstComponent = pickerView.selectedRow(inComponent: 0)
return pickerData[selectedRowInFirstComponent].1.count
}
}
// what to show for a specific row (row) and column (component)
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
print("function 2 called with values: component: \(component), row: \(row)")
if component == 0 {
// refresh and reset 2nd component everytime another 1st component is chosen
pickerView.reloadComponent(1)
pickerView.selectRow(0, inComponent: 1, animated: true)
// return the first value of the tuple (so the category name) at index row
return pickerData[row].0
} else {
// component is 1, so we look which row is selected in the first component
let selectedRowInFirstComponent = pickerView.selectedRow(inComponent: 0)
// we check if the selected row is the minimum of the given row index and the amount of elements in a given category tuple array
print("---",row, (pickerData[selectedRowInFirstComponent].1.count)-1)
let safeRowIndex = min(row, (pickerData[selectedRowInFirstComponent].1.count)-1)
return pickerData[selectedRowInFirstComponent].1[safeRowIndex]
}
//return pickerData[component].1[row]
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
// This method is triggered whenever the user makes a change to the picker selection.
// The parameter named row and component represents what was selected.
}
}
Is this a problem with my code or generally a complicated aspect of UIPickers that can not be trivially solved?
Additionally, is there a nicer way to develop this functionality?
I solved the error, however I do not understand why this solves it.
The solution is to imlement the func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int)method, which I did not believe to be necessary just to show the fields.
In other words, just add this to my existing code:
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if component == 0 {
pickerView.reloadComponent(1)
} else {
let selectedRowInFirstComponent = pickerView.selectedRow(inComponent: 0)
print(pickerData[selectedRowInFirstComponent].1[row])
}
}

How to update a UIPickerview after changing it's data source contents

I have 2 UIPickerViews in an ios program which I am running on an iPad simulator.
They have one component in each.
I find the relevant picker view by using a switch on the tag. The two single component views need to be changed by adding or deleting components.
This is easy enough in the data source with
pickerData.append(textInput)
pickerData.sort()
pickerData.reloadAllComponents
and
pickerData.remove(at: lastDataSelected)
picker.reloadAllComponents()
where lastDataSelected is the row integer.
This works to change the data source but not entirely when transferred to the UIPickerViews.
The UIPickerView display is not updated until I scroll the view. To be more precise, the item selected is correct but the text label is not updated. After scrolling the data labels are all showing correctly.
I have tried to programatically scroll from one end to the other but this does not help.
So how can I tell the program to update the view without the user scrolling it?
picker.reloadInputViews() does not help.
Apart from this the number of items (rows) isn't changed to reflect the changes in the picker data so the last item falls off the list when adding a new one.
So the second question is how to get the UIPickerView functions to update the number of rows?
I haven't been able to find any examples of dynamically updated picker views so hope someone can help or point me in the right direction.
The remaining code is fairly standard I believe but I'm obviously missing something in the update process.
override func viewDidLoad() {
super.viewDidLoad()
flvPicker = UIPickerView()
flvPicker.delegate = self
flvPicker.dataSource = self
flvPicker.tag = 0
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
switch pickerView.tag {
case 0:
return 1
case 1:
etc...
}
}
var numberOfRowsInComponent = 0
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
switch pickerView.tag {
case 0:
return flvPickerData.count
case 1:
etc...
}
}
func pickerView(_
pickerView: UIPickerView,
titleForRow row: Int, forComponent component: Int) -> String? {
switch pickerView.tag {
case 0:
return flvPickerData[row]
case 1:
etc...
}
}
func pickerView( _ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
switch pickerView.tag {
case 0:
flavourSelected = flvPickerData[row]
lastFlavourSelected = row
case 1: etc...
}
}
I think the question is really how to get the UIPickerView to update correctly after making changes to it's data source and therefore row count.
You can use reloadComponent(_:) method from UIPickerView.
A little late to the party, but if you want to update a picker view, there's also the method selectRow. This also has the benefit of an animation property, so you can animate any updates.
Example:
for (index, day) in weeklyOptions[0].enumerated() {
if scheduledTime.contains(day) {
weeklyDatePicker.selectRow(index, inComponent: 0, animated: true)
}
}

PickerView Default row selected but returns zero unless the picker view is moved

In my picker view I want the default row to be zero. This row has a value of 1. I want to be able to touch nothing on the view contoller except a button. I know there are similar questions but they did not work for me.
override func viewWillAppear(_ animated: Bool) {
self.pickerView.delegate = self
self.pickerView.dataSource = self
self.pickerView.selectRow(0, inComponent: 0, animated: true)
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return String(numbers[row])
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return numbers.count
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
therow = pickerView.selectedRow(inComponent: 0) + 1
}
then
#IBAction func submitTapped(_ sender: Any) {
Print (therow)
}
When I tap submit and print the value at row 0 it is 0, but if I wiggle the picker view and put it back on row 0 then it prints 1. I need to be able to touch nothing on the picker view and have it return the proper value of the default row.
You should use the row that the pickerview delegate method gives you , so you should modify your code as follows:
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
therow = numbers[row]
//theRowIndex = row //this is the index of row that you selected
}
e.g if numbers array is numbers = [1, 2, 3, 4], when you click on first row above code will set therow to be 1 and if you click on second row, it will set therow to be 2 and so on.
if you want to use the code that you wrote then you can use as follows:
therow = numbers[pickerView.selectedRow(inComponent: 0)]
this will give you the number for selected row , but I think you dont need it inside the above method.
Now if you dont want to touch the picker then I think you need to do this:
#IBAction func submitTapped(_ sender: Any) {
therow = numbers[self.pickerView.selectedRow(inComponent: 0)]
print(therow)
}
Use this statement once you load your picker view with data.
yourPicker.selectRow(0, inComponent:0, animated:true)
You can change the default selected value by changing the first parameter of selectRow.
I think the reason why this happens is that didSelectRow is somehow not called if you selected the row programmatically. As per the docs:
Called by the picker view when the user selects a row in a component.
So you need to set your therow property programmatically after you call selectRow:
self.pickerView.selectRow(0, inComponent: 0, animated: true)
therow = 1 // <--- this line

Return 2 variables in swift

I'm a newbie developer, trying to return 2 variables but without any success. Here are what I have already tried:
I tried to put these 2 variables (I'm not sure if those are variables or they have different name) into the array and then call return, but the error was:
"cannot convert return expression of type [Int] to return type int"
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
var typeAndStoreArray = [Int]()
typeAndStoreArray.append(stores.count)
typeAndStoreArray.append(types.count)
return typeAndStoreArray
}
I tried to put stores.count into variable called sc and types.count into variable called tc, but here as well I had an error
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
let sn = stores.count
let tn = types.count
return (sn, tn)
}
Try to understand the functionality of this delegate method.
The method is called multiple times for each component. It passes the index of the component and expects to return the corresponding number of rows.
So if your stores array is component 0 and types is component 1 you have to write
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0 {
return stores.count
} else {
return types.count
}
}
You are misusing picker view. To have two wheels of values, you need to return counts for each wheel separately. A component in picker view is like the wheel. So, to have the first wheel (component) displaying stores and then second wheel displaying types, you need to return counts separately like this
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0 {
return stores.count
} else
return types.count
}
}

How to restrict UIPickerView Component

How to restrict the UIPickerView component scrolling.
I have two components in UIPickeView one second component ends scroll allows user to touch the first component to change the value in it.
How can I restrict the user to touch component one until and unless the second component scrolling complemented.
Using this extension, you can check UIPickerView is scrolling or not. Depend on scrolling, you can enable/disable interaction with UIPickerView.
extension UIView {
func isScrolling () -> Bool {
if let scrollView = self as? UIScrollView {
if (scrollView.isDragging || scrollView.isDecelerating) {
return true
}
}
for subview in self.subviews {
if subview.isScrolling() {
return true
}
}
return false
}
}
UIPickerViewDelegate to detect scrolling or not.
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if pickerView.isScrolling() {
pickerView.isUserInteractionEnabled = false
} else {
pickerView.isUserInteractionEnabled = true
}
//Use this for reduce lines
//pickerView.isUserInteractionEnabled = !pickerView.isScrolling()
return "Row \(row)"
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
pickerView.isUserInteractionEnabled = true
}
The only problem is you can not touch current scrolling component, until component completed scrolling. HOPE, someone solve this also.

Resources