SwiftUI:如何使用@Binding 变量实现自定义初始化

SwiftUI: How to implement a custom init with @Binding variables

我正在处理货币输入屏幕,需要实现自定义 init 以根据初始化金额设置状态变量。

我认为这可行,但我收到编译器错误:

Cannot assign value of type 'Binding<Double>' to type 'Double'

struct AmountView : View {
    @Binding var amount: Double

    @State var includeDecimal = false

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

啊!你是如此接近。这就是你如何做的。您错过了一个美元符号 (beta 3) 或下划线 (beta 4),或者您的金额前面的 self 属性,或者金额参数后面的 .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(但没有自我,因为您使用的是传递的参数,而不是结构的 属性):

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)和属性(金额)使用了不同的名称,所以你在使用的时候可以清楚的看到每个.

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
    }
}

请注意,属性 不需要 .value,这要归功于 属性 包装器 (@Binding),它创建了使.value 不必要。但是,对于参数,没有这样的事情,你必须明确地做。如果您想了解更多关于 属性 包装器的信息,请查看 WWDC session 415 - Modern Swift API Design 并跳转到 23:12.

如您所见,从启动器修改@State 变量将抛出以下错误:线程 1:致命错误:访问外部状态 View.body。为避免这种情况,您应该删除 @State。这是有道理的,因为 includeDecimal 不是事实来源。它的价值来自数量。但是,通过删除@State,includeDecimal 将不会在金额更改时更新。要实现这一点,最好的选择是将 includeDecimal 定义为计算的 属性,以便其值来自真实来源(数量)。这样,每当金额发生变化时,您的 includeDecimal 也会发生变化。如果您的视图依赖于 includeDecimal,它应该在更改时更新:

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(测试版 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 (在初始化时)是否为整数来初始化它。好的。那么,如果 includeDecimalfalse,然后您将其更改为 true,会发生什么?您是否打算以某种方式强制 amount 成为 non-integer?

无论如何,您不能在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

(请注意,at some point $$includeDecimal 语法将更改为 _includeDecimal。)

2020 年年中,让我们回顾一下:

至于@Binding amount

  1. _amount只推荐在初始化时使用。并且在初始化期间永远不要这样分配 self.$amount = xxx

  2. amount.wrappedValueamount.projectedValue 不经常使用,但是你可以看到像

  3. 这样的情况
@Environment(\.presentationMode) var presentationMode

self.presentationMode.wrappedValue.dismiss()
  1. @binding 的一个常见用例是:
@Binding var showFavorited: Bool

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

状态:

管理您声明为状态的任何属性的存储。当 state 值改变时,视图使其外观无效并重新计算主体,您应该只从内部访问 state 属性视图的主体,或来自调用的方法。

注意:要将状态属性传递给视图层次结构中的另一个视图,请使用带有的变量名$ 前缀运算符.

struct ContentView: View {
    @State private var isSmile : Bool = false
    var body: some View {
        VStack{
            Text(isSmile ? "" : "").font(.custom("Arial", size: 120))
            Toggle(isOn: $isSmile, label: {
                    Text("State")
                }).fixedSize()
        }
    }
}

绑定:

父视图声明一个 属性 来保存 isSmile state,使用 State 属性 包装器以表明此 属性 是不同视图的值来源。

struct ContentView: View {
    @State private var isSmile : Bool = false
    var body: some View {
        VStack{
            Text(isSmile ? "" : "").font(.custom("Arial", size: 120))
            SwitchView(isSmile: $isSmile)
        }
    }
}

使用 绑定 在存储数据的 属性 和显示和更改数据的视图之间创建双向连接。

struct SwitchView: View {
    @Binding var isSmile : Bool
    var body: some View {
        VStack{
                Toggle(isOn: $isSmile, label: {
                    Text("Binding")
                }).fixedSize()
        }
    }
}

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

你的情况:

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

这里引用自 Apple 文档:

The compiler synthesizes storage for the instance of the wrapper type by prefixing the name of the wrapped property with an underscore (_)—for example, the wrapper for someProperty is stored as _someProperty. The synthesized storage for the wrapper has an access control level of private.

Link:https://docs.swift.org/swift-book/ReferenceManual/Attributes.html -> 属性包装部分

接受的答案是一种方式,但还有另一种方式

struct AmountView : View {
var amount: Binding<Double>
  
init(withAmount: Binding<Double>) {
    self.amount = withAmount
}

var body: some View { ... }
}

您删除了@Binding 并将其设为 Binding 类型的变量 棘手的部分是更新这个 var。您需要更新它的 属性 称为包装值。例如

 amount.wrappedValue = 1.5 // or
 amount.wrappedValue.toggle()
   

您可以使用静态函数或自定义初始化来实现。

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())

实际上你根本不需要自定义初始化,因为逻辑可以很容易地移动到 .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 保持私有并在内部初始化为 documentation suggests

Don’t initialize a state property of a view at the point in the view hierarchy where you instantiate the view, because this can conflict with the storage management that SwiftUI provides. To avoid this, always declare state as private, and place it in the highest view in the view hierarchy that needs access to the value

.