Place for calculations in SwiftUI

I understand, but at this point, I don‘t necessarily have a specific trigger in mind, that should start the calculation as I‘m just trying my very first steps with the language (after browsing through the Tutorial on swift.org and getting an overview of the basic concepts mentioned on hackwithswift.com) to understand bit by bit how it works.

Here, I initially just wanted to start to create a text field showing an editable date value. So I looked up how to store a custom date (declare a variable), how to convert it to a string (make a calculation) and show it in a GUI (set the Text object) and I already stumbled into this seemingly hard to solve problem.

For the sake of this thread I just simplified this algorithm to an integer to be incremented and shown as it shows the exact same problem.

At a later point, this view would be editable as said and part of list with many such date views, where you would then click on a specific date to make it editable and also show some cancel/confirm buttons, which trigger the corresponding action before showing the non-editable text again and of course there is some DatePicker, which can be used preferable instead of a Text field, …

But at this point, I basically want to learn WHY things work or don‘t work and what to do instead and why to make it work, so I‘m not doing this for the result or to ONLY get a blueprint I can just use without even knowing why it works.

So with the problem that occured, I would now like to know, why I can‘t change the variable i without giving it a modifier like State or mutating as normally a variable can by definition be changed? And I‘d like to know, why i = i + 1 before Text(i) doesn‘t work, but var j = i + 1 before Text(j) does work? And I‘d like to know, how I can make version with i = i + 1 work as adding @State or mutating to the declaration is not enough to get rid of all error messages?

Thanks, and do you actually have an idea, what I would need to do in my script to make it work while not using an additional variable (so basically keeping the i = i + 1 line)?

just add "return":

var body: some View {
    i = i + 1 // or simpler i += 1
    return Text("\(i)")
}

Note, if it was "let/var" you won't have to:

var body: some View {
    let x = i
    Text("\(x)") // fine
}

SwiftUI is effectively a slightly different "language of its own" with its own set of features and constraints. The difference is not massive though.

This gives a "runtime issue":

Modifying state during view update, this will cause undefined behavior.

1 Like

Not in the view body itself. Doing so will give you a runtime issue, "Modifying state during view update, this will cause undefined behavior."

Some Views, like Button, let you provide an "action" closure, and that is a spot that you can safely add one to your State.

Other places would be inside of a .task modifier, or a .onAppear modifier, or, I think, any scope that is an ordinary Swift closure, as opposed to a ViewBuilder.

Wow, thanks a lot, "return" solved the problem.

Could you tell me, why the "i = i + 1" version works only with "return", but the "j = i + 1" version (or let x = i in your example) works without "return"?

From my understanding an (enclosed) function only returns something, when a return value has been declared, so in theory there should always be a return value with a View here, although in practice, for some reason I don't understand, it's sometimes obviously not necessary?

So why is an explicit return value with a View in this case under certain conditions not necessary? Why do I need a return value for i = i + 1; Text(i), but no return value for j = i + 1; Text(j)?

Thanks so much for your clarification.

Thanks for your explanations, could you recommend good tutorials, that would also include these types of explanations that it may not be possible to modify variables in certain parts of View structs, and where it may be possible instead and why?

This is not good practice. View initializers and the body property are executed at arbitrary times and, as a consequence, should not contain side effects like modifying variables. As @Syre notes, this code results in an issue if run.


Here's some code that updates a variable when the view is shown:

import SwiftUI

struct TestView: View {
    @State private var i = 1
    
    var body: some View {
        Text("\(i)")
            .onAppear(perform: {
                i += 1
            })
    }
}

And here's another way to do the same thing:

struct TestView: View {
    @StateObject var state = TestState()
    
    var body: some View {
        Text("\(state.i)")
    }
}

final class TestState: ObservableObject {
    @Published var i = 1
    
    init() {
        i += 1
    }
}

Either one will work, but which one is better depends on your use case. Like many other people in this thread, I recommend that you read/view a tutorial on how SwiftUI works — its declarative style is much different from the imperative style you're used to. Here are three good options for learning SwiftUI:

  1. Watch the WWDC talk "Introduction to SwiftUI" — a quick introduction to SwiftUI and how to think about things within SwiftUI. If you're still confused about Swift's data flow afterwards, "Data Essentials in SwiftUI" is a good next step.
  2. Follow Apple's SwiftUI tutorials — more detailed examples on how to use SwiftUI.
  3. Follow 100 Days of SwiftUI — if the previous two tutorials are too confusing for you, then this is a good option. It's made for beginners and teaches both Swift (the language) and SwiftUI (the framework). If you're impatient, you can skip ahead — the "100 days" part is just a suggestion.

You'll probably learn why this code is necessary before the 100 days are up — @State is taught on day 16.

4 Likes

Yep, I was thinking about something else:

var body: some View {
    print("xxx")
    return Text("xxx")
}

I'd double the suggestion to move that increment logic into a model. There you can use "normal" swift.

1 Like

Thanks a lot for the detailed explanations and the recommendations. I‘ll take a look into them.

1 Like

Thanks, and do you have a quick explanation why the return statement can sometimes be omitted?

Not a quick one :slight_smile: Read about "Result Builders" here and here.

Note there's a similar looking (but different) place in swift, that allows omitting "return" keyword in a single statement functions & closures:

func foo() -> Bool {
    true
}

but not here:

func foo() -> Bool {
    print("Hello")
    true // error. needs to be "return true"
}
1 Like

It's actually the same thing, the computed body property normally being a single statement closure that doesn't require a return. That's why putting return in worked (well, except for the runtime error, which is a different issue altogether); since the body no longer consisted of a single statement, the return statement was necessary.

Is it?

var body: some View {
    let x = 1
    Text("x") // ✅
}

func foo() -> Bool {
    let x = 1
    true // ❌ error
}
2 Likes