SwiftUI:我可以在视图中添加初始化代码,而不必覆盖默认的初始化程序吗?

huangapple go评论67阅读模式
英文:

SwiftUI: can I add on-init code to a view without overriding the default initializer?

问题

我是一名经验丰富的程序员,但对Swift和SwiftUI完全不熟悉。(昨天在Playgrounds应用程序中学习了一些入门课程。)

我尝试编写的应用程序接受多个长度测量值作为输入,这有一定难度,因此我认为应该将其封装到自己的视图中。受[此答案][1]启发,这是我目前的进展:

struct LengthMeasurementField: View {
    var placeholder: String
    @Binding var binding: Measurement<UnitLength>
    @State var textValue = ""
    
    var body: some View {
        TextField(placeholder, text: $textValue)
            .fixedSize()
            .textFieldStyle(.roundedBorder)
            .onChange(of: textValue) { val in
                if let newValue = Double(val) {
                    binding = Measurement(value: newValue, unit: UnitLength.inches)
                }
            }
    }
}

然后我这样使用它:

HStack {
    Text("Diameter")
    LengthMeasurementField(placeholder: "Diameter", binding: $inputs.diameter)
}

这样渲染了,但如果在视图首次渲染时inputs.diameter已经有一个值,那个值会被忽略 - UI仅呈现占位文本。这是因为 textValue 初始化为空字符串,并且只能通过onChange更新。

我想让textValue使用绑定的字符串格式化值初始化。我尝试过这样:

init() {
    textValue = String(format: "%f", binding.value)
}

但是,这当然会消除默认的初始化程序,因此现在我的用法会出错。然后我尝试显式实现默认初始化程序,但它无法编译,我认为由于@Binding发生了一些特殊情况:

init(placeholder: String, binding: Measurement<UnitLength>) {
    self.placeholder = placeholder
    self.binding = binding // 在初始化之前使用变量'self.binding'
    self.textValue = String(format: "%f", binding.value) // 在初始化之前使用变量'self.binding'
} // 从初始化程序返回,而没有初始化所有存储的属性

这提出了几个问题:

  1. 为什么它无法编译?我该如何修复它?
  2. 是否有办法在视图上提供一些初始化时的代码,同时仍然可以访问默认的初始化程序?

附注:我知道如果不使用Measurement,可以更容易地完成所有这些操作,只需在其中具有格式化Double的字段即可。我计划通过下拉菜单选择单位,并且不会在长期内将其硬编码为.inches,因此我认为我的当前方法可能是值得的。如果您有其他了解或者已经存在此功能的内置工具,请告诉我,我会感激您的任何建议!

英文:

I'm a very experienced programmer, but I'm completely new to Swift and SwiftUI. (I took a few of the intro courses in the Playgrounds app yesterday.)

The app I'm trying to write takes several length measurements as input, which is nontrivial, so I figured I should encapsulate that into its own view. Using [this answer][1] as inspiration, here's what I have so far:

struct LengthMeasurementField: View {
    var placeholder : String
    @Binding var binding : Measurement&lt;UnitLength&gt;
    @State var textValue = &quot;&quot;
    
    var body: some View {
        TextField(placeholder, text: $textValue)
            .fixedSize()
            .textFieldStyle(.roundedBorder)
            .onChange(of: textValue) { val in
            if let newValue = Double(val) {
                binding = Measurement(value: newValue, unit: UnitLength.inches)
            }
        }
    }
}

Which I then use as follows:

HStack {
    Text(&quot;Diameter&quot;)
    LengthMeasurementField(placeholderText: &quot;Diameter&quot;, binding: $inputs.diameter)
}

This renders, but if inputs.diameter already has a value when the view is first rendered, that value is ignored -- the UI renders with just the placeholder text. This is because textValue is initialized as empty string and only updated via onChange.

I'd like to have textValue initialize with the string-formatted value from the binding. I tried this:

init() {
    textValue = String(format: &quot;%f&quot;, binding.value)
}

But, of course, that eliminates the default initializer, so now my usage breaks. I then tried implementing the default initializer explicitly, but it doesn't compile, I think something special is happening because of the @Binding:

    init(placeholder: String, binding: Measurement&lt;UnitLength&gt;) {
        self.placeholder = placeholder
        self.binding = binding // Variable &#39;self.binding&#39; used before being initialized
        self.textValue = String(format: &quot;%f&quot;, binding.value) // Variable &#39;self.binding&#39; used before being initialized
    } // Return from initializer without initializing all stored properties

This raises a few questions:

  1. Why doesn't it compile? How can I fix it?
  2. Is there a way for me to provide some on-init code for my view while still having access to the default initializer?

Side note: I know that I could do all this much more easily if I didn't use Measurement, and just had a field with a formatted Double in it. I plan to make the unit selectable via a dropdown, and won't be hardcoding it to .inches in the long term, so I think my current approach is probably going to be worthwhile. If you know otherwise, or if there's some built-in thing that already does this somewhere, I'd appreciate any advice you might have!

答案1

得分: 2

你覆盖合成的初始化方法是正确的方法。你只是定义了错误的参数类型:

init(placeholder: String, binding: Binding<Measurement<UnitLength>>) {
                                       ^^^^^^^

你可以在Xcode中右键点击结构体名称,然后选择"显示代码操作 > 生成成员初始化程序"来让Xcode为你生成这段代码。

英文:

Your overriding of the synthesized initializer is the correct approach. You've just defined the wrong parameter types:

init(placeholder: String, binding: Binding&lt;Measurement&lt;UnitLength&gt;&gt;) {
                                   ^^^^^^^

You can get Xcode to write this for you by right-clicking the struct name, and selecting "Show Code Actions> Generate Memberwise Initializer."

答案2

得分: 2

使用视图修饰符.onAppearLengthMeasurementField中设置textValue以回答问题2,这可以使问题变得不相关。

.onAppear {
    textValue = binding.formatted()
}
英文:

To answer question 2 which kind of makes question irrelevant, you can use the view modifier .onAppear to set up textValue correctly in LengthMeasurementField

.onAppear {
    textValue = binding.formatted()
}

huangapple
  • 本文由 发表于 2023年6月5日 22:29:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/76407474.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定