Using UIPickerView with multiple components in swift - ios

Hi there I have been trying to get this working for ages with no success so apologies if this seems easy for you guys.
I want to use a 3 wheel UIPickerView - The 1st wheel didSelectRow value will be used to change the array for the remaining two wheels but they will be the same as it is a conversion app.
It seems to throw me an error up when populating the wheels saying that Anyobject is not identical to String. I cannot see anything wrong but I know it is something basic I have missed.
Any pointers would be much appreciated.
Motty.
// ViewController.swift
// picker test
import UIKit
class ViewController: UIViewController, UIPickerViewDelegate {
#IBOutlet weak var picker1Label: UILabel!
#IBOutlet weak var picker2Label: UILabel!
#IBOutlet weak var bigPicker: UIPickerView!
var wheelContents = []
var length = ["metres","feet","yards","inches","mm","cm","miles"]
var volume = ["m3","US Gall","Imp Gall","Barrels", "cubic FT","litres"]
var conType = ["length","volume"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
wheelContents = [conType,length,length]
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//For main selection of type of conversion
// returns the number of 'columns' to display.
func numberOfComponentsInPickerView(bigPicker: UIPickerView) -> Int{
return wheelContents.count
}
// returns the # of rows in each component..
func pickerView(bigPicker: UIPickerView, numberOfRowsInComponent component: Int) -> Int{
return wheelContents[component].count
}
func pickerView(bigPicker: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String!{
return wheelContents[component][row]
}
}

You need to tell Swift that your wheelContents array is an array of array of String:
var wheelContents:[[String]] = []
If you don't explicitly give wheelContents a type, Swift infers it to be NSArray which is what is giving you problems.

You need to say swift, that your wheelContents is an array of String arrays:
var wheelContents:[[String]] = []
Also you should set the delegate of your pickerView to self because you handle the delegate in your class. Otherwise the functions won't work properly:
//In the viewDidLoad method
bigPicker.delegate = self

Since you declared wheelContents like this wheelContents = [], without specifying its elements type, the compiler automatically infers that it is array of AnyObjects aka [AnyObject].
That's the reason why when you are returning wheelContents[component].count it generates an error: at that moment the compiler is expecting a String! but you are providing an AnyObject.
It's a really easy fix, you should just specify what the content of the array it is going to be when you declare it. (it's an array of arrays of strings aka [[String]])

You need to set the datasource of pickerView along with the delegate in viewDidLoad
pickerView.dataSource = self

Related

Why is add button causing a crash?

What I'm trying to do is have a button that will pull the information from UIPickerviewer that will then add that to an array but when the program runs and you hit the button it crashes. Quite new to programming in swift... Would like a better explanation.
import UIKit
class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
#IBOutlet weak var PickerView: UIPickerView!
#IBOutlet var list: [UILabel]!
#IBAction func add(_ sender: UIButton) {
var shift = true
}
#IBOutlet weak var label: UILabel!
var deviceList = [""]
var shift = false
override func viewDidLoad() {
super.viewDidLoad()
label.text = "Device List"
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
let devices = ["Mobile Phone","Microwave","Heater","Fridge","Router","Drill"]
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return devices[row]
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return devices.count
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
label.text = devices[row]
if(shift == true){
deviceList.append(label.text!)
print(deviceList)
shift = false
}
}
}
If you're getting a crash when you tap on your button I assume there is some conflicting assignments with the IBOutlet events referencing. The reason being is because your add(_:) action method isn't doing anything but creating a new boolean property called shift, and even though it's the same name as the class property shift (self.shift), that actually shouldn't cause a crash. Sometimes in interface builder you can have an outlet that you accidentally assigned more than one event, and that has usually caused a crash in my experience if you deleted one of the events in your code. For example: say you created a event for your button (like add(_:)), and then forgot and accidentally created a second one. You then realized your mistake and deleted the code for the second event, but didn't know you had to remove the event outlet. Interface builder will allow you to make this mistake, but will cause the app to crash when you tap the button, because it's trying to call an event that doesn't exist anymore.
If you want to check if you have any unnecessary event assignments through IB, control+click on the button in your documents outline on the left side of the screen using Interface Builder.
You'll see the referencing outlets listed under the "Referencing Outlets" header. You can remove any unnecessary ones by clicking the little "X" to the top left. You can also tell if the outlet is actually connected, by checking if it has a little filled circle to the right (this means it's connected). Here is an example, in this case I created an event call to a method called tryAgain: which I then removed. It causes a crash when I tap the button.
If I click the "X" button and remove the tryAgain: event. The app will work when the button is clicked.
I'm not certain this is your issue, but based on what you've shared it sounds likely to me. Hope this helps. I would recommend actually executing some code in your add(_:) method, however.
Nothing about the IBAction you posted should be causing a crash. You do need to get rid of the word "var" in the body of the function so that it changes the state of your instance variable rather than setting a local variable that never does anything, but that won't cause a crash.
That said, your "label" outlet is declared as an implicitly unwrapped optional, which is normal for outlets:
#IBOutlet weak var label: UILabel!
If the outlet is not connected to anything, though, then any time you try to read from or write to the label outlet or a property of the label, you'll crash if label is nil.
Check to see if you outlet is hooked up correctly. If it's not, then I would expect you to crash when you operate your picker. (Not when you click the add button.)

