#####構建了一個簡單的貨幣轉換器來試驗SwiftUI的狀態驅動的視圖更新。面試
今天咱們將開始構建咱們的第一個使用SwiftUI的小項目。Xcode 11目前仍然處於第一個beta版本,因此有一些東西尚未用,咱們還在弄清楚SwiftUI是如何工做的,因此有時這會致使咱們是否作錯了什麼的混亂或者框架是。swift
可是咱們確實理解了今天咱們要構建的東西,而且咱們知道咱們將使用的SwiftUI組件正常工做。咱們正在建設的是一個小型貨幣轉換器。咱們以前使用過這個例子,由於它讓咱們能夠玩反應視圖並使用它們:具備多個輸入和輸出的視圖相互影響。數組
貨幣轉換器有一個輸入金額的文本字段和一個選擇貨幣匯率的選擇器視圖,若是它們的任何一個值發生變化,咱們須要將輸入文本解析爲一個金額並使用所選的匯率來更新輸出。可是咱們沒必要爲執行這些更新編寫太多代碼,由於當應用程序的狀態發生變化時,SwiftUI的狀態驅動視圖將自動更新。bash
咱們爲macOS應用程序建立了一個新的Xcode項目,咱們選擇了SwiftUI選項。當咱們運行應用程序時,會打開一個窗口,其中包含一個標有「Hello World」的標籤,因此到目前爲止一切彷佛都有效。咱們ContentView
用咱們本身的Converter
視圖替換默認值,該視圖包含將佈置各類子視圖的水平堆棧:網絡
import SwiftUI
struct Converter: View {
var body: some View {
HStack {
// ...
}
}
}
複製代碼
水平堆棧中的第一個子視圖是一個TextField
視圖,該視圖Binding
在其初始化器中佔用一個。爲了將字符串變量轉換爲綁定,咱們在將它傳遞給文本字段時用美圓符號做爲前綴,並經過標記它將變量自己轉換爲狀態@State
:閉包
struct Converter: View {
@State var text: String = "100"
var body: some View {
HStack {
TextField($text)
}
}
}
複製代碼
一旦視圖被渲染,系統就會爲咱們管理狀態。經過在狀態變量前加上美圓符號,咱們將其轉換爲a Binding
,這是一個雙向通訊通道:每當text
屬性更改時,文本字段都會更新,反之亦然。框架
咱們在中添加了一些視圖HStack
,咱們爲文本字段設置了一個寬度,這樣它就不會伸展到填滿窗口,而後咱們運行應用程序:ide
struct Converter: View {
@State var text: String = "100"
var body: some View {
HStack {
TextField($text).frame(width: 100)
Text("EUR")
Text("=")
Text("TODO")
Text("USD")
}
}
}
複製代碼
如今讓咱們開始添加一些功能。咱們爲輸出添加了一個計算屬性,咱們嘗試Double
從輸入文本中解析a 。若是解析成功,咱們將該值乘以硬編碼速率,並返回包含在字符串中的結果。若是沒法解析輸入,則返回錯誤消息:學習
struct Converter: View {
@State var text: String = "100"
var output: String {
let parsed = Double(text)
return parsed.map { String($0 * 1.13) } ?? "parse error"
}
var body: some View {
HStack {
TextField($text).frame(width: 100)
Text("EUR")
Text("=")
Text(output)
Text("USD")
}
}
}
複製代碼
如今,當咱們輸入文本字段時,text
變量會更新。對狀態的這種改變致使了對視圖的從新渲染。由於咱們正在爲輸出使用計算屬性,因此咱們當即看到新計算的輸出值。ui
爲了改善輸出編號的格式,咱們使用數字樣式設置爲的數字格式化程序.currency
。咱們將格式化程序的貨幣符號設置爲空字符串,由於咱們已經在另外一個標籤中顯示貨幣:
struct Converter: View {
@State var text: String = "100"
let formatter: NumberFormatter = {
let f = NumberFormatter()
f.numberStyle = .currency
f.currencySymbol = ""
return f
}()
var output: String {
let parsed = Double(text)
return parsed.map { formatter.string(from: NSNumber(value: $0 * 1.13)) } ?? "parse error"
}
var body: some View {
HStack {
TextField($text).frame(width: 100)
Text("EUR")
Text("=")
Text(output)
Text("USD")
}
}
}
複製代碼
接下來咱們能夠嘗試包括一系列貨幣。咱們定義了一個簡單的貨幣名稱和費率字典。稍後,咱們能夠經過網絡加載這些費率,但咱們從一些硬編碼值開始:
struct Converter: View {
let rates: [String: Double] = ["USD": 1.13, "GBP": 0.89]
// ...
}
複製代碼
咱們將現有的水平堆棧包裝在一個垂直堆棧中,在它下面,咱們添加一個List
包含每種貨幣行的視圖。
爲了構建一個貨幣列表,咱們從rates
字典中獲取密鑰,並對它們進行明確排序,以確保它們在狀態更新時不會跳轉。而後咱們必須經過identified(by:)
使用指向Hashable
屬性的鍵路徑調用來指定如何識別數組的元素。因爲咱們正在使用一組惟一字符串,所以咱們能夠簡單地傳入關鍵路徑\.self
。
在每一行中,咱們設置一個水平堆棧,包括貨幣符號,間隔符和顯示貨幣匯率的標籤。咱們在rates
字典中查找速率,而後強制解包結果,由於咱們使用的是有效密鑰:
struct Converter: View {
// ...
var body: some View {
VStack {
HStack {
// ...
}
List {
ForEach(self.rates.keys.sorted().identified(by: \.self) { key in
HStack {
Text(key)
Spacer()
Text("\(self.rates[key]!)")
}
}
}
}
}
}
複製代碼
該列表如今顯示在應用程序中,但它以某種方式掩蓋了其餘視圖。這彷佛是SwiftUI中的一個錯誤,咱們 - 爲了簡潔起見 - 經過給列表一個固定的高度來解決這個問題:
struct Converter: View {
// ...
var body: some View {
VStack {
// ...
List {
// ...
}.frame(height: 100)
}
}
}
複製代碼
如今咱們在列表中顯示貨幣匯率,但咱們尚未對它們作任何事情。爲了準備顯示每一個速率的轉換輸出量,咱們將輸入量的解析拉出到計算屬性:
struct Converter: View {
// ...
@State var text: String = "100"
// ...
var parsedInput: Double? {
Double(text)
}
var output: String {
parsedInput.flatMap { formatter.string(from: NSNumber(value: $0 * 1.13)) } ?? "parse error"
}
// ...
}
複製代碼
旁註:Swift 5.1容許咱們return
在計算屬性中省略單行返回語句的關鍵字 - 咱們已經從單語句閉包中很好地理解了這一點。
理想狀況下,若是輸入解析有效,咱們但願編寫相似下面的內容,將轉換後的金額添加到每一個貨幣列表行:
HStack {
Text(key)
Spacer()
Text("\(self.rates[key]!)")
if let input = self.parsedInput {
Spacer()
Text("\(input * self.rates[key]!)")
}
}
複製代碼
但if let
視圖構建器功能中不支持。相反,咱們首先檢查變量是否不是nil
,而後強制解包它:
HStack {
Text(key)
Spacer()
Text("\(self.rates[key]!)")
if self.parsedInput != nil {
Spacer()
Text("\(self.parsedInput! * self.rates[key]!)")
}
}
複製代碼
如今應用程序運行,咱們能夠看到使用轉換結果更新的貨幣列表。
可是如今咱們看到它,咱們意識到列表不是這個應用程序的最佳界面。使用選擇器視圖選擇貨幣並更新原始水平堆棧中的輸出會更好。
咱們List
用a 代替Picker
。初始化器Picker
接受選擇的綁定和標籤,後者咱們將其設置爲空文本。咱們還爲選擇器的選擇添加了一個狀態:
struct Converter: View {
// ...
@State var selection: String = "USD"
// ...
var body: some View {
VStack {
// ...
Picker(selection: $selection, label: Text("")) {
ForEach(self.rates.keys.sorted().identified(by: \.self) { key in
// ...
}
}
}
}
}
複製代碼
經過綁定,selection
當咱們在用戶界面中選擇一個值時,選擇器會自動更新屬性。如今,咱們可使用此選訂貨幣從計算屬性中提供當前選定的匯率。在其中,咱們強制解包查找率,由於咱們知道選擇設置爲選擇器中的值,該選擇器僅包含rates
字典的有效鍵:
struct Converter: View {
// ...
@State var selection: String = "USD"
var rate: Double {
rates[selection]!
}
// ...
}
複製代碼
如今經過rate
計算output
字符串中的屬性,咱們將轉換爲在選擇器中選擇的貨幣:
struct Converter: View {
// ...
var output: String {
parsedInput.flatMap { formatter.string(from: NSNumber(value: $0 * self.rate)) } ?? "parse error"
}
// ...
}
複製代碼
爲了反映這一點,咱們在貨幣符號標籤中顯示選擇:
HStack {
TextField($text).frame(width: 100)
Text("EUR")
Text("=")
Text(output)
Text(selection)
}
複製代碼
最後,咱們從選擇器的行中刪除了無關的視圖,以便它們只包含Text
:
struct Converter: View {
// ...
var body: some View {
VStack {
// ...
Picker(selection: $selection, label: Text("")) {
ForEach(self.rates.keys.sorted().identified(by: \.self) { key in
Text(key)
}
}
}
}
}
複製代碼
當咱們運行應用程序時,咱們發現一切都按照咱們指望的方式運行。咱們能夠更改輸入金額和貨幣選擇,輸出顯示轉換金額和選定的貨幣符號。若是咱們輸入無效的輸入量(例如包含字母數字的輸入量),輸出標籤將顯示「解析錯誤」。
接下來要採起一些合乎邏輯的步驟。首先,咱們能夠經過網絡加載轉換率。其次,若是咱們可以轉換轉換以便咱們能夠輸入外幣金額並將其轉換回歐元,那將是很好的。