Как выполнить код без просмотра внутри представления 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:)

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