How can I invoke a sub-class of UIPickerView inSwift

I have a ViewController with a UIPickerView as a single control myPickerView which is of a class MyPickerView which I created as a sub-class of UIPickerView. I invoke myPickerView in ViewController viewDidLoad by myPickerView.viewDidLoad. However, this does not execute the source functions of MyPickerView.
I need a clarification of how I can make this work. My reason for MyPickerView is that it has a lot of special code that I did not want to clutter up the main ViewController. See the example code below:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var myPickerView: MyPickerView!
override func viewDidLoad() {
super.viewDidLoad()
myPickerView.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
import UIKit
var gSep = ","
class MyPickerView: UIPickerView , UIPickerViewDataSource, UIPickerViewDelegate {
var pickerData = [[" "],[" "]]
var category = [""]
var subCategory = [""]
var dictMenuList = [String:String]()
//MARK:- category/subcategory picker
func viewDidLoad() {
println("MyPickerView: viewDidLoad")
dictMenuList = ["Medical":"Sub-Cat 1.1,Sub-Cat 1.2,Sub-Cat 1.3,Sub-Cat 1.4,Sub-Cat 1.5,Sub-Cat 1.6,Sub-Cat 1.7",
"Taxes": "Sub-Cat 2.1,Sub-Cat 2.2,Sub-Cat 2.3,Sub-Cat 2.4",
"Bills": "Sub-Cat 3.1,Sub-Cat 3.2,Sub-Cat 3.3,Sub-Cat 3.4,Sub-Cat 3.5,Sub-Cat 3.6,Sub-Cat 3.7"]
println("MyPickerView dictMenuList: \(dictMenuList)")
self.reloadAllComponents()
let firstKey = self.loadPickerWithCategory(0)
self.loadPickerWithSubCategory(firstKey)
}
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
println("MyPickerView: numberOfComponentsInPickerView \(pickerData.count)")
return pickerData.count
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return pickerData[component].count
}
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if component == 0 {
let selectedKey = category[row]
loadPickerWithSubCategory(selectedKey)
}
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
return pickerData[component][row]
}
func pickerView(pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusingView view: UIView!) -> UIView
{
var pickerLabel = UILabel()
pickerLabel.textColor = UIColor.blackColor()
pickerLabel.text = pickerData[component][row]
pickerLabel.font = UIFont(name: pickerLabel.font.fontName, size: 17)
pickerLabel.textAlignment = NSTextAlignment.Center
return pickerLabel
}
func loadPickerWithCategory (row: Int) -> String{
println("loadPickerWithCategory")
category = [String](dictMenuList.keys)
println("MyPickerView: category: \(category)")
if category.isEmpty {
return ""
}
let n1 = dictMenuList.count
pickerData[0].removeAll(keepCapacity: true)
for i in 0 ..< n1
{
pickerData[0].append(category[i])
}
return category[row]
}
func loadPickerWithSubCategory (key: String) {
println("MyPickerView: loadPickerWithSubCategory")
let x = dictMenuList[key]
subCategory = x!.componentsSeparatedByString(gSep)
let n1 = subCategory.count
pickerData[1].removeAll(keepCapacity: true)
if subCategory.isEmpty {
return
}
for i in 0 ..< n1
{
pickerData[1].append(subCategory[i])
}
self.reloadAllComponents()
}
}
The method viewDidLoad is a view controller method, not a view method. A UIPickerView is a subclass of UIView, not UIViewController, so the system will not call your viewDidLoad method.
You need to override one or more of the init methods.
If you're loading your picker view from a Storyboard or XIB, you probably want to override initWithCoder.
If you're creating your picker in code, you probably want to override initWithFrame.
I sometimes create a method setup that I call from both initWithCoder: and from initWithFrame:. That way my setup code gets called regardless of how the view object is loaded.
I vaguely remember reading that there is a better way of handling this dueling initializers problem in Swift, but I don't remember what it is. (I'm still learning Swift.)
EDIT:
It just occurs to me that you can use the method awakeFromNib to do setup after your view has been loaded and all of it's outlets are set up. That's roughly equivalent to the viewDidLoad call for view controllers. I should have thought of that sooner.
(awakeFromNib is a method of NSObject, so it's a bit hard to find if you don't know it exists.)
First of all viewDidLoad() is a method of the UIViewController class and is called after the controller's view is loaded into the memory. Read more here. You can not use it in views.
So you should implement an init method inside your custom picker class. I'd recommend to override initWithFrame and initWithCoder and set up your component there.
And you will initialize your custom picker like this:
myPickerView = MyPickerView(frame: yourFrame)

