SwiftUI: Jak zaimplementować niestandardowy init ze zmiennymi @Binding

103

Pracuję na ekranie wprowadzania pieniędzy i muszę zaimplementować niestandardową initkonfigurację zmiennej stanu na podstawie zainicjowanej kwoty.

Myślałem, że to zadziała, ale otrzymuję błąd kompilatora:

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
źródło

Odpowiedzi:

166

Argh! Byłeś tak blisko. Tak to się robi. Pominięto znak dolara (beta 3) lub podkreślenie (beta 4) i albo self przed właściwością amount, albo .value za parametrem amount. Wszystkie te opcje działają:

Zobaczysz, że usunąłem @Statein includeDecimal, sprawdź wyjaśnienie na końcu.

To jest użycie własności (wstaw 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
    }
}

lub używając .value after (ale bez self, ponieważ używasz przekazanego parametru, a nie właściwości struktury):

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

To jest to samo, ale używamy różnych nazw dla parametru (withAmount) i właściwości (amount), więc wyraźnie widzisz, kiedy używasz każdego z nich.

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

Należy zauważyć, że wartość .value nie jest konieczna w przypadku właściwości, dzięki opakowaniu właściwości (@Binding), które tworzy metody dostępu, które sprawiają, że wartość .value jest niepotrzebna. Jednak z parametrem czegoś takiego nie ma i trzeba to zrobić wprost. Jeśli chcesz dowiedzieć się więcej o opakowaniach właściwości, zajrzyj do sesji WWDC 415 - Modern Swift API Design i przejdź do 23:12.

Jak odkryłeś, modyfikacja zmiennej @State w inicjatorze spowoduje zgłoszenie następującego błędu: Wątek 1: Błąd krytyczny: Dostęp do State poza View.body . Aby tego uniknąć, należy usunąć @State. Ma to sens, ponieważ includeDecimal nie jest źródłem prawdy. Jego wartość wynika z kwoty. Usunięcie @State includeDecimalnie spowoduje jednak aktualizacji w przypadku zmiany kwoty. Aby to osiągnąć, najlepszą opcją jest zdefiniowanie parametru includeDecimal jako obliczonej właściwości, tak aby jego wartość pochodziła ze źródła prawdy (kwoty). W ten sposób za każdym razem, gdy zmienia się kwota, zmienia się również parametr includeDecimal. Jeśli Twój widok zależy od includeDecimal, powinien zostać zaktualizowany po zmianie:

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

Jak wskazał rob mayoff , możesz również użyć $$varName(beta 3) lub _varName(beta4), aby zainicjować zmienną stanu:

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

// Beta 4:
_includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
kontiki
źródło
Dzięki! To bardzo pomogło! Dostaję błąd wykonania na self.includeDecimal = round(self.amount)-self.amount > 0odThread 1: Fatal error: Accessing State<Bool> outside View.body
keegan3d
Cóż, to ma sens. @Statezmienne powinny stanowić źródło prawdy. Ale w twoim przypadku powielasz tę prawdę, ponieważ wartość includeDecimal można wyprowadzić z twojego rzeczywistego źródła prawdy, którym jest kwota. Masz dwie możliwości: 1. Uczyniasz zmienną includeDecimal prywatną zmienną (bez @State) lub jeszcze lepiej amount. W ten sposób również zmienia się kwota includeDecimal. Powinieneś to zadeklarować w ten sposób: private var includeDecimal: Bool { return round(amount)-amount > 0 }i usunąćself.includeDecimal = ...
kontiki
Hmm, muszę mieć możliwość zmiany, includeDecimalwięc potrzebuję tego jako zmiennej @State w widoku. Naprawdę chcę zainicjować go wartością początkową
keegan3d
1
@ Let's_Create Obejrzałem je w całości tylko raz, ale dzięki Bogu za przycisk naprzód ;-)
kontiki
1
Naprawdę miłe wyjaśnienie, dzięki. Myślę, że teraz .valuezostał zastąpiony przez .wrappedValue, byłoby miło zaktualizować odpowiedź i usunąć opcje beta.
user1046037
11

Powiedziałeś (w komentarzu) „Muszę być w stanie się zmienić includeDecimal”. Co to znaczy zmienić includeDecimal? Najwyraźniej chcesz go zainicjować na podstawie tego, czy amount(w czasie inicjalizacji) jest liczbą całkowitą. W porządku. Więc co się stanie, jeśli includeDecimaltak, falsea później zmienisz to na true? Czy zamierzasz w jakiś sposób wymusić amountbycie niecałkowitym?

W każdym razie nie możesz modyfikować includeDecimalw init. Ale możesz go zainicjować w initnastępujący sposób:

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

(Należy pamiętać, że w pewnym momencie$$includeDecimal składnia zostanie zmieniony _includeDecimal).

rob mayoff
źródło
Och, wspaniale, podwójne $$ było tym, czego potrzebowałem w tej części!
keegan3d
3

Skoro jest połowa 2020 roku, podsumujmy:

Co się tyczy @Binding amount

  1. _amountzaleca się używać tylko podczas inicjalizacji. I nigdy nie przypisuj w ten sposób self.$amount = xxxpodczas inicjalizacji

  2. amount.wrappedValuei amount.projectedValuenie są często używane, ale możesz zobaczyć przypadki takie jak

@Environment(\.presentationMode) var presentationMode

self.presentationMode.wrappedValue.dismiss()
  1. Typowym przypadkiem użycia @binding jest:
@Binding var showFavorited: Bool

Toggle(isOn: $showFavorited) {
    Text("Change filter")
}
LiangWang
źródło