A #State static property is being reinitiated without notice - swift

I have a view which looks like this:
struct Login: View {
#State static var errorMessage = ""
init() {
// ...
}
var body: some View {
// ...
}
}
I set errorMessage as static so I can set an error message from anywhere.
The problem is that even being static, it is always reinitiated each time the login view is showed, so the error message is always empty. I was thinking that maybe the presence of the init() method initiate it somehow, but I didn't figure how to fix this. What can I do?

I set errorMessage as static so I can set an error message from anywhere.
This is a misunderstanding of #State. The point of #State variables is to manage internal state to a View. If something external is even looking at a #State variable, let alone trying to set it, something is wrong.
Instead, what you need is an #ObservableObject that is passed to the view (or is accessed as a shared instance). For example:
class ErrorManager: ObservableObject {
#Published var errorMessage: String = "xyz"
}
This is the global thing that manages the error message. Anyone can call errorManager.errorMessage = "something" to set it. You can of course make this a shared instance if you wanted by adding a property:
static let shared = ErrorManager()
With that, you then pass it to the View:
struct Login: View {
#ObservedObject var errorManager: ErrorManager
var body: some View {
Text(errorManager.errorMessage)
}
}
Alternately, you could use the shared instance if you wanted:
#ObservedObject var errorManager = ErrorManager.shared
And that's it. Now change to the error automatically propagate. It's more likely that you want a LoginManager or something like that to handle the whole login process, and then observe that instead, but the process is the same.

#State creates a stateful container that is associated with an instance of a view. Each instance of your view has it's own copy of that #State container.
In contrast, a static variable does not change across instances.
These two concepts are not compatible. You should not be using static with #State.

Related

A struct mutating properties or ObservableObject Published properties to drive data changes