How to build a variable from another variable

I hope the title makes some sense but what I am trying to do is to set a field value to the item selected variable from my dataPicker. I have been able to make this work when there is only one field to set but my project will have multiple fields on each view that will call data from the dataPicker based on what field it is. I hope that is clear. Maybe as you look at the code it will.
I have set up a test project to limit things to this issue only. So my variable to tell the view what array to populate in the dataPicker is either season or sport. the field that will receive the data from the season/sport array is enterSeason and enterSport. When the picker has returned a value from season, I want to combine it with enter to create the var enterSeason to set that == itemSelected. This language is very new to me so I am trying the only way I have used before to combine text and variables in one value. It is obviously not working. Help is appreciated.
import UIKit
class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource, UIGestureRecognizerDelegate {
#IBOutlet var enterSeason: UITextField!
#IBOutlet var enterSport: UITextField!
var dataPickerView = UIPickerView()
var season = ["2013", "2014", "2015"] //multi-season
//var season = ["2015"] //single-season
var sport = ["Baeball", "Football", "Basketball", "Hokey"]
var activeField = []
override func viewDidLoad() {
super.viewDidLoad()
enterSeason.inputView = dataPickerView
dataPickerView.delegate = self
dataPickerView.dataSource = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return activeField.count
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
return activeField[row] as! String
}
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
var itemSelected = activeField[row] as! String
self.enter"\activeField".text = itemSelected
}
}
EDIT : How do you show and hide the picker? Your code anly shows variable declarations and the delegate methods... answers could vary accordingly..
Since you show the picker as text field's input view, set UITextFieldDelegate for each of these text fields .. and in the textFieldDidBeginEditing check which field becomes active with simple if else
func textFieldDidBeginEditing(textField: UITextField) {
if textField === enterSeason {
activeField = season
}
else if textField === enterSport {
activeField = sport
}
}
And in the picker selector, set value of the relevant text field as per current activeField object
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if activeField === season {
enterSeason.text = season[row] as! String
}
else if activeField === sport {
enterSeason.text = sport[row] as! String
}
}
Setting the delegate for your text fields in storboard/xib :
P.S.
- Rename activeField to activeDataArray or somethiong more appropriate
EDIT 2 : As you mentioned, second approach i have mentioned below is not suitable for you because there are too many of these fields i am still keeping it as part of the answer as it may help someone else
But what you are trying to achieve is very simple and approach is too convoluted / weird. So heres another way you can implement the whole thing..
The easiest (but still probably not the best) way is to have two instances of the UIPickerView for each field. you can directly check pickerView == seasonPickerView OR pickerView == sportPickerViewin an if else block and do the conditional programming and you wont need the activeField variable..

Using the value on 1st component on UIPickerView to change the remaining components

