SwiftUI: как реализовать пользовательский init с переменными @Binding
Я работаю над экраном ввода денег и мне нужно реализовать 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
}
...
}
7 ответов
Argh! Вы были так близко. Вот как ты это делаешь. Вы пропустили знак доллара (бета 3) или знак подчеркивания (бета 4) и либо самостоятельно перед вашим свойством количества, либо.value после параметра количества. Все эти варианты работают:
Вы увидите, что я удалил @State
в includeDecimal проверьте объяснение в конце.
Это использует свойство (поместите себя перед ним):
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 after (но без self, потому что вы используете переданный параметр, а не свойство struct):
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 415. Современный дизайн API Swift и перейдите к 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 { ... }
}
Как указано Роб Майофф, вы также можете использовать $$varName
(бета 3) или _varName
(бета4) для инициализации переменной состояния:
// Beta 3:
$$includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
// Beta 4:
_includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
Вы должны использовать подчеркивание для доступа к синтезированному хранилищу для самой оболочки свойств.
В твоем случае:
init(amount: Binding<Double>) {
_amount = amount
includeDecimal = round(amount)-amount > 0
}
Вот цитата из документа Apple:
Компилятор синтезирует хранилище для экземпляра типа оболочки, добавляя к имени обернутого свойства знак подчеркивания (_) - например, оболочка для someProperty сохраняется как _someProperty. Синтезированное хранилище для оболочки имеет уровень управления доступом private.
Ссылка: https://docs.swift.org/swift-book/ReferenceManual/Attributes.html -> раздел propertyWrapper
Вы сказали (в комментарии): "Мне нужно быть в состоянии изменить includeDecimal
". Что значит изменить includeDecimal
? Вы, очевидно, хотите инициализировать его в зависимости от того, amount
(во время инициализации) является целым числом. Хорошо. Так что же произойдет, если includeDecimal
является false
а затем вы измените его на true
? Собираетесь ли вы как-то силой amount
чтобы потом быть нецелым?
Во всяком случае, вы не можете изменить includeDecimal
в init
, Но вы можете инициализировать его в 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
.)
Поскольку сейчас середина 2020 года, подведем итоги:
Относительно @Binding amount
_amount
рекомендуется использовать только во время инициализации. И никогда не назначайте такself.$amount = xxx
во время инициализацииamount.wrappedValue
а такжеamount.projectedValue
не часто используются, но вы можете увидеть такие случаи, как
@Environment(\.presentationMode) var presentationMode
self.presentationMode.wrappedValue.dismiss()
- Типичный вариант использования @binding:
@Binding var showFavorited: Bool
Toggle(isOn: $showFavorited) {
Text("Change filter")
}
Вы можете добиться этого либо с помощью статической функции, либо с помощью пользовательской инициализации.
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, так как логику можно легко перенести в
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. Чтобы избежать этого, всегда объявляйте состояние как приватное и размещайте его в самом верхнем представлении в иерархии представлений, которому требуется доступ к значению.
.
Принятый ответ в одну сторону, но есть и другой способ
struct AmountView : View {
var amount: Binding<Double>
init(withAmount: Binding<Double>) {
self.amount = withAmount
}
var body: some View { ... }
}
Вы удаляете @Binding и делаете его переменной типа Binding. Хитрость заключается в обновлении этой переменной. Вам нужно обновить его свойство, называемое обернутым значением. например
amount.wrappedValue = 1.5 // or
amount.wrappedValue.toggle()
Состояние:
Для управления хранением любого имущества, объявленного вами как состояние . Когда значение состояния изменяется, представление делает его внешний вид недействительным и повторно вычисляет тело, и вам следует обращаться к свойству состояния только изнутри тела представления или из вызываемых методов.
Примечание . Чтобы передать свойство состояния другому представлению в иерархии представлений, используйте имя переменной с оператором префикса $ .
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()
}
}
}
Привязка:
Родительское представление объявляет свойство для хранения
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()
}
}
}