#####构建了一个简单的货币转换器来试验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)
}
}
}
}
}
复制代码
当咱们运行应用程序时,咱们发现一切都按照咱们指望的方式运行。咱们能够更改输入金额和货币选择,输出显示转换金额和选定的货币符号。若是咱们输入无效的输入量(例如包含字母数字的输入量),输出标签将显示“解析错误”。
接下来要采起一些合乎逻辑的步骤。首先,咱们能够经过网络加载转换率。其次,若是咱们可以转换转换以便咱们能够输入外币金额并将其转换回欧元,那将是很好的。