#36 用 UIPickerView, UITapGestureRecognizer串接聖經經文API

在尋找適合練習的API get時,忽然看到有Bible的API,查詢經文需要知道聖經的書卷名稱(例如馬太福音、哥林多前書等等)、第幾章以及第幾節,若再加上不同的翻譯版本,則有四個變數,非常適合用來練習UIPickerView。

搭配UITapGestureRecognizer,輕拍螢幕時串接API更新顯示經文

作業來源:

APP畫面:

GIF

首先,先找到聖經經文API的文件:

仔細觀察它的參數有哪些

解析 JSON:

也可以用Postman解析JSON,但在上傳資料時才會真正顯出它的功用

Postman

不需要全部寫進 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 的 dataSourcedelegate 都要連到 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則也會閃退。

APP crash

串接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再找其他的來練習。

--

--