SwiftUI: como implementar um init personalizado com variáveis ​​@Binding

103

Estou trabalhando em uma tela de entrada de dinheiro e preciso implementar um padrão init para definir uma variável de estado com base no valor inicializado.

Achei que isso funcionaria, mas estou recebendo um erro do compilador de:

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
    }
    ...
}
keegan3d
fonte

Respostas:

165

Argh! Você estava tão perto. É assim que se faz. Você perdeu um cifrão (beta 3) ou sublinhado (beta 4), e mesmo na frente de sua propriedade de valor, ou .value após o parâmetro de valor. Todas essas opções funcionam:

Você verá que removi o @Statein includeDecimal, verifique a explicação no final.

Isso é usar a propriedade (coloque-se na frente dela):

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

ou usando .value after (mas sem self, porque você está usando o parâmetro passado, não a propriedade da estrutura):

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

É o mesmo, mas usamos nomes diferentes para o parâmetro (withAmount) e a propriedade (amount), para que você veja claramente quando está usando cada um.

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

Observe que .value não é necessário com a propriedade, graças ao wrapper de propriedade (@Binding), que cria os acessadores que tornam o .value desnecessário. Porém, com o parâmetro, não existe tal coisa e você tem que fazer isso explicitamente. Se você quiser saber mais sobre os wrappers de propriedade, verifique a sessão WWDC 415 - Modern Swift API Design e pule para 23:12.

Como você descobriu, modificar a variável @State do initilizer gerará o seguinte erro: Thread 1: Erro fatal: Accessing State outside View.body . Para evitá-lo, você deve remover o @State. O que faz sentido porque includeDecimal não é uma fonte de verdade. Seu valor é derivado da quantidade. Ao remover @State, no entanto, includeDecimalnão será atualizado se a quantidade mudar. Para isso, a melhor opção é definir seu includeDecimal como uma propriedade computada, de forma que seu valor seja derivado da fonte da verdade (quantidade). Dessa forma, sempre que o valor muda, seu includeDecimal também muda. Se sua visualização depende de includeDecimal, ela deve ser atualizada quando mudar:

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

Conforme indicado por rob mayoff , você também pode usar $$varName(beta 3) ou _varName(beta4) para inicializar uma variável de estado:

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

// Beta 4:
_includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
kontiki
fonte
Obrigado! Isso ajudou muito! Estou recebendo um erro de execução em self.includeDecimal = round(self.amount)-self.amount > 0deThread 1: Fatal error: Accessing State<Bool> outside View.body
keegan3d
Bem, meio que faz sentido. @Statevariáveis ​​devem representar uma fonte de verdade. Mas, no seu caso, você está duplicando essa verdade, porque o valor de includeDecimal pode ser derivado de sua fonte real de verdade, que é amount. Você tem duas opções: 1. Você torna includeDecimal uma var privada (sem @State), ou ainda melhor 2. Você torna uma propriedade computada que deriva seu valor de amount. Dessa forma, se a quantidade mudar, includeDecimaltambém muda . Você deve declará-lo assim: private var includeDecimal: Bool { return round(amount)-amount > 0 }e remover oself.includeDecimal = ...
kontiki
Hmm, eu preciso ser capaz de mudar, includeDecimalentão preciso disso como uma variável @State na visualização. Eu realmente quero inicializá-lo com um valor inicial
keegan3d
1
@ Let's_Create Eu os assisti totalmente apenas uma vez, mas graças a Deus pelo botão de avançar ;-)
kontiki
1
Explicação muito boa, obrigado. Acho que agora o .valuefoi substituído por .wrappedValue, seria bom atualizar a resposta e remover as opções beta.
user1046037
11

Você disse (em um comentário) “Eu preciso ser capaz de mudar includeDecimal”. O que significa mudar includeDecimal? Aparentemente, você deseja inicializá-lo com base em se amount(no momento da inicialização) é um número inteiro. OK. Então, o que acontece se includeDecimalfor falsee depois você muda para true? Você vai forçar de alguma formaamount a ser um não inteiro?

De qualquer forma, você não pode modificar includeDecimalem init. Mas você pode inicializá-lo init, assim:

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

(Observe que em algum momento a $$includeDecimalsintaxe será alterada para _includeDecimal.)

rob mayoff
fonte
Oh incrível, o dobro de $$ era o que eu precisava para esta parte!
keegan3d
3

Já que estamos em meados de 2020, vamos recapitular:

Quanto a @Binding amount

  1. _amountsó é recomendado para ser usado durante a inicialização. E nunca atribua desta forma self.$amount = xxxdurante a inicialização

  2. amount.wrappedValuee amount.projectedValuenão são usados ​​com frequência, mas você pode ver casos como

@Environment(\.presentationMode) var presentationMode

self.presentationMode.wrappedValue.dismiss()
  1. Um caso de uso comum de @binding é:
@Binding var showFavorited: Bool

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