How to execute non-view code inside a SwiftUI view
Asked Answered
P

5

12

I have been struggling with this over and over again, so I think I'm missing something. I need to do math, make a setting, assign a value or any of a host of simple operations in reaction to some user action, such as the example shown here, and SwiftUI is wanting a View where I don't need a view. There's got to be a way around the ViewBuilder's rules. I kind of worked around this by creating an unnecessary view and executing the code I need inside the View's init(), but that seems terribly awkward.

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")
            }
        }
    }
}
Priestcraft answered 25/7, 2020 at 15:46 Comment(0)
S
0

I think the best way to do this now is using onChange(of:perform:) modifier, for iOS14.0+

These simple executions (like do math, assign a value) are nothing but actions in swift terminology, which should be performed after touching any UI element because swiftUI is declarative. In your case, you can use this with any View type. Other similar options are .onAppear() or .onDisappear() (self-explanatory).

Surprisingly, apple documentation for these are actually good and elaborate.

Link - https://developer.apple.com/documentation/swiftui/view/onchange(of:perform:)

Scleritis answered 12/9, 2023 at 19:58 Comment(0)
T
18

Way 1 (best):

struct ExecuteCode : View {
    init( _ codeToExec: () -> () ) {
        codeToExec()
    }
    
    var body: some View {
        EmptyView()
    }
}

usage:

HStack {
    ExecuteCode { 
        print("SomeView1 was re-drawn!")
        print("second print")
    }

    SomeView1()
}

Way 2:

( my first way is better - you're able to write only simple code here )

HStack {
    // `let _ =` works inside of View!
    let _ = print("SomeView1 was re-drawn!") 

    SomeView1()
}

Way 3:

( +- the same situation as in first way. Good enough solution. )

HStack {
    let _ = { // look here. "let _ =" is required
        print("SomeView1 was re-drawn!")
        print("second print")
    }() // look here. "()" is also required
    
    SomeView1()
}

But other dev's possibly can not understand this code (especially if it is large), so first one is a little bit better.

Taintless answered 26/1, 2021 at 16:9 Comment(3)
Works Perfectly in SwiftUI with synchronous executionMainsail
These are all fine if all you want to do is print something to the console. If you want to actually perform side effects like in the original question, none of these is appropriate. The correct approach if you need to perform side effects is to use a custom Binding or use the onChange modifier as shown in some of the other answers.Gelya
@robmayoff ofc, you're right :) but still - this is useful instrument :)Taintless
L
4

Views are actually so-called Function Builders, and the contents of the view body are used as arguments to to the buildBlock function, as mentioned by @Asperi.

An alternative solution if you must run code inside this context is using a closure that returns the desired view:

VStack {
    // ... some views ...
    { () -> Text in
      // ... any code ...
      return Text("some view") }()
    // ... some views ...
}
Lake answered 18/10, 2021 at 21:53 Comment(0)
C
1

In SwiftUI 2.0, there's a new ViewModifier onChange(of:perform:), that allows you to react to changes in values.

But you can create something similar to that with a neat trick (I forgot where I saw it, so unfortunately I can't leave proper attribution), by extending a Binding with onChange method:

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

You can use it like so:

Toggle(isOn: $showStuff.onChange(perform: { (new, old) in
  if new {
     UserDefaults.standard.set(true, forKey: "Something")
  }
}))
Compass answered 25/7, 2020 at 17:38 Comment(0)
R
0

You cannot do what you try to do, because actually every view block inside body is a ViewBuidler.buildBlock function arguments. Ie. you are in function arguments space. I hope you would not expect that expression like

foo(Toggle(), if showStuff { ... } )

would work (assuming foo is func foo(args: View...). But this is what you try to do in body.

So expressions in SwiftUI have to be out of ViewBuilder block (with some exceptions which ViewBuilder itself supports for views).

Here is a solution for your case:

SwiftUI 2.0

struct ContentView: View {
    @AppStorage("Something") var showStuff = false

    var body: some View {
        VStack {
            Toggle(isOn: $showStuff) {
                Text("Label")
            }
        }
    }
}

SwiftUI 1.0

Find in already solved SwiftUI toggle switches

Note: View.body (excluding some action modifiers) is equivalent of UIView.draw(_ rect:)... you don't store UserDefaults in draw(_ rect:), do you?

Representational answered 25/7, 2020 at 16:8 Comment(0)
S
0

I think the best way to do this now is using onChange(of:perform:) modifier, for iOS14.0+

These simple executions (like do math, assign a value) are nothing but actions in swift terminology, which should be performed after touching any UI element because swiftUI is declarative. In your case, you can use this with any View type. Other similar options are .onAppear() or .onDisappear() (self-explanatory).

Surprisingly, apple documentation for these are actually good and elaborate.

Link - https://developer.apple.com/documentation/swiftui/view/onchange(of:perform:)

Scleritis answered 12/9, 2023 at 19:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.