I would like help to further understand the implications of using the following 2 methods for driving data between multiple views.
My situation:
A parent view initialises multiple child views with data passed in.
This data is a big object.
Each view takes a different slice of the data.
Each view can manipulate the initial data (filtering, ordering etc)
Using an observableObeject to store this data and multiple published properties for each view :
can be passed in as an environment object that can be accessed by any view using #EnvironmentObject.
You can create a Binding to the published properties and change them.
Execute a method on the ObservableObject class and manipulate a property value which gets published using objectWillChange.send() inside the method.
I have achieved the desired listed above by using a struct with mutating methods. Once these properties are changed in the struct, the views which bind to these properties causes a re-render.
My struct does not do any async work. It sets initial values. Its properties are modified upon user action like clicking filter buttons.
Example
struct MyStruct {
var prop1 = "hello"
var prop2: [String] = []
init(prop2: [String]) {
self.prop2 = prop2
}
mutating func changeProp2(multiplier: Int) {
let computation = ...
prop2 = computation //<----- This mutates prop2 and so my view Binded to this value gets re-renderd.
}
}
struct ParentView: View {
var initValue: [String] // <- passed in from ContentView
#State private var myStruct: MyStruct
init(initValue: [String]) {
self.myStruct = MyStruct(prop2: initValue)
}
var body: some View {
VStack {
SiblingOne(myStruct: $myStruct)
SiblingTwo(myStruct: $myStruct)
}
}
}
struct SiblingOne: View {
#Binding var myStruct: MyStruct
var body: some View {
HStack{
Button {
myStruct.changeProp2(multiplier: 10)
} label: {
Text("Mutate Prop 2")
}
}
}
}
struct SiblingTwo: View {
#Binding var myStruct: MyStruct
var body: some View {
ForEach(Array(myStruct.prop2.enumerated()), id: \.offset) { idx, val in
Text(val)
}
}
}
Question:
What use cases are there for using an ObservableObject than using a struct that mutates its own properties?
There are overlap use cases however I wish to understand the differences where:
Some situation A favours ObservableObject
Some situation B favours struct mutating properties
Before I begin, when you say "these properties causes a re-render" nothing is actually re-rendered all that happens is all the body that depend on lets and #State vars that have changed are invoked and SwiftUI builds a tree of these values. This is super fast because its just creating values on the memory stack. It diffs this value tree with the previous and the differences are used to create/update/remove UIView objects on screen. The actual rendering is another level below that. So we refer to this as invalidation rather than render. It's good practice to "tighten" the invalidation for better performance, i.e. only declare lets/vars in that View that are actually used in the body to make it shorter. That being said no one has ever compared the performance between one large body and many small ones so the real gains are an unknown at the moment. Since these trees of values are created and thrown away it is important to only init value types and not any objects, e.g. don't init any NSNumberFormatter or NSPredicate objects as a View struct's let because they are instantly lost which is essentially a memory leak! Objects need to be in property wrappers so they are only init once.
In both of your example situations its best to prefer value types, i.e. structs to hold the data. If there is just simple mutating logic then use #State var struct with mutating funcs and pass it into subviews as a let if you need read access or #Binding var struct if you need write access.
If you need to persist or sync the data then that is when you would benefit from a reference type, i.e. an ObservableObject. Since objects are created on the memory heap these are more expensive to create so we should limit their use. If you would like the object's life cycle to be tied to something on screen then use #StateObject. We typically used one of these to download data but that is no longer needed now that we have .task which has the added benefit it will cancel the download automatically when the view dissapears, which no one remembered to do with #StateObject. However, if it is the model data that will never be deinit, e.g. the model structs will be loaded from disk and saved (asynchronously), then it's best to use a singleton object, and pass it in to the View hierarchy as an environment object, e.g. .environmentObject(Store.shared), then for previews you can use a model that is init with sample data rather that loaded from disk, e.g. .environmentObject(Store.preview). The advantage here is that the object can be passed into Views deep in the hierarchy without passing them all down as let object (not #ObservedObject because we wouldn't want body invovked on these intermediary Views that don't use the object).
The other important thing is your item struct should usually conform to Identifiable so you can use it in a ForEach View. I noticed in your code you used ForEach like a for loop on array indices, that's a mistake and will cause a crash. It's a View that you need to supply with Indentifiable data so it can track changes, i.e. moves, insertions, deletions. That is simply not possible with array indices, because if the item moves from 0 to 1 it still appears as 0.
Here are some examples of all that:
struct UserItem: Identifiable {
var username: String
var id: String {
username
}
}
class Store: ObservableObject {
static var shared = Store()
static var preview = Store(preview: true)
#Published var users: [UserItem] = []
init(preview: Bool = false) {
if (preview) {
users = loadSampleUsers()
}
else {
users = loadUsersFromDisk()
}
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(Store.shared)
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach($store.users) { $user in
UserView(user: $user)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Store.preview)
}
}
struct UserView: View {
#Binding var user: UserItem
var body: some View {
TextField("Username", text: $user.username)
}
}

why doesn't this code set the #State variable in a struct in swiftui

I've got the following code, which seems very simple.
import SwiftUI
struct Tester : View
{
#State var blah : String = "blah"
func setBlah(_ val : String) {
blah = val
}
var body: some View {
Text("text")
}
}
var test = Tester()
test.setBlah("blee")
print(test.blah)
I would normally expect to see the final print statement display "blee", but it is "blah" -- the variable never changed. If I pass blah into another view via a binding, I am able to change the value of blah there. Any insight here would be appreciated -- including rtfm, if you can point me to the right manual -- I have looked, and haven't found an answer.
Edit: after reading #jnpdx 's answer, I have a slightly different version, but it still doesn't work -- I'm not worried about this specific code working, but trying to understand the magic that #jnpdx refers to, in terms of how #State works, and why a binding passed to another view is able to modify the original #State variable, while I am unable to within the struct itself. I am guessing there is some part of SwiftUI that needs to be instantiated that the #State property's communicate with in order to store the variables outside of the struct, as the apple documentation says. New version follows:
import Foundation
import SwiftUI
struct Tester : View
{
#State var blah : String
func setBlah(_ val : String) {
$blah.wrappedValue = val
}
var body: some View {
Text("smoe text")
}
}
var test = Tester(blah: "blah")
test.setBlah("blee") // expect to see blee printed, but get nil instead
print(test.blah)
Thanks :)
#State in SwiftUI doesn't work like simple mutating functions on a struct -- it's more like a separate layer of state that gets stored alongside the view hierarchy.
Let's look at what this would have to look like if it were not SwiftUI/#State:
struct Tester
{
var blah : String = "blah"
mutating func setBlah(_ val : String) {
blah = val
}
}
var test = Tester()
test.setBlah("blee") // prints correctly
print(test.blah)
Note that above, setBlah has to be marked mutating because it mutates the struct. Whereas in your example, the compiler doesn't require it, because the struct itself is not actually mutating -- the #State property wrapper is doing some behind-the-scenes magic.
Check out the documentation on State: https://developer.apple.com/documentation/swiftui/state
In particular:
Don’t initialize a state property of a view at the point in the view hierarchy where you instantiate the view, because this can conflict with the storage management that SwiftUI provides. To avoid this, always declare state as private, and place it in the highest view in the view hierarchy that needs access to the value. Then share the state with any child views that also need access, either directly for read-only access, or as a binding for read-write access.
By marking #State as private, you can prevent things like outside entities trying to manipulate it directly. However, in your example, you've circumvented this a bit by making a setter function that would avoid the private issue even if it were included. So, really, setBlah should be marked private as well.

