Как выполнить код без просмотра внутри представления SwiftUI
Я боролся с этим снова и снова, поэтому я думаю, что что-то упускаю. Мне нужно выполнить математику, задать настройку, присвоить значение или выполнить любую из множества простых операций в ответ на какое-либо действие пользователя, например, в примере, показанном здесь, а SwiftUI хочет получить представление, в котором мне не нужно представление. Должен быть способ обойти правила ViewBuilder. Я вроде как работал над этим, создавая ненужное представление и выполняя нужный мне код внутри init() представления, но это кажется ужасно неудобным.
import SwiftUI
struct ContentView: View
{
@State var showStuff = false
var body: some View
{
VStack
{
Toggle(isOn: $showStuff)
{
Text("Label")
}
if showStuff
{
UserDefaults.standard.set(true, forKey: "Something")
}
}
}
}
5 ответов
В SwiftUI 2.0 появился новый ViewModifier onChange(of:perform:)
, что позволяет вам реагировать на изменение значений.
Но вы можете создать нечто подобное с помощью хитрого трюка (я забыл, где это видел, поэтому, к сожалению, я не могу оставить правильную атрибуцию), расширив Binding
с участием onChange
метод:
extension Binding {
func onChange(perform action: @escaping (Value, Value) -> Void) -> Self {
.init(
get: { self.wrappedValue },
set: { newValue in
let oldValue = self.wrappedValue
DispatchQueue.main.async { action(newValue, oldValue) }
self.wrappedValue = newValue
})
}
}
Вы можете использовать это так:
Toggle(isOn: $showStuff.onChange(perform: { (new, old) in
if new {
UserDefaults.standard.set(true, forKey: "Something")
}
}))
struct ExecuteCode : View {
init( _ codeToExec: () -> () ) {
codeToExec()
}
var body: some View {
return EmptyView()
}
}
использование:
HStack{
SomeView1()
Spacer()
ExecuteCode { print("SomeView1 was re-drawn!") }
}
Представления на самом деле являются так называемыми построителями функций, и содержимое тела представления используется в качестве аргументов для
buildBlock
функция, как упоминалось @Asperi.
Альтернативное решение, если вы должны запускать код внутри этого контекста, использует закрытие, которое возвращает желаемое представление:
VStack {
// ... some views ...
{ () -> Text in
// ... any code ...
return Text("some view") }()
// ... some views ...
}
Вы не можете делать то, что пытаетесь сделать, потому что фактически каждый блок просмотра внутри body
это ViewBuidler.buildBlock
аргументы функции. Т.е. вы находитесь в пространстве аргументов функции. Надеюсь, вы не ожидали такого выражения вроде
foo(Toggle(), if showStuff { ... } )
будет работать (при условии foo
является func foo(args: View...)
. Но это то, что вы пытаетесь сделать вbody
.
Таким образом, выражения в SwiftUI должны находиться вне блока ViewBuilder (за некоторыми исключениями, которые сам ViewBuilder поддерживает для представлений).
Вот решение для вашего случая:
SwiftUI 2.0
struct ContentView: View {
@AppStorage("Something") var showStuff = false
var body: some View {
VStack {
Toggle(isOn: $showStuff) {
Text("Label")
}
}
}
}
SwiftUI 1.0
Найдите в уже решенных тумблерах SwiftUI
Заметка: View.body
(за исключением некоторых модификаторов действий) эквивалентно UIView.draw(_ rect:)
... вы не храните UserDefaults в draw(_ rect:), не так ли?
Я думаю, что лучший способ сделать это сейчас — использовать модификатор onChange(of:perform:) для iOS14.0+.
Эти простые действия (например, выполнить математические операции, присвоить значение) — это не что иное, как действия в терминологии Swift, которые должны выполняться после прикосновения к любому элементу пользовательского интерфейса, поскольку SwiftUI является декларативным. В вашем случае вы можете использовать это с любым типом представления. Другими похожими параметрами являются .onAppear() или .onDisappear() (очевидно).
Удивительно, но документация Apple по ним на самом деле хорошая и подробная.
Ссылка — https://developer.apple.com/documentation/swiftui/view/onchange(of:perform:)