[SwiftUI練級(jí)-2] 餐費(fèi)計(jì)算器 · part2
更多內(nèi)容歡迎關(guān)注公眾號(hào):BoBo清吧
第一天我們了解構(gòu)建這個(gè)餐費(fèi)計(jì)算器app需要用到的技術(shù),是時(shí)候把這些知識(shí)轉(zhuǎn)化成一個(gè)實(shí)際的app了。
SwiftUI 的一大優(yōu)點(diǎn)是,從理論到實(shí)踐的轉(zhuǎn)換很直接。
當(dāng)然,某些情況下我會(huì)留上一兩手,以便你提起注意力。不過(guò)要完成這個(gè)項(xiàng)目的大多數(shù)知識(shí),你已經(jīng)有所了解。現(xiàn)在讓我們把這些碎片組裝起來(lái)。
接下來(lái)一共有四個(gè)主題,你將應(yīng)用前面學(xué)到的有關(guān)Form,@State,Picker的知識(shí)。
用文本框從用戶(hù)那里讀取文本
我們?cè)跇?gòu)建的是一個(gè)AA計(jì)算器app,意味著用戶(hù)需要輸入賬單的總金額,參與AA的人數(shù),以及他們想要留下的小費(fèi)數(shù)額。
希望你已經(jīng)想到,我們需要用到三個(gè)@State屬性,因?yàn)橛腥龎K數(shù)據(jù)是我們需要用戶(hù)輸入app的。
那就從添加三個(gè) @State 屬性到ContentView結(jié)構(gòu)體開(kāi)始吧:
@State private var checkAmount =""@State private var numberOfPeople = 2@State private var tipPercentage = 2
如你所見(jiàn),上面的代碼給賬單金額一個(gè)空的字符串,給總?cè)藬?shù)默認(rèn)值2,給小費(fèi)比例默認(rèn)值2。
你一定會(huì)好奇,為什么賬單金額要用字符串來(lái)表示呢?很顯然Int或者Double不是更合理嗎?這個(gè)嘛,原因在于我們別無(wú)選擇:SwiftUI只允許用字符串來(lái)存儲(chǔ)文本框的值。
AA的人數(shù)默認(rèn)取2也是合理的 —— 雖然大多數(shù)時(shí)候不一定正確,但它是一個(gè)好的默認(rèn)值。那2%的小費(fèi)比例呢?看起來(lái)很奇怪是不是?我們一般不會(huì)留 2% 的小費(fèi)吧?
是的,當(dāng)然不會(huì)。但這里的2并不直接代表百分比,我們這里會(huì)用它來(lái)從一組預(yù)先定義好的小費(fèi)比例的數(shù)組中選取一個(gè)數(shù)值。你將會(huì)看到不同類(lèi)型的 Picker 如何工作。
因?yàn)槲覀冃枰鎯?chǔ)所有可能的小費(fèi)比例,讓我們創(chuàng)建一個(gè)新的屬性:
lettipPercentages = [10, 15, 20, 25, 0]
現(xiàn)在你明白2實(shí)際上代表 20% 的小費(fèi),因?yàn)樗莟ipPercentages[2]的值。
我們將一步一步地構(gòu)建這個(gè)app,首先從用戶(hù)輸入賬單金額的文本框開(kāi)始。
把body屬性修改成這樣:
var body: some View {? ? Form {? ? ? ? Section {? ? ? ? ? ? TextField("Amount", text:$checkAmount)? ? ? ? }? ? }}
上面的代碼會(huì)創(chuàng)建一個(gè)section,它包含一行:我們的文本框。當(dāng)你在表單中創(chuàng)建文本框的時(shí)候,第一個(gè)參數(shù)是字符串,它被用作占位符——文本框里顯示的灰色文本,告知用戶(hù)文本框里應(yīng)該填寫(xiě)什么內(nèi)容。第二個(gè)參數(shù)是對(duì)checkAmount屬性的雙向綁定,這意味著當(dāng)用戶(hù)輸入時(shí),屬性也將被更新。
@State屬性包裝器有一個(gè)極大的好處是:它會(huì)自動(dòng)關(guān)注屬性的變化。當(dāng)改變發(fā)生時(shí),body計(jì)算屬性會(huì)被自動(dòng)調(diào)用。換句話(huà)說(shuō),你的UI 會(huì)被重新加載,以反映改變的狀態(tài),而這正是 SwiftUI 工作方式的一個(gè)基礎(chǔ)特性。
為了演示這一點(diǎn),我們添加第二個(gè) Section,展示checkAmount的值,就像這樣:
Form {? ? Section {? ? ? ? TextField("Amount", text:$checkAmount)? ? }? ? Section {? ? ? ? Text("$\(checkAmount)")? ? }}
稍后我們會(huì)讓它再顯示別的東西,現(xiàn)在讓我們?cè)谀M器里運(yùn)行這個(gè)app嘗試一下吧。
點(diǎn)擊賬單金額的文本框,輸入文字——不需要一定是數(shù)字,任何文字都可以。你會(huì)發(fā)現(xiàn)隨著你的輸入,第二個(gè) section 會(huì)自動(dòng)更新,反映你的動(dòng)作。
這個(gè)同步過(guò)程之所以會(huì)發(fā)生是因?yàn)椋?/p>
我們的文本框有一個(gè)隊(duì)checkAmount屬性的雙向綁定。
checkAmount屬性被標(biāo)記了@State,它會(huì)自動(dòng)關(guān)注屬性值的變化。
當(dāng)一個(gè)@State屬性發(fā)生改變時(shí),SwiftUI 將重新調(diào)用body屬性 (也就是重新加載我們的 UI)。
因此第二靜態(tài)文本會(huì)自動(dòng)獲得更新后的checkAmount的值。
最終的項(xiàng)目并不會(huì)在那個(gè)文本視圖里展示checkAmount,但目前為止這樣足夠了。在我們推進(jìn)之前,我還想指出一個(gè)重要的問(wèn)題:當(dāng)你點(diǎn)擊文本框輸入文本時(shí),用戶(hù)看到的是一個(gè)常規(guī)的字母鍵盤(pán)。當(dāng)然,他們可以點(diǎn)擊鍵盤(pán)上的按鈕切換到數(shù)字鍵盤(pán),但這會(huì)變得很煩人并且沒(méi)有必要。
幸運(yùn)的是,文本框有一個(gè)修改器,允許我們強(qiáng)制不同的鍵盤(pán)類(lèi)型:keyboardType()。我們可以給這個(gè)參數(shù)指定我們想要的鍵盤(pán)類(lèi)型,在這例子中,.numberPad或者.decimalPad都是不錯(cuò)的選擇。兩種鍵盤(pán)都會(huì)顯示數(shù)字0 到數(shù)字 9,但是.decimalPad也會(huì)顯示小數(shù)點(diǎn)。因此,如果用戶(hù)可以輸入像 ¥120.50 這樣的數(shù)字而非全部都是整數(shù)。
修改文本框:
TextField("Amount", text:$checkAmount)? ? .keyboardType(.decimalPad)
你注意到我在.keyboardType前面加了一個(gè)換行,并且有意地相對(duì)TextField做了一級(jí)縮進(jìn)。這并非要求的,但可以幫助閱讀,以一眼看清修改器是應(yīng)用于哪些視圖。
再次運(yùn)行app,這次你會(huì)發(fā)現(xiàn)你可以直接輸入數(shù)字到文本框里了。
Tip:.numberPad和.decimalPad鍵盤(pán)類(lèi)型告訴 SwiftUI 展示數(shù)字 0 到數(shù)字 9 以及可選的小數(shù)點(diǎn),但這并未阻止用戶(hù)輸入其他類(lèi)型的值。舉個(gè)例子,如果用戶(hù)有實(shí)體鍵盤(pán)他們可以輸入他們想要的文本,或者他們從別的某個(gè)地方復(fù)制了文本并且粘貼進(jìn)文本框。沒(méi)關(guān)系,到最后我們會(huì)處理這個(gè)問(wèn)題。
在表格里創(chuàng)建Picker
SwiftUI 的 Picker 服務(wù)于幾種目的,它們長(zhǎng)啥樣是取決具體的設(shè)備以及它們所處的上下文。
在我們的項(xiàng)目中,我們有一個(gè)表單,要求用戶(hù)輸入賬單總金額,然后我們會(huì)添加一個(gè) Picker,以便用戶(hù)可以選擇總共有幾個(gè)人要參與分?jǐn)傎M(fèi)用。
Picker,就像文本框,也需要一個(gè)基于屬性的雙向綁定,以便追蹤一個(gè)值。前面我們已經(jīng)創(chuàng)建了一個(gè)@State屬性服務(wù)于這個(gè)控件,名字叫numberOfPeople,因此我們接下來(lái)的任務(wù)是遍歷2到99,然后在 Picker 中顯示它們。
修改表單中的第一個(gè)Section,改成這樣:
Section {? ? TextField("Amount", text:$checkAmount)? ? ? ? .keyboardType(.decimalPad)? ? Picker("Number of people", selection:$numberOfPeople) {? ? ? ? ForEach(2 ..< 100) {? ? ? ? ? ? Text("\($0) people")? ? ? ? }? ? }}
在模擬器里再次嘗試以上代碼。
希望你注意到幾件事:
表單里出現(xiàn)一個(gè)新行,左邊是 “Number of people” ,右邊是 “4 people”。
右邊緣有一個(gè)灰色的指示器,它是iOS用以表示點(diǎn)擊該行會(huì)跳轉(zhuǎn)一個(gè)新界面的方式。
點(diǎn)擊該行并沒(méi)有跳轉(zhuǎn)新界面。
該行顯示的是 “4 people”,但我們給numberOfPeople屬性的默認(rèn)值是 2。
我們會(huì)修復(fù)上面的問(wèn)題,先來(lái)個(gè)簡(jiǎn)單的吧:為什么是4個(gè)人而不是numberOfPeople的默認(rèn)值2呢?注意,用ForEach創(chuàng)建視圖時(shí)的代碼是這樣的:
ForEach(2 ..< 100) {復(fù)制代碼
計(jì)數(shù)是從 2 到 100,創(chuàng)建行。這意味著我們的第0行 —— 被創(chuàng)建的第一行,包含的是 “2 People”,因此當(dāng)我們給numberOfPeople設(shè)置值為 2 時(shí),我們實(shí)際上是在選取第3行,也就是 “4 People”。
所以,盡管說(shuō)起來(lái)有點(diǎn)繞,我們的 UI 顯示 “4 people” 而不是 “2 people” 其實(shí)并不是一個(gè)bug。那么我們的代碼中就剩一個(gè)大bug了:為什么點(diǎn)擊這一行沒(méi)有反應(yīng)呢?
如果你是表單之外創(chuàng)建 picker,iOS 會(huì)顯示 spinning wheel 風(fēng)格的 picker。但是在這里,我們已經(jīng)告知 SwiftUI 這是一個(gè)供用戶(hù)輸入的表單,因此 picker 的外觀會(huì)自動(dòng)改變,以便盡量少占空間。
現(xiàn)在,讓我們添加一個(gè)導(dǎo)航視圖,它能為我們做兩件事:一是在頂部提供一塊區(qū)域放置標(biāo)題,二是讓iOS在需要時(shí)滑入新的視圖。
#代碼如下:
var body: some View {? ? NavigationView {? ? ? ? Form {? ? ? ? ? ? Section {? ? ? ? ? ? ? ? TextField("Amount", text:$checkAmount)? ? ? ? ? ? ? ? ? ? .keyboardType(.decimalPad)? ? ? ? ? ? ? ? ? ? ? ? ? Picker("Number of people", selection:$numberOfPeople) {? ? ? ? ? ? ? ? ? ? ForEach(2 ..< 100) {? ? ? ? ? ? ? ? ? ? ? ? Text("\($0) people")? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? ? ? Section {? ? ? ? ? ? ? ? Text("$\(checkAmount)")? ? ? ? ? ? }? ? ? ? }? ? }}
再次運(yùn)行程序,你會(huì)看到頂部區(qū)域現(xiàn)在有一大片灰色區(qū)域,這是iOS提供給我們放置標(biāo)題的空間。我們稍后會(huì)添加標(biāo)題,現(xiàn)在讓我們先點(diǎn)擊 Number Of People 這一行,你會(huì)發(fā)現(xiàn)新的一屏滑入,包含所有 Picker 的選項(xiàng)。
你會(huì)發(fā)現(xiàn) “4 People” 的旁邊有一個(gè)選中標(biāo)記。因?yàn)樗潜贿x中的值,當(dāng)你點(diǎn)擊其他的選項(xiàng),屏幕會(huì)自動(dòng)滑回前一屏,選項(xiàng)變成你剛才選擇的新值。
到這里,你應(yīng)該對(duì)”聲明式UI設(shè)計(jì)“的重要性有所體會(huì)。聲明式意味著我們說(shuō)出我們想要的,而不是事情應(yīng)該怎么做。我們說(shuō)我們需要一個(gè) picker ,里面包含了哪些值,但剩下的事,包括是采用舵輪還是新的視圖,這是由 SwiftUI 決定的。它之所以選擇了滑出新的視圖,是因?yàn)?picker 處在表單內(nèi),但在其他的平臺(tái)和環(huán)境下,它可能會(huì)選擇別的方式。
在我們完成這一步之前,讓我們給導(dǎo)航欄加上標(biāo)題,用一個(gè)修改器來(lái)實(shí)現(xiàn):
.navigationBarTitle("WeSplit")
提示:我猜你一定會(huì)忍不住把這個(gè)修改器加在NavigationView 后面對(duì)吧?但實(shí)際上它應(yīng)該被加在Form的后面。原因是,NavigationView 其實(shí)可以在你的程序運(yùn)行時(shí)顯示許多不同的標(biāo)題,這些標(biāo)題是從它當(dāng)下具體的子內(nèi)容中獲取的。你可以把它理解成視圖的一個(gè)屬性,NavigationView 作為視圖的父節(jié)點(diǎn),可以讀取這些屬性,并且根據(jù)上下文選擇最合適的那個(gè)標(biāo)題,浮動(dòng)到頂部區(qū)域。