我在一个金钱输入屏幕上工作,我需要实现一个自定义init来根据初始化的金额设置一个状态变量。

我认为以下方法可以奏效:

struct AmountView : View {

    @Binding var amount: Double   
    @State var includeDecimal = false

    init(amount: Binding<Double>) {
        self.amount = amount
        self.includeDecimal = round(amount)-amount > 0
    }
}

然而,这给了我一个编译器错误如下:

不能将类型为“Binding”的值分配给类型为“Double”的值

我如何实现一个自定义init方法,在一个绑定结构?


当前回答

既然现在是2020年年中,让我们来回顾一下:

关于@绑定金额

_amount建议只在初始化时使用。永远不要这样分配自己。初始化时$amount = XXX 量。wrappedValue和amount。projectedValue并不经常使用,但是您可以看到这样的情况

@Environment(\.presentationMode) var presentationMode

self.presentationMode.wrappedValue.dismiss()

@binding的一个常见用例是:

@Binding var showFavorited: Bool

Toggle(isOn: $showFavorited) {
    Text("Change filter")
}

其他回答

你可以用静态函数或者自定义init来实现。

import SwiftUI
import PlaygroundSupport

struct AmountView: View {
    @Binding var amount: Double
    @State var includeDecimal: Bool
    var body: some View {
        Text("The amount is \(amount). \n Decimals  \(includeDecimal ? "included" : "excluded")")
    }
}

extension AmountView {
    static func create(amount: Binding<Double>) -> Self {
        AmountView(amount: amount, includeDecimal: round(amount.wrappedValue) - amount.wrappedValue > 0)
    }
    init(amount: Binding<Double>) {
        _amount = amount
        includeDecimal = round(amount.wrappedValue) - amount.wrappedValue > 0
    }
}
struct ContentView: View {
    @State var amount1 = 5.2
    @State var amount2 = 5.6
    var body: some View {
        AmountView.create(amount: $amount1)
        AmountView(amount: $amount2)
    }
}

PlaygroundPage.current.setLiveView(ContentView())

实际上你根本不需要自定义init,因为逻辑可以很容易地移动到. onappear,除非你需要显式地在外部设置初始状态。

struct AmountView: View {
    @Binding var amount: Double
    @State private var includeDecimal = true
    
    var body: some View {
        Text("The amount is \(amount, specifier: includeDecimal ? "%.3f" : "%.0f")")
        Toggle("Include decimal", isOn: $includeDecimal)
            .onAppear {
                includeDecimal = round(amount) - amount > 0
            }
    }
}

通过这种方式,您可以像文档建议的那样保持@State为私有并在内部初始化。

不要在视图中的点初始化视图的状态属性 实例化视图的层次结构,因为这可能会发生冲突 SwiftUI提供的存储管理。为了避免这种情况, 始终将状态声明为私有,并将其放置在最高视图中 需要访问值的视图层次结构

.

啊!你差一点就成功了。这就是你要做的。您漏掉了一个美元符号(beta 3)或下划线(beta 4),或者在amount属性前面的self,或者在amount参数后面的.value。所有这些选项都有效:

您将看到我在includeDecimal中删除了@State,检查最后的解释。

这是在使用属性(把self放在它前面):

struct AmountView : View {
    @Binding var amount: Double
    
    private var includeDecimal = false
    
    init(amount: Binding<Double>) {

        // self.$amount = amount // beta 3
        self._amount = amount // beta 4

        self.includeDecimal = round(self.amount)-self.amount > 0
    }
}

或者在后面使用.value(但不使用self,因为你使用的是传入的形参,而不是结构体的属性):

struct AmountView : View {
    @Binding var amount: Double
    
    private var includeDecimal = false
    
    init(amount: Binding<Double>) {
        // self.$amount = amount // beta 3
        self._amount = amount // beta 4

        self.includeDecimal = round(amount.value)-amount.value > 0
    }
}

这是相同的,但是我们为参数(withAmount)和属性(amount)使用了不同的名称,因此您可以清楚地看到您在使用它们。

struct AmountView : View {
    @Binding var amount: Double
    
    private var includeDecimal = false
    
    init(withAmount: Binding<Double>) {
        // self.$amount = withAmount // beta 3
        self._amount = withAmount // beta 4

        self.includeDecimal = round(self.amount)-self.amount > 0
    }
}
struct AmountView : View {
    @Binding var amount: Double
    
    private var includeDecimal = false
    
    init(withAmount: Binding<Double>) {
        // self.$amount = withAmount // beta 3
        self._amount = withAmount // beta 4

        self.includeDecimal = round(withAmount.value)-withAmount.value > 0
    }
}

注意,由于属性包装器(@Binding)创建了使.value变得不必要的访问器,因此.value对于属性来说不是必需的。然而,对于参数,没有这样的事情,您必须显式地做它。如果你想了解更多关于属性包装器的知识,请查看WWDC会议415 - Modern Swift API Design并跳转到23:12。

As you discovered, modifying the @State variable from the initilizer will throw the following error: Thread 1: Fatal error: Accessing State outside View.body. To avoid it, you should either remove the @State. Which makes sense because includeDecimal is not a source of truth. Its value is derived from amount. By removing @State, however, includeDecimal will not update if amount changes. To achieve that, the best option, is to define your includeDecimal as a computed property, so that its value is derived from the source of truth (amount). This way, whenever the amount changes, your includeDecimal does too. If your view depends on includeDecimal, it should update when it changes:

struct AmountView : View {
    @Binding var amount: Double
    
    private var includeDecimal: Bool {
        return round(amount)-amount > 0
    }
    
    init(withAmount: Binding<Double>) {
        self.$amount = withAmount
    }

    var body: some View { ... }
}

正如rob mayoff所指出的,你也可以使用$$varName (beta 3),或_varName (beta4)来初始化一个状态变量:

// Beta 3:
$$includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)

// Beta 4:
_includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)

你说(在评论中)“我需要能够改变includeDecimal”。改变includeDecimal意味着什么?显然,您希望根据amount(初始化时)是否为整数来初始化它。好的。如果includeDecimal为假,然后你把它改成真,会发生什么?你是否会迫使数量变成非整数?

总之,你不能在init中修改includeDecimal。但是你可以在init中初始化它,像这样:

struct ContentView : View {
    @Binding var amount: Double

    init(amount: Binding<Double>) {
        $amount = amount
        $$includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
    }

    @State private var includeDecimal: Bool

(注意,在某些时候$$includeDecimal语法将被更改为_includeDecimal。)

您应该使用下划线来访问属性包装器本身的合成存储。

在你的情况下:

init(amount: Binding<Double>) {
    _amount = amount
    includeDecimal = round(amount)-amount > 0
}

以下是苹果文件中的一段话:

编译器通过在包装属性的名称前加上下划线(_)来合成包装器类型实例的存储——例如,someProperty的包装器存储为_someProperty。包装器的合成存储具有私有的访问控制级别。

链接:https://docs.swift.org/swift-book/ReferenceManual/Attributes.html -> propertyWrapper部分

既然现在是2020年年中,让我们来回顾一下:

关于@绑定金额

_amount建议只在初始化时使用。永远不要这样分配自己。初始化时$amount = XXX 量。wrappedValue和amount。projectedValue并不经常使用,但是您可以看到这样的情况

@Environment(\.presentationMode) var presentationMode

self.presentationMode.wrappedValue.dismiss()

@binding的一个常见用例是:

@Binding var showFavorited: Bool

Toggle(isOn: $showFavorited) {
    Text("Change filter")
}