观察 SwiftUI TextField 中 NumberFormatter 的变化

Observe changes to NumberFormatter in SwiftUI TextField

我正在构建一个 macOS 应用程序,其中有一个可观察的 Formatter 对象,该对象使用 @AppStorage 来存储有效数字设置(请参阅 Formatter.swift)。使用主应用程序结构 MyApp.swift,将数字格式化程序作为环境对象传递给其他视图。在首选项 window SettingsView.swift 中,使用步进器调整有效数字。最后,数字格式化程序被分配给 ContentView.swift 中的文本字段以格式化输入。

问题是当设置视图中的有效数字发生变化时,内容视图中的文本字段不会自动更新它们的格式。文本标签会自动更新,因为它们直接读取应用存储值。但是文本字段没有观察到数字格式化程序的变化。如果我更改设置,重新启动应用程序,那么文本字段将正确显示更新后的格式。但是,当格式化程序的有效数字发生变化时,如何让文本字段更新?

Formatter.swift

import Foundation
import SwiftUI

class Formatter: ObservableObject {
    
    @AppStorage("minSigDigits") var minSigDigits = 1
    @AppStorage("maxSigDigits") var maxSigDigits = 6
    
    var numberFormatter: NumberFormatter {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        formatter.usesSignificantDigits = true
        formatter.minimumSignificantDigits = self.minSigDigits
        formatter.maximumSignificantDigits = self.maxSigDigits
        return formatter
    }
    
    func resetValues() {
        self.minSigDigits = 1
        self.maxSigDigits = 6
    }
}

MyApp.swift

import SwiftUI

@main
struct MyApp: App {
    
    @StateObject var formatter = Formatter()
    
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(formatter)
        }
        
        Settings {
            SettingsView().environmentObject(formatter)
        }
    }
}

ContentView.swift

import SwiftUI

struct ContentView: View {
    
    @State private var min: Double = 0.0
    @State private var max: Double = 1.0
    @EnvironmentObject var formatter: Formatter
    
    var body: some View {
        VStack {
            Text("Min Sig. Digits = \(formatter.minSigDigits)")
            TextField("enter min value", value: $min, formatter: formatter.numberFormatter)
            
            Text("Max Sig. Digits = \(formatter.maxSigDigits)")
            TextField("enter max value", value: $max, formatter: formatter.numberFormatter)
        }
        .padding()
        .frame(width: 400, height: 300)
    }
}

SettingsView.swift

import SwiftUI

struct SettingsView: View {
    
    @EnvironmentObject var formatter: Formatter
    
    var body: some View {
        VStack {
            Stepper("Min Significant Digits: \(formatter.minSigDigits)", value: $formatter.minSigDigits, in: 1...5)
            Stepper("Max Significant Digits: \(formatter.maxSigDigits)", value: $formatter.maxSigDigits, in: 6...10)
            
            Button("Reset Values") {
                formatter.resetValues()
            }
        }
        .padding()
        .frame(width: 300, height: 200)
    }
}

向 TextFields 添加一个 .id,这将在更改时强制重绘:

        Text("Min Sig. Digits = \(formatter.minSigDigits)")
        TextField("enter min value", value: $min, formatter: formatter.numberFormatter)
            .id(formatter.minSigDigits)
        
        Text("Max Sig. Digits = \(formatter.maxSigDigits)")
        TextField("enter max value", value: $max, formatter: formatter.numberFormatter)
            .id(formatter.maxSigDigits)

编辑: ...或将 .id 设置为整个视图以重绘所有视图:

    VStack {
        Text("Min Sig. Digits = \(formatter.minSigDigits)")
        TextField("enter min value", value: $min, formatter: formatter.numberFormatter)
        
        Text("Max Sig. Digits = \(formatter.maxSigDigits)")
        TextField("enter max value", value: $max, formatter: formatter.numberFormatter)
    }
    .id(formatter.maxSigDigits + formatter.minSigDigits)

基本逻辑是,每次id改变,SwiftUI都会重绘那部分。

你的 formatter.numberFormatter getter 每次都在创建一个对象(实际上是 2 个对象,因为它被调用了两次)它在主体中被调用,你不能在 SwiftUI 中这样做。 Body 需要快,因为它被重复调用并且创建立即丢弃的对象会减慢它的速度。

您需要重新设计,以便在 @AppStorage 的默认设置更改时无需通知 SwiftUI,您需要自己收听默认设置,使用新值更新 numberFormatter,并告诉 SwiftUI 重绘。

一种方法是将 numberFormatter 设为 @Published。然后使用 Combine 来监听您感兴趣的两个用户默认值的变化,使用这些值创建一个新的格式化程序,然后将管道的末端分配给 numberFormatter 已发布的 属性 ,这将通知 SwiftUI .这实际上就是 Combine 的 ObservableObject 的设计目的。您可以通过不使用它 @Published 而只是一个 let 来进一步优化它,然后手动发送 objectWillChange() 然后更新 let 格式化程序以使用默认值中的新参数。