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

  1. _amountрекомендуется использовать только во время инициализации. И никогда не назначайте такself.$amount = xxx во время инициализации

  2. amount.wrappedValue а также amount.projectedValue не часто используются, но вы можете увидеть такие случаи, как

@Environment(\.presentationMode) var presentationMode

self.presentationMode.wrappedValue.dismiss()
  1. Типичный вариант использования @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()
        }
    }
}

Привязка:

Родительское представление объявляет свойство для хранения 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()
        }
    }
}

Другие вопросы по тегам