#####构建了一个简单的货币转换器来试验SwiftUI的状态驱动的视图更新。
今天我们将开始构建我们的第一个使用SwiftUI的小项目。Xcode 11目前仍然处于第一个beta版本,所以有一些东西还没有用,我们还在弄清楚SwiftUI是如何工作的,所以有时这会导致我们是否做错了什么的混乱或者框架是。
但是我们确实理解了今天我们要构建的东西,并且我们知道我们将使用的SwiftUI组件正常工作。我们正在建设的是一个小型货币转换器。我们之前使用过这个例子,因为它让我们可以玩反应视图并使用它们:具有多个输入和输出的视图相互影响。
货币转换器有一个输入金额的文本字段和一个选择货币汇率的选择器视图,如果它们的任何一个值发生变化,我们需要将输入文本解析为一个金额并使用所选的汇率来更新输出。但是我们不必为执行这些更新编写太多代码,因为当应用程序的状态发生变化时,SwiftUI的状态驱动视图将自动更新。
点击此处进交流群 有技术的来闲聊 没技术的来学习
我们为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
,我们为文本字段设置了一个宽度,这样它就不会伸展到填满窗口,然后我们运行应用程序:
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
变量会更新。对状态的这种改变导致了对视图的重新渲染。因为我们正在为输出使用计算属性,所以我们立即看到新计算的输出值。
为了改善输出编号的格式,我们使用数字样式设置为的数字格式化程序.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)
}
}
}
}
}
复制代码
结果和后续步骤
当我们运行应用程序时,我们发现一切都按照我们期望的方式运行。我们可以更改输入金额和货币选择,输出显示转换金额和选定的货币符号。如果我们输入无效的输入量(例如包含字母数字的输入量),输出标签将显示“解析错误”。
接下来要采取一些合乎逻辑的步骤。首先,我们可以通过网络加载转换率。其次,如果我们能够转换转换以便我们可以输入外币金额并将其转换回欧元,那将是很好的。