#36 用 UIPickerView, UITapGestureRecognizer串接聖經經文API
在尋找適合練習的API get時,忽然看到有Bible的API,查詢經文需要知道聖經的書卷名稱(例如馬太福音、哥林多前書等等)、第幾章以及第幾節,若再加上不同的翻譯版本,則有四個變數,非常適合用來練習UIPickerView。
搭配UITapGestureRecognizer,輕拍螢幕時串接API更新顯示經文
作業來源:
APP畫面:
首先,先找到聖經經文API的文件:
仔細觀察它的參數有哪些
解析 JSON:
也可以用Postman解析JSON,但在上傳資料時才會真正顯出它的功用
不需要全部寫進 struct,只寫需要的項目就好
struct BibleVerse: Decodable {
let reference: String
let text: String
let translation_id: String
}
建立資料:
透過API文件,確認可以使用文章開頭所說的四個項目:
✨ 書卷名稱
✨ 第幾章
✨ 第幾節
✨ 翻譯版本
去組成網址,取得對應的經文。
聖經的書卷很多,加上對應的章節,資料會很龐大,所以先用四福音(馬太福音、馬可福音、路加福音跟約翰福音)的部分來練習。
為了讓UIPickerView有依循的資料:第一步是建立資料的struct
struct Bible {
let bookName: String
let chapters: [Chapter]
}
struct Chapter {
let chapterNo: String
let verses:[String]
}
然後依照struct把資料用array具體化出來:
跟UITableVIew做法非常相似
有些書卷的章節有80節這麼多,身為IT新鮮人就試著寫程式來解決吧!
在playground寫了一個迴圈幫我打好數字
翻譯版本則自成一個array
let translation = ["web","bbe","kjv"]
UIPikerView :
UIPikerView 的 dataSource 跟 delegate 都要連到 Controller 才會正常運作。
UIPikerView就是可以上下滑動選則的滾輪(wheel),程式寫法跟UITableView 很像,所以也要讓所在的 Controller 繼承UIPickerViewDataSource,其底下的兩個 function 會決定外觀
Component:決定有幾個 wheel
row:決定一個 Component 裡有多少選項
extension BibleViewController: UIPickerViewDataSource {
func numberOfComponents(in pickerView: UIPickerView) -> Int {
4
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0 {
return bibles.count
} else if component == 1 {
let bookRow = pickerView.selectedRow(inComponent: 0)
return bibles[bookRow].chapters.count
} else if component == 2 {
let bookRow = pickerView.selectedRow(inComponent: 0)
let chapterRow = pickerView.selectedRow(inComponent: 1)
return bibles[bookRow].chapters[chapterRow].verses.count
} else {
return translation.count
}
}
}
說明:
Component共有4個,跟array一樣,從左至右分別是 0,1,2,3
row則對應資料上array的變化
Controller 繼承 UIPickerViewDelegate,其底下兩個function決定顯示的文字及點選時wheel要做的反應
extension BibleViewController: UIPickerViewDelegate {
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0 {
return bibles[row].bookName
} else if component == 1 {
let bookRow = pickerView.selectedRow(inComponent: 0)
return bibles[bookRow].chapters[row].chapterNo
} else if component == 2 {
let bookRow = pickerView.selectedRow(inComponent: 0)
let chapterRow = pickerView.selectedRow(inComponent: 1)
return bibles[bookRow].chapters[chapterRow].verses[row]
} else {
return translation[row]
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if component == 0 {
pickerView.selectRow(6, inComponent: 1, animated: true)
pickerView.selectRow(13, inComponent: 2, animated: true)
} else if component == 1 {
pickerView.selectRow(13, inComponent: 2, animated: true)
}
pickerView.reloadAllComponents()
let bookRow = pickerView.selectedRow(inComponent: 0)
let chapterRow = pickerView.selectedRow(inComponent: 1)
let verseRow = pickerView.selectedRow(inComponent: 2)
let translationRow = pickerView.selectedRow(inComponent: 3)
let bookName = bibles[bookRow].bookName
let chapter = bibles[bookRow].chapters[chapterRow].chapterNo
let verse = bibles[bookRow].chapters[chapterRow].verses[verseRow]
let translationVersion = translation[translationRow]
self.bookName = bookName
self.chapter = chapter
self.verse = verse
self.translationVersion = translationVersion
}
func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
if component == 0 {
return 120
} else if component == 1, component == 2 {
return 30
} else {
return 70
}
}
}
說明:
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String?
依照資料去設定要wheel顯示的字樣
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if component == 0 {
pickerView.selectRow(6, inComponent: 1, animated: true)
pickerView.selectRow(13, inComponent: 2, animated: true)
} else if component == 1 {
pickerView.selectRow(13, inComponent: 2, animated: true)
}
pickerView.reloadAllComponents()
let bookRow = pickerView.selectedRow(inComponent: 0)
let chapterRow = pickerView.selectedRow(inComponent: 1)
let verseRow = pickerView.selectedRow(inComponent: 2)
let translationRow = pickerView.selectedRow(inComponent: 3)
let bookName = bibles[bookRow].bookName
let chapter = bibles[bookRow].chapters[chapterRow].chapterNo
let verse = bibles[bookRow].chapters[chapterRow].verses[verseRow]
let translationVersion = translation[translationRow]
self.bookName = bookName
self.chapter = chapter
self.verse = verse
self.translationVersion = translationVersion
}
function UIPickerVew didSelectRow 決定點選時wheel的反應。
可能引起APP 閃退的狀況:
✨狀況一:
pickerView.reloadAllComponents():顯示更新,要寫在pickerView.selectRow之後,不然可能會忽然閃退。
可是為什麼有些project上的順序是相反的還是可以運作,我猜想是因為資料比較單純變化較少,一旦資料變得複雜一些就可能會閃退。
✨狀況二:
當點選row時,可能會使children component變化,假設children component只有6個row,意即row只有0到5,若設定點選後row=6則也會閃退。
串接API GET:
var bookName = "Matthew"
var chapter = "1"
var verse = "1"
var translationVersion = "web"
func fetchBibleVerse(bookName: String, chapter: String, verse: String) {
let urlString = "https://bible-api.com/\(bookName)\(chapter):\(verse)?translation=\(translationVersion)"
if let url = URL(string: urlString) {
//var request = URLRequest(url: url)
URLSession.shared.dataTask(with: url) { data, response, error in
if let data {
let decoder = JSONDecoder()
do {
let result = try decoder.decode(BibleVerse.self, from: data)
DispatchQueue.main.async {
self.verseLabel.text = result.text
self.bookNameLabel.text = result.reference
self.versionLabel.text = "(\(result.translation_id))"
}
} catch {
print(error)
}
}
}.resume()
}
}
說明:
這次的作業,是把網址的參數用變數來取代,而在UIPickerView轉動時,又可以改變變數,最後再用手勢輕點去取得經文。
特別注意:URLSession的部分,最後一定要加上.resume( )去實際執行程式,不然不會有作用,這部分是在background thread執行。
跟畫面有關的程式一定要在main thread執行,所以會需要加入DispatchQueue.main.async { },讓裡面的程式在main thread裡執行。
UITapGestureRecognizer:
這裡有兩個做法:
作法一:
直接加一個UITapGestureRecognizer在所要作用的object上(例如ImageView),然後拉IBAction function,裡面輸入輕點後要觸發的事。
但這個做法有一個缺點,當要做用的object改變時(如從ImageView變成view)時,整個IBAction跟UITapGestureRecognizer要刪除重做。
做法二:
不需要新增UITapGestureRecognizer,只需輸入程式碼
@objc func refrech() {
fetchBibleVerse(bookName: bookName, chapter: chapter, verse: verse)
}
func tapToRefresh() {
let tapGesture = UITapGestureRecognizer()
tapGesture.numberOfTapsRequired = 2
tapGesture.addTarget(self, action: #selector(refrech))
backgroundImageView.isUserInteractionEnabled = true
backgroundImageView.addGestureRecognizer(tapGesture)
}
說明:
tapGesture.addTarget(self, action: #selector(refrech))
輕點之後要觸發的事。
action之後只能接受@objc的function,乾脆宣告一個refresh的@objc function,然後把要觸發的事包進去。
backgroundImageView.addGestureRecognizer(tapGesture)
把要輕點的object直接加進手勢裡
backgroundImageView.isUserInteractionEnabled = true
記得要開啟isUserInteractionEnable
GitHub:
這次的作業著重在UIPickerView跟UITapGestureRecognizer的練習。
聖經的API幾乎都是文字,沒有太多的變化,API再找其他的來練習。