Initializing State from another object

I’m trying to build a detail view screen where a user can edit the contents of an object and save it, but I’m struggling to figure out how best to initialize the view.

I’m passing in a “MailFilter” object (which I decoded from a web API) to this view (from a List item’s NavigationLink), and am creating state for each of the fields which need to be editable, such that I can use them with a SwiftUI form. I have a custom initializer to set these parameters (and make sure they aren’t nil—as they can be in the actual filter object).

Code Block
struct FilterView: View {
var filter: MailFilter
@State var from: String
@State var to: String
init(_ filter: MailFilter) {
self.filter = filter
from = filter.criteria.from ?? ""
to = filter.criteria.to ?? ""
}
var body: some View {
Form {
Section(header: Text("From")) {
TextField("From", text: $from)
}
Section(header: Text("To")) {
TextField("To", text: $to)
}
}
}
}

However, this approach doesn’t seem to work. I’m given errors that I’m trying to use self. before initializing the variables—in the initializer! Is there a different approach I should be taking to get my object’s data into this detail view?

Accepted Reply

hi,

instead of writing
Code Block
from = filter.criteria.from ?? ""
to = filter.criteria.to ?? ""

you should be writing
Code Block
_from = State(initialValue: filter.criteria.from ?? "")
_to = State(initialValue: filter.criteria.to ?? "")

this is a syntactic issue with @State: you need to initialize the property wrapper, not its value.

hope that helps,
DMG

Replies

hi,

instead of writing
Code Block
from = filter.criteria.from ?? ""
to = filter.criteria.to ?? ""

you should be writing
Code Block
_from = State(initialValue: filter.criteria.from ?? "")
_to = State(initialValue: filter.criteria.to ?? "")

this is a syntactic issue with @State: you need to initialize the property wrapper, not its value.

hope that helps,
DMG
Thank you!

I had seen adding the _ to my property names elsewhere briefly, but wasn't entirely sure how best to initialize a State<>. Makes sense that I would need to init the wrapper.
You should not initialise a @State member from within the initializer of a view, no matter how.

As discussed in Swift Forum, due to the internal implementation and due to how SwiftUI works, it seems initialising a @State is only safe in this form, outside any view initialiser:

Code Block
struct MyView: View {
@State var value = 0
...
}

Note, that MyView value may be constructed multiple times in a single scene. This is in contrast to how UIKit works, where the views will be created only once for a scene.

When you initialise a @State within the view's initialiser, it will do it at the very first creation of this view, but it has no effect on subsequent initialisations. The SwiftUI framework will restore the previously cached value and thus overwrite the value you set in the initialiser.

Furthermore, mutating a @State variable is safe only within the result builder - that is, when it will be called directly or indirectly from body of the view - for example in onAppear.

So, in your case, if you want the variables from and to to be mutated by the view and being initialised by a view's initialiser use a binding (Binding<T>) instead.

If you don't need to modify the variables, just use plain let constants which can be initialised in the view's initializer.

See also:
Swift Forum
Swift Forum

Code Block
import SwiftUI
struct MailFilterView: View {
    @State
    private var from = ""
    @State
    private var to = ""
    @Binding
    var filter: MailFilter
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("from")) {
                    TextField("from...", text: $from)
                }
                Section(header: Text("to")) {
                    TextField("to...", text: $to)
                }
                Section(header: Text("save changes")) {
                    Button(
                        action: {
                            filter.criteria.from = from
                            filter.criteria.to = to
                            // dismiss the view
                        }, label: {
                            Text("Save")
                        })
                        .disabled(from.isEmpty || to.isEmpty)
                }
            }
            .onAppear {
                from = filter.criteria.from
                to = filter.criteria.to
            }
        }
    }
}
struct MailFilter {
    var criteria: Criteria
}
struct Criteria {
    var from, to: String
}