just got some great help which made the UIPickerView wheels work perfectly but they I have been trying to get the data in the 2nd and 3rd components to change dependant on the position of the 1st component.
I can get through some println lines to work out that the variable that I pull out whatConversion has the correct value but I have no idea how to change the array and make the UIPickerView update with the new values.
Please help for my sanity and also I am going to have to put extra time in at work after spending nearly all day on these, what dozen lines of code.
Thanks in advance
Motty
import UIKit
class ViewController: UIViewController, UIPickerViewDelegate {
#IBOutlet weak var picker1Label: UILabel!
#IBOutlet weak var picker2Label: UILabel!
#IBOutlet weak var bigPicker: UIPickerView!
var wheelContents:[[String]] = []
var length = ["metres","feet","yards","inches","mm","cm","miles"]
var volume = ["m3","US Gall","Imp Gall","Barrels", "cubic FT","litres"]
var conType = ["length","volume"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
bigPicker.delegate = self
wheelContents = [conType, length, length]
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//For main selection of type of conversion
// returns the number of 'columns' to display.
func numberOfComponentsInPickerView(bigPicker: UIPickerView) -> Int{
return wheelContents.count
}
// returns the # of rows in each component..
func pickerView(bigPicker: UIPickerView, numberOfRowsInComponent component: Int) -> Int{
return wheelContents[component].count
}
func pickerView(bigPicker: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String!{
return wheelContents[component][row]
}
func pickerView(bigPicker: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
var whatConversion = wheelContents[0][bigPicker.selectedRowInComponent(0)]
switch(whatConversion){
case "length":
wheelContents = [conType, length, length]
bigPicker.numberOfRowsInComponent(wheelContents[component].count)
break
case "volume":
wheelContents = [conType, volume, volume]
break
default:
break
}
}
}
I am even happier I managed to figure this out myself by looking at the Class definitions and functions
used bigPicker.reloadAllComponents()

UIPickerView with "external" DataSource and Delegate in Swift

I have two different UIPickerViews in my View. They work great when I set the dataSource and the delegate to the View they are hosted in via the storyboard, but when I try to do that via code as described below, it does not work.
Both pickers shall have different data to display (and maybe even different behaviours for the delegate). Thus I would like to connect them to different data sources programmatically.
I tried to create my own class implementing the UIPickerViewDataSource- and UIPickerViewDelegate-Protocols and connecting objects of that class to my PickerViews, but it does not work. An exception is thrown at runtime terminating with uncaught exception of type NSException stating this:
2015-01-09 17:50:05.333 Pet Stats[4953:244338] -[NSConcreteMapTable numberOfComponentsInPickerView:]: unrecognized selector sent to instance 0x7b4616d0
2015-01-09 17:50:05.338 Pet Stats[4953:244338] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSConcreteMapTable numberOfComponentsInPickerView:]: unrecognized selector sent to instance 0x7b4616d0'
How can I get this to work? What did I miss? Here is my code:
WeightWheelController.swift
import UIKit
class WeightWheelController: NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
let ElementCount: Int!
init(pickerInterval: Int) {
ElementCount = pickerInterval
}
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return ElementCount
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
return String(row + 1)
}
func pickerView(pickerView: UIPickerView!, didSelectRow row: Int, inComponent component: Int)
{
println("External Controller:" + String(row + 1))
}
}
WeightWheelInputViewController.swift
import UIKit
class WeightWheelInputViewController: UIViewController {
#IBOutlet weak var picker1: UIPickerView!
#IBOutlet weak var picker2: UIPickerView!
override func viewDidLoad() {
super.viewDidLoad()
//picker attached to c1 should show number from 1 to 150
let c1 = WeightWheelController(pickerInterval: 150)
//picker attached to c1 should show number from 1 to 10
let c2 = WeightWheelController(pickerInterval: 10)
picker1.dataSource = c1
picker1.delegate = c1
picker2.dataSource = c2
picker2.delegate = c2
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
BRIEF UPDATE:
In this question I have found that you can use different tags for different picker views. That would be one option; yet, I don't like it. I would like to rather follow a MVC'ish approach and connect different controllers to each picker. Isn't that possible in any way?
Both delegate and datasource are unowned references. This means that c1 and c2 will get released as soon as you go out of scope. Try declaring c1 and c2 as properties of the class.
Unowned references do not create a strong hold on the referred object (a.k.a. they don't increase the retain count in order to prevent ARC from deallocating the referred object).
Also make sure you remove the delegate and datasource properties of the pickerviews from the interface builder.
class WeightWheelInputViewController: UIViewController {
#IBOutlet weak var picker1: UIPickerView!
#IBOutlet weak var picker2: UIPickerView!
var c1 : WeightWheelController!
var c2 : WeightWheelController!
override func viewDidLoad() {
super.viewDidLoad()
c1 = WeightWheelController(pickerInterval: 150)
c2 = WeightWheelController(pickerInterval: 10)
picker1.dataSource = c1
picker1.delegate = c1
picker2.dataSource = c2
picker2.delegate = c2
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}

Resources