SwiftUI: Pass value to and use it in init of child view

I've been trying to create a small calendar app with SwiftUI and ran into some issues while trying to pass a value to a child view and use it in its init.
My code looks like this:
ContentView (parent view):
struct ContentView: View {
#State var selectedMonth: Date
var body: some View {
MonthGridView(selectedMonth: $selectedMonth)
}
}
MonthGridView (child view):
struct MonthGridView: View {
#Binding private var selectedMonth: Date
var days: [Int]
//this is the part where I'm having troubles
init(selectedMonth: Binding<Date>) {
self._selectedMonth = selectedMonth
days = dayIndices(currentMonth: $selectedMonth) //custom function, also in this line is the error right now
}
var body: some View {
//code
}
}
I have looked through a lot of posts on here and a wide variety of tutorials and this is what I came up with. I've tried moving some code around, but wasn't able to get it fully working. I imagine the problem is somewhere around the init, maybe about the Binding wrapper, but I was unable to find information about how to unwrap it.
Appreciate any help getting this working.
It'll be easier to understand the problem if we “de-sugar” the #Binding property wrapper. When you say this:
#Binding private var selectedMonth: Date
Swift translates that into this:
private var _selectedMonth: Binding<Date>
private var $selectedMonth: Date { _selectedMonth.projectedValue }
private var selectedDate: Date {
get { _selectedMonth.wrappedValue }
nonmutating set { _selectedMonth.wrappedValue }
}
Here is your init again:
init(selectedMonth: Binding<Date>) {
self._selectedMonth = selectedMonth
days = dayIndices(currentMonth: $selectedMonth) //custom function, also in this line is the error right now
}
You're using $selectedMonth before days has been initialized. But as I showed above, $selectedMonth is a computed property. You are not allowed to call any methods on self before self is fully initialized, and the getter of a computed property counts as a method.
In general, you can work around the limitation by accessing _selectedMonth.projectedValue directly:
days = dayIndices(currentMonth: _selectedMonth.projectedValue)
However, in this case, all of _selectedMonth, _selectedMonth.projectedValue, and the init parameter selectedMonth are the same Binding, you can use any of them directly. So either of these will also work:
days = dayIndices(currentMonth: selectedMonth)
days = dayIndices(currentMonth: _selectedMonth)

SwiftUI: Trouble creating a simple boolean that every other view can see

