SwiftUI 和测量转换:只执行第一次转换
SwiftUI and Measurement conversions : only first conversion is performed
在 SwiftUI 中使用时,我一直在为测量而苦恼。我希望能够即时转换测量结果,但只有第一次转换有效。所有后续的都失败了。
我已经设法将其缩小为可重现的测试用例。
首先,快速检查一下 Foundation 是否正常工作:
// Let’s check that conversion is possible, and works.
var test = Measurement<UnitLength>(value: 13.37, unit: .meters)
print("Original value: \(test.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
// prints: Original value: 13.37 m
test.convert(to: .centimeters)
print("In centimeters: \(test.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
// prints: In centimeters: 1,337 cm
test.convert(to: .kilometers)
print("In kilometers: \(test.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
// prints: In kilometers: 0.01337 km
test.convert(to: .meters)
print("Back to meters: \(test.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
// prints: Back to meters: 13.37 m
好的,所以它适用于基础级别。我可以多次来回转换测量值。
现在 运行 下面这个 ContentView,click/tap 任何按钮。第一个会成功,其他的会失败。
struct ContentView: View {
@State var distance = Measurement<UnitLength>(value: 13.37, unit: .meters)
var body: some View {
VStack {
Text("Distance = \(distance.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
Button("Convert to cm") { print("Convert to cm"); distance.convert(to: .centimeters) }
Button("Convert to m") { print("Convert to m"); distance.convert(to: .meters) }
Button("Convert to km") { print("Convert to km"); distance.convert(to: .kilometers) }
}
.onChange(of: distance, perform: { _ in
print("→ new distance = \(distance.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
})
.frame(minWidth: 300)
.padding()
}
}
这在 macOS 和 iOS 上都可以重现。
这到底是为什么第一次转换成功,后面的都失败了? onChange 甚至没有被触发。
我在 Xcode 13.3、macOS 12.3、iOS 15.4
Measurement
在过去的几年里,苹果并没有得到太多的爱。虽然这应该适用于 SwiftUI,但事实并非如此。答案是在视图模型 class 中进行转换,并让它更新发布的值。像这样:
class MeasurementConverterViewModel: ObservableObject {
@Published var distance: Measurement<UnitLength>
init(distance: Measurement<UnitLength>) {
self.distance = distance
}
public func convertToCM() {
distance.convert(to: .centimeters)
}
public func convertToM() {
distance.convert(to: .meters)
}
public func convertToKM() {
distance.convert(to: .kilometers)
}
}
这确实有效,但我会检查 Open Radar 以查看是否有人发布了错误报告并提交了错误。这应该像您在 SwiftUI 中编码一样工作。
如果您看一下 Measurement
的 header,您会看到它是一个 ReferenceConvertible
并且该状态的文档“应用于支持的类型的装饰通过 Foundation 引用类型。”因此它可能不具有 @State
SwiftUI 检测更改所需的值语义。这是一个解决方法:
import SwiftUI
struct MTContentViewConfig: Equatable {
var distance = Measurement<UnitLength>(value: 13.37, unit: .meters)
var seed = 0
public mutating func convert(to otherUnit: UnitLength) {
distance.convert(to: otherUnit)
seed += 1
}
}
struct MTContentView: View {
@State var config = MTContentViewConfig()
var body: some View {
VStack {
Text("Distance = \(config.distance.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
Button("Convert to cm") { print("Convert to cm")
config.convert(to: .centimeters) }
Button("Convert to m") { print("Convert to m")
config.convert(to: .meters) }
Button("Convert to km") { print("Convert to km")
config.convert(to: .kilometers) }
}
.onChange(of: config) { newVal in
print("→ new distance = \(newVal.distance.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
}
.frame(minWidth: 300)
.padding()
}
}
顺便说一下,我建议为 Text
提供 MeasurementFormatter
,而不是手动格式化,这样 Text
会在区域设置更改时自动更新 UILabel
.
在 SwiftUI 中使用时,我一直在为测量而苦恼。我希望能够即时转换测量结果,但只有第一次转换有效。所有后续的都失败了。
我已经设法将其缩小为可重现的测试用例。
首先,快速检查一下 Foundation 是否正常工作:
// Let’s check that conversion is possible, and works.
var test = Measurement<UnitLength>(value: 13.37, unit: .meters)
print("Original value: \(test.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
// prints: Original value: 13.37 m
test.convert(to: .centimeters)
print("In centimeters: \(test.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
// prints: In centimeters: 1,337 cm
test.convert(to: .kilometers)
print("In kilometers: \(test.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
// prints: In kilometers: 0.01337 km
test.convert(to: .meters)
print("Back to meters: \(test.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
// prints: Back to meters: 13.37 m
好的,所以它适用于基础级别。我可以多次来回转换测量值。
现在 运行 下面这个 ContentView,click/tap 任何按钮。第一个会成功,其他的会失败。
struct ContentView: View {
@State var distance = Measurement<UnitLength>(value: 13.37, unit: .meters)
var body: some View {
VStack {
Text("Distance = \(distance.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
Button("Convert to cm") { print("Convert to cm"); distance.convert(to: .centimeters) }
Button("Convert to m") { print("Convert to m"); distance.convert(to: .meters) }
Button("Convert to km") { print("Convert to km"); distance.convert(to: .kilometers) }
}
.onChange(of: distance, perform: { _ in
print("→ new distance = \(distance.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
})
.frame(minWidth: 300)
.padding()
}
}
这在 macOS 和 iOS 上都可以重现。
这到底是为什么第一次转换成功,后面的都失败了? onChange 甚至没有被触发。
我在 Xcode 13.3、macOS 12.3、iOS 15.4
Measurement
在过去的几年里,苹果并没有得到太多的爱。虽然这应该适用于 SwiftUI,但事实并非如此。答案是在视图模型 class 中进行转换,并让它更新发布的值。像这样:
class MeasurementConverterViewModel: ObservableObject {
@Published var distance: Measurement<UnitLength>
init(distance: Measurement<UnitLength>) {
self.distance = distance
}
public func convertToCM() {
distance.convert(to: .centimeters)
}
public func convertToM() {
distance.convert(to: .meters)
}
public func convertToKM() {
distance.convert(to: .kilometers)
}
}
这确实有效,但我会检查 Open Radar 以查看是否有人发布了错误报告并提交了错误。这应该像您在 SwiftUI 中编码一样工作。
如果您看一下 Measurement
的 header,您会看到它是一个 ReferenceConvertible
并且该状态的文档“应用于支持的类型的装饰通过 Foundation 引用类型。”因此它可能不具有 @State
SwiftUI 检测更改所需的值语义。这是一个解决方法:
import SwiftUI
struct MTContentViewConfig: Equatable {
var distance = Measurement<UnitLength>(value: 13.37, unit: .meters)
var seed = 0
public mutating func convert(to otherUnit: UnitLength) {
distance.convert(to: otherUnit)
seed += 1
}
}
struct MTContentView: View {
@State var config = MTContentViewConfig()
var body: some View {
VStack {
Text("Distance = \(config.distance.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
Button("Convert to cm") { print("Convert to cm")
config.convert(to: .centimeters) }
Button("Convert to m") { print("Convert to m")
config.convert(to: .meters) }
Button("Convert to km") { print("Convert to km")
config.convert(to: .kilometers) }
}
.onChange(of: config) { newVal in
print("→ new distance = \(newVal.distance.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number)))")
}
.frame(minWidth: 300)
.padding()
}
}
顺便说一下,我建议为 Text
提供 MeasurementFormatter
,而不是手动格式化,这样 Text
会在区域设置更改时自动更新 UILabel
.