#How to use UIPickerView in Swift

目的: 學習使用 UIPickerView

第一步 在 storyboard 添加 Picker View

第二步 從 Picker View 拉線到 View Controller

方法 1. 直接拉線

dataSource ✅

delegate ✅

⭐ 方法 2. 透過程式

先拉 @IBOutlet weak var PickerView: UIPickerView!

接下來在 override func viewDidLoad() 輸入以下程式

override func viewDidLoad() {
super.viewDidLoad()
PickerView.delegate = self
PickerView.dataSource = self

第三步 遵從 protocol ,並宣告 protocol 的 func()

在 ViewController 新增 UIPickerViewDelegate 、UIPickerViewDataSource

class ViewController: UIViewController , UIPickerViewDelegate, UIPickerViewDataSource { 

點選 Fix,讓 Xcode 幫你寫出 func

第四步 開始測試

在 第一個 func numberOf…() 回傳 1,第二個 func pickerView(…)回傳 5

//決定 pickerview 區域數量
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
//決定 pickerview 區域行數
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return 5
}

執行後我們會看到有 1 個區域,且有 5 個問號

新增以下 func 傳入資料 “test”

//決定pickerview 區域的每筆資料
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
"test"
}

新增自訂資料 ,並且更改回傳程式

let cityArray = ["", "台北市", "新北市", "高雄市"]
let TaipeiDistArray = ["松山區", "大安區", "士林區", "信義區"]
//決定pickerview 區域行數
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return cityArray.count
}
//決定pickerview 區域的每筆資料
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return cityArray[row]
}

新增第二個區域

關鍵參數 component 組件編號,component 是由 0 開始 由左至右編號

//決定pickerview 區域數量
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 2
}
//決定pickerview 區域行數
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0{
return cityArray.count
}else {
return TaipeiDistArray.count
}

}
//決定pickerview 區域的每筆資料
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0{
return cityArray[row]
}else {
return TaipeiDistArray[row]
}

}

如果不熟悉的話,可以透過以下程式來加深印象

    //決定pickerview 區域數量
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 2
}
//決定pickerview 區域行數
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return 4
}
//決定pickerview 區域的每筆資料
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return "component \(component) row \(row)"
}

讓每個縣市與區域同步

將資料改為自訂義 class ,新增 Swift File CityandDist

class CityandDist {
var city: String
var dist: [String]

init(city: String, dist: [String]) {
self.city = city
self.dist = dist
}
}

在 ViewController 新增 Array

let cityanddistarray = [
CityandDist(city: "台北市", dist: ["松山區", "大安區", "士林區", "信義區"]),
CityandDist(city: "新北市", dist: ["林口區", "新莊區", "三重區", "中和區", "永和區"]),
CityandDist(city: "高雄市", dist: ["楠梓區", "左營區", "鼓山區", "三民區", "鹽埕區", "前金區", "新興區", "苓雅區", "前鎮區", "小港區", "旗津區", "鳳山區", "大寮區", "鳥松區", "林園區", "仁武區", "大樹區", "大社區", "岡山區", "路竹區", "橋頭區", "梓官區", "彌陀區", "永安區"])
]
]

右邊的 row 的數量及資料,會由左邊我們所選到的城市來決定,所以 selectedRow(inComponent: 0) 參數會填入 0

    //決定pickerview 區域行數
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0{
return cityanddistarray.count
}else {
let selectcity = pickerView.selectedRow(inComponent: 0)
return cityanddistarray[selectcity].dist.count
}
}
//決定pickerview 區域的每筆資料
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {

if component == 0{
return cityanddistarray[row].city
}else {
let selectcity = pickerView.selectedRow(inComponent: 0)
return cityanddistarray[selectcity].dist[row]
}
}

執行後會發現程式沒有達到我們預期希望的執行結果

這時候就需要以下程式碼來幫助我們

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
pickerView.reloadComponent(1)
}

