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 .