開發多個 column 的 UIPickerView — 以選擇台灣的縣市行政區為例

在 iOS App 輸入資料時,我們時常會看到長得像滾輪的 UIPickerView。UIPickerView 可以只有一排,但也可以多排,比方下圖台鐵 App 的 picker view 從第一排選擇地區,第二排選擇火車的站名。

製作多個 column 的 UIPickerView 會比單排複雜許多。接下來彼得潘將以搬家為例,說明如何利用兩個 column 的 picker view 選擇台灣的縣市 & 行政區。

我們想要製做的 App 畫面如下,我們的目標是搬到台北市大安區的豪宅,先假設我們即將中樂透,不用擔心錢的問題。(不過畫面的背景似乎有點像鬼屋)

加入台灣行政區的 JSON 檔,定義 JSON 對應的資料型別 & array

參考以下連結的說明操作。

asset 裡的 taiwanDistricts 儲存台灣行政區的 JSON 資料。

下圖顯示 JSON 的部分內容。

定義 JSON 對應的資料型別 & array data。

import UIKit

struct City: Decodable {
let name: String
let districts: [District]
}

struct District: Decodable {
let zip: String
let name: String
}

extension City {
static var data: [Self] {
guard let data = NSDataAsset(name: "taiwanDistricts")?.data else {
return []
}
return (try? JSONDecoder().decode([Self].self, from: data)) ?? []
}
}

在畫面上加入 picker view,label,image view & button

我們將在兩個地方顯示從 picker 選擇的行政區,在 label & 點選 button 顯示的 alert 上。

將 view controller 設為 picker view 的 dataSource & delegate

連結 outlet,action & 宣告 property cities 儲存行政區的資料

class ViewController: UIViewController {
@IBOutlet weak var districtPickerView: UIPickerView!
@IBOutlet weak var districtLabel: UILabel!
let cities = City.data

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}

@IBAction func showAlert(_ sender: Any) {
}
}

定義 protocol UIPickerViewDataSource & UIPickerViewDelegate 的相關 function

extension ViewController: UIPickerViewDataSource, UIPickerViewDelegate {

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

func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0 {
return cities.count
} else {
let cityRow = pickerView.selectedRow(inComponent: 0)
return cities[cityRow].districts.count
}

}

func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0 {
return cities[row].name
} else {
let cityRow = pickerView.selectedRow(inComponent: 0)
return cities[cityRow].districts[row].name
}
}

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if component == 0 {
pickerView.reloadComponent(1)
pickerView.selectRow(0, inComponent: 1, animated: true)
}
}

}

說明

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

設定 picker view 有兩排,第一排顯示縣市清單,第二排顯示縣市對應的行政區清單。

func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0 {
return cities.count
} else {
let cityRow = pickerView.selectedRow(inComponent: 0)
return cities[cityRow].districts.count
}

}

設定 picker view 每排顯示的資料數量。第一排的數量是縣市的數量,第二排的數量是選取的縣市對應的行政區數量。我們必須先取得第一排選取的縣市,才能設定第二排顯示的行政區數量,比方台北市有 12 個行政區,高雄市有 40 個行政區。

我們利用 pickerView.selectedRow(inComponent: 0) 取得第一排選取的 row number,比方當 row number 為 0 時,表示選到台北市,而台北市有 12 個行政區,因此 cities[cityRow].districts.count 會回傳 12。

func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0 {
return cities[row].name
} else {
let cityRow = pickerView.selectedRow(inComponent: 0)
return cities[cityRow].districts[row].name
}
}

設定 picker view 顯示的字串,第一排顯示縣市清單,第二排顯示縣市對應的行政區清單。

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if component == 0 {
pickerView.reloadComponent(1)
pickerView.selectRow(0, inComponent: 1, animated: true)
}
}

當使用者滑動 picker,picker 停止滑動時會呼叫 pickerView(_:didSelectRow:inComponent:)。當第一排選取的縣市改變時,我們必須更新第二排的行政區。比方三民區在高雄市,若是沒有更新,讓三民區出現在台北市,那可就要天下大亂了。

當 component 為 0 時,表示選取的縣市改變,因此我們從 pickerView 呼叫 reloadComponent(1) 更新第二排行政區的內容,然後呼叫 selectRow(0, inComponent: 1, animated: true) 讓第二排滑到第一筆,確保每次縣市改變時,都會顯示縣市的第一個行政區。

結果

Cool,現在我們的 picker view 已經可以正確顯示縣市跟行政區,而且選取的縣市改變時,行政區也會立即更新。

滑動 picker,停止滑動時在 label 顯示選取的內容

從 pickerView 呼叫 selectedRow(inComponent: 0) & selectedRow(inComponent: 1) ,取得第一排跟第二排選取的 row number,然後再從 cities 取得對應的縣市跟行政區。

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if component == 0 {
pickerView.reloadComponent(1)
pickerView.selectRow(0, inComponent: 1, animated: true)
}
let cityRow = pickerView.selectedRow(inComponent: 0)
let districtRow = pickerView.selectedRow(inComponent: 1)
districtLabel.text = "\(cities[cityRow].name)\(cities[cityRow].districts[districtRow].name)"
}

點擊 button 時顯示 alert

從 districtPickerView 呼叫 selectedRow(inComponent: 0) & selectedRow(inComponent: 1) ,取得第一排跟第二排選取的 row number,然後再從 cities 取得對應的縣市跟行政區。

@IBAction func showAlert(_ sender: Any) {
let cityRow = districtPickerView.selectedRow(inComponent: 0)
let districtRow = districtPickerView.selectedRow(inComponent: 1)
let message = "\(cities[cityRow].name)\(cities[cityRow].districts[districtRow].name)"
let alertController = UIAlertController(title: "我想搬到", message: message, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default)
alertController.addAction(okAction)
present(alertController, animated: true)
}

範例連結

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com