成功了!

來試試看取所選資料與 Label 做連結

@IBOutlet weak var cityLabel: UILabel!
@IBOutlet weak var distLabel: UILabel!

改寫以下 func()

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
pickerView.reloadComponent(1)
let selectcity = pickerView.selectedRow(inComponent: 0)
let selectdist = pickerView.selectedRow(inComponent: 1)
let city = cityanddistarray[selectcity].city
let dist = cityanddistarray[selectcity].dist[selectdist]
print("city: \(city)")
print("dist: \(dist)")
cityLabel.text = city
distLabel.text = dist



}

看起來似乎沒有問題了對吧?我們再多試幾次看看

是什麼造成閃退的呢?
用左邊的圖中操作來看看出現的錯誤

出現了 index out of range ,原來是我們 選擇新北市的地區時,新北市的資料比台北市多了一筆,當我們新北市選擇最後一個地區,再選回台北市時,台北市的地區 dist[selectdist] 沒有第五筆的資料會造成閃退

解決方法

讓我們思考一下,是什麼造成閃退的?由於每個縣市的地區數量不一定匹配所造成的閃退,那每次更新城市都讓地區跳回第一個 row,不就不會閃退了對吧?讓我們來改寫一下程式

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if component == 0 {
pickerView.reloadComponent(1)
//讓每個地區回到第一列
pickerView.selectRow(0, inComponent: 1, animated: true)
}

let selectcity = pickerView.selectedRow(inComponent: 0)
let selectdist = pickerView.selectedRow(inComponent: 1)
let city = cityanddistarray[selectcity].city
let dist = cityanddistarray[selectcity].dist[selectdist]
print("city: \(city)")
print("dist: \(dist)")
cityLabel.text = city
distLabel.text = dist

}

終於成功了!

但真的都沒有問題了嗎?我們再多測試看看

怎麼又閃退了?原來是我們如果沒有等地區停止就快速切換城市的話,也會造成閃退 Index out of range

那如果我們能夠在滑動地區時,限制住畫面中城市,不讓它任意滑動的話,是不是就能解決了?

先將以下程式新增在 ViewController ,func isScrolling () 能幫助我們判斷 PickerView 是否在捲動

//判斷手反在捲動
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
}
}

接下來在以下程式加入程式碼,關鍵程式 isUserInteractionEnabled 這能夠讓 PickerView 禁止滑動

    //決定pickerview 區域的每筆資料
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if pickerView.isScrolling() {
pickerView.isUserInteractionEnabled = false
} else {
pickerView.isUserInteractionEnabled = true
}

if component == 0 {
return cityanddistarray[row].city
}else {
let selectcity = pickerView.selectedRow(inComponent: 0)
return cityanddistarray[selectcity].dist[row]
}
}

當我們滑動時,限制我們不能任意再次滑動畫面,當滾輪停止時,我們就能再次滑動

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
pickerView.isUserInteractionEnabled = true
if component == 0 {
pickerView.reloadComponent(1)
//讓每個地區回到第一列
pickerView.selectRow(0, inComponent: 1, animated: true)
}
let selectcity = pickerView.selectedRow(inComponent: 0)
let selectdist = pickerView.selectedRow(inComponent: 1)
let city = cityanddistarray[selectcity].city
let dist = cityanddistarray[selectcity].dist[selectdist]
print("city: \(city)")
print("dist: \(dist)")
cityLabel.text = city
distLabel.text = dist
}

太棒了!終於成功了!當然使用 UIPickerView 的方法有很多種,為了弄清楚使用這個元件花了不少的時間,比較不熟悉的是資料的部分,我有看到有人用特別的資料寫法,在快速移動地區再切換城市時,不會造成閃退,當然每個人都有喜好,每個人的程式都不同才有趣,之後會繼續深入配合其他元件做出變化,如果有人想要交流,歡迎留言謝謝。

GitHub

--

--