I’m loading data that appears throughout my app from a content management system's API. Once the data has finished loading, I’d like to be able to change a boolean from false to true, then make that boolean readable by every other view in my app so that I can create conditional if/else statements depending on whether the data has loaded yet or not.
I’ve tried doing this by creating an Observable Object at the root level to store the boolean, setting it to false by default:
#main
struct MyApp: App {
#StateObject var dataLoadingObject = DataHasLoaded()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(dataLoadingObject)
}
}
}
class DataHasLoaded: ObservableObject {
#Published var status: Bool = false
}
Then, in my data model file, I've tried to reference that Observable Object using #EnvironmentObject and then switch the boolean to true once the data has loaded:
class DataStore: ObservableObject {
#Published var stories: [Story] = storyData
#EnvironmentObject var dataLoadingObject: DataHasLoaded
init() {
// Fetching and appending
// the data here
self.dataLoadingObject.status = true
}
}
The app builds successfully, but when the data loads it crashes and I get this error message: Thread 1: Fatal error: No ObservableObject of type DataHasLoaded found. A View.environmentObject(_:) for DataHasLoaded may be missing as an ancestor of this view.
It seems like the issue is that my data model view isn't explicitly a child of ContentView(), so it's not aware of the Environment Object. That's where I get stuck. Do I:
a) Try to get my data model to be a child of ContentView()?
b) Use something that doesn't require a parent/child relationship?
c) Solve this some totally different way?
d) Continue to bang my head against my desk?
Thanks for your help!
#EnvironmentObject can be used in View only. It can't be used in ObservableObject.
You can pass DataHasLoaded in init instead:
class DataStore: ObservableObject {
#Published var stories: [Story] = storyData
var dataLoadingObject: DataHasLoaded
init(dataLoadingObject: DataHasLoaded) {
self.dataLoadingObject = dataLoadingObject
...
}
}
Just make sure you always pass the same instance.

SwiftUI not being updated with manual publish

I have a class, a “clock face” with regular updates; it should display an array of metrics that change over time.
Because I’d like the clock to also be displayed in a widget, I’ve found that I had to put the class into a framework (perhaps there’s another way, but I’m too far down the road now). This appears to have caused a problem with SwiftUI and observable objects.
In my View I have:
#ObservedObject var clockFace: myClock
In the clock face I have:
class myClock: ObservableObject, Identifiable {
var id: Int
#Publish public var metric:[metricObject] = []
....
// at some point the array is mutated and the display updates
}
I don’t know if Identifiable is needed but it’s doesn’t make any difference to the outcome. The public is demanded by the compiler, but it’s always been like that anyway.
With these lines I get a runtime error as the app starts:
objc[31175] no class for metaclass
So I took off the #Published and changed to a manual update:
public var metric:[metricObject] = [] {
didSet {
self.objectWillChange.send()`
}
}
And now I get a display and by setting a breakpoint I can see the send() is being called at regular intervals. But the display won’t update unless I add/remove from the array. I’m guessing the computed variables (which make up the bulk of the metricObject change isn’t being seen by SwiftUI. I’ve subsequently tried adding a “dummy” Int to the myClock class and setting that to a random value to trying to trigger a manual refresh via a send() on it’s didSet with no luck.
So how can I force a periodic redraw of the display?
What is MetricObject and can you make it a struct so you get Equatable for free?
When I do this with an Int it works:
class PeriodicUpdater: ObservableObject {
#Published var time = 0
var subscriptions = Set<AnyCancellable>()
init() {
Timer
.publish(every: 1, on: .main, in: .default)
.autoconnect()
.sink(receiveValue: { _ in
self.time = self.time + 1
})
.store(in: &subscriptions)
}
}
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
Text("\(self.updater.time)")
}
}
So it's taken a while but I've finally got it working. The problem seemed to be two-fold.
I had a class defined in my framework which controls the SwiftUI file. This class is sub-classed in both the main app and the widget.
Firstly I couldn't use #Published in the main class within the framework. That seemed to cause the error:
objc[31175] no class for metaclass
So I used #JoshHomman's idea of an iVar that's periodically updated but that didn't quite work for me. With my SwiftUI file, I had:
struct FRMWRKShape: Shape {
func drawShape(in rect: CGRect) -> Path {
// draw and return a shape
}
}
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
FRMWRKShape()
//....
FRMWRKShape() //slightly different parameters are passed in
}
}
The ContentView was executed every second as I wanted, however the FRMWRKShape code was called but not executed(?!) - except on first starting up - so the view doesn't update. When I changed to something far less D.R.Y. such as:
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
Path { path in
// same code as was in FRMWRKShape()
}
//....
Path { path in
// same code as was in FRMWRKShape()
// but slightly different parameters
}
}
}
Magically, the View was updated as I wanted it to be. I don't know if this is expected behaviour, perhaps someone can say whether I should file a Radar....