282 votes

SwiftUI : Comment implémenter un init personnalisé avec des variables @Binding

Je travaille sur un écran d'entrée d'argent et j'ai besoin d'implémenter une fonction personnalisée. init pour définir une variable d'état basée sur le montant initialisé.

Je pensais que cela fonctionnerait, mais j'obtiens une erreur de compilateur 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
    }
    ...
}

439voto

kontiki Points 633

Argh ! Tu étais si proche. C'est comme ça que ça se passe. Il vous manque le signe dollar (beta 3) ou le trait de soulignement (beta 4), et soit self devant votre propriété amount, soit .value après le paramètre amount. Toutes ces options fonctionnent :

Vous verrez que j'ai enlevé le @State en includeDecimal Vérifiez l'explication à la fin.

C'est l'utilisation de la propriété (mettre self devant) :

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 en utilisant .value après (mais sans self, car vous utilisez le paramètre passé, pas la propriété de la structure) :

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

C'est la même chose, mais nous utilisons des noms différents pour le paramètre (withAmount) et la propriété (amount), de sorte que vous voyez clairement quand vous utilisez chacun d'eux.

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

Notez que .value n'est pas nécessaire avec la propriété, grâce à l'enveloppe de la propriété (@Binding), qui crée les accesseurs qui rendent le .value inutile. Cependant, avec le paramètre, il n'y a pas de telle chose et vous devez le faire explicitement. Si vous souhaitez en savoir plus sur les wrappers de propriétés, consultez la page Session 415 de la WWDC - Conception d'API Swift modernes et sauter à 23:12.

Comme vous l'avez découvert, la modification de la variable @State à partir de l'initialisateur entraînera l'erreur suivante : Thread 1 : Erreur fatale : Accès à l'état en dehors de View.body . Pour l'éviter, vous devez soit supprimer l'@State. Ce qui est logique car includeDecimal n'est pas une source de vérité. Sa valeur est dérivée du montant. En supprimant @State, cependant, includeDecimal ne sera pas mis à jour si le montant change. Pour cela, la meilleure option est de définir votre includeDecimal comme une propriété calculée, de sorte que sa valeur soit dérivée de la source de vérité (le montant). De cette façon, lorsque le montant change, votre includeDecimal change aussi. Si votre vue dépend de includeDecimal, elle doit être mise à jour lorsqu'elle change :

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

Comme l'indique rob mayoff vous pouvez également utiliser $$varName (bêta 3), ou _varName (beta4) pour initialiser une variable d'état :

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

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

34voto

Simon Wang Points 1

Vous devez utiliser l'underscore pour accéder au stockage synthétisé pour le wrapper de propriété lui-même.

Dans votre cas :

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

Voici la citation du document d'Apple :

Le compilateur synthétise le stockage pour l'instance du type wrapper en préfixant le nom de la propriété wrappée par un trait de soulignement (_) - par exemple, le wrapper pour someProperty est stocké sous la forme _someProperty. Le stockage synthétisé pour le wrapper a un niveau de contrôle d'accès de private.

Lien : https://docs.swift.org/swift-book/ReferenceManual/Attributes.html -> section propertyWrapper

14voto

rob mayoff Points 124153

Vous avez dit (dans un commentaire) "Je dois être capable de changer includeDecimal ". Qu'est-ce que cela signifie de changer includeDecimal ? Vous voulez apparemment l'initialiser en fonction du fait que amount (au moment de l'initialisation) est un nombre entier. Ok. Alors que se passe-t-il si includeDecimal es false et plus tard, vous le changez en true ? Allez-vous en quelque sorte forcer amount pour être alors non entier ?

De toute façon, vous ne pouvez pas modifier includeDecimal en init . Mais vous pouvez l'initialiser dans init comme ceci :

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

(Notez que à un moment donné le site $$includeDecimal La syntaxe sera modifiée en _includeDecimal .)

8voto

Jacky Points 442

Puisque nous sommes au milieu de l'année 2020, récapitulons :

Quant à @Binding amount

  1. _amount ne doit être utilisé que pendant l'initialisation. Et ne jamais assigner de cette façon self.$amount = xxx pendant l'initialisation

  2. amount.wrappedValue y amount.projectedValue ne sont pas fréquemment utilisés, mais vous pouvez voir des cas comme

    @Environment(.presentationMode) var presentationMode

    self.presentationMode.wrappedValue.dismiss()

  3. Un cas courant d'utilisation de @binding est :

    @Binding var showFavorited: Bool

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

4voto

bhagyash ingale Points 23

La réponse acceptée est un moyen, mais il en existe un autre.

struct AmountView : View {
var amount: Binding<Double>

init(withAmount: Binding<Double>) {
    self.amount = withAmount
}

var body: some View { ... }
}

Vous supprimez le @Binding et vous en faites une variable de type Binding. La partie délicate est la mise à jour de cette variable. Vous devez mettre à jour sa propriété appelée valeur enveloppée. par exemple

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X