How to initialize a #State variable inside a view [duplicate] - swift

This question already has answers here:
SwiftUI #State var initialization issue
(8 answers)
Closed 1 year ago.
I have a view that is initialized with a list of items, and then during the initialization process, we need to pick one item at random. Something like this:
struct ItemsView: View {
var items:[Item]
#State var current:Item?
init(items:[Item] = []) {
self.items = items
if items.count > 0 {
let index = Int.random(in: 0..<items.count)
self.current = items[index] // doesnt work
}
if current != nil {
print("init with", self.items.count, "items. Chose", current!)
}
}
// ...
}
The job of the ItemsVew is to show one item at time, at random, so we start by picking an item at random, but self.current = items[index] literally doesn't do anything as far as I can tell, for reasons I don't understand. So either I am doing something silly, or I am thinking about how to solve this in an incorrectly somehow.
So how do you initialize a State variable in the init function. I have tried:
self.current = State(initialValue: items[index])
But that simply triggers a compiler error. So how do we select an initial item that will be used when the view is displayed?
Cannot assign value of type 'State<Item>' to type 'Item'
What am I doing wrong?

Because #State is a property wrapper, you want to assign to the underlying variable itself, not the wrapped value type (which is Item? in this case).
self._current = State(initialValue: items[index])
// ^ note the underscore
The best documentation for this is available in the original Swift Evolution SE-0258 proposal document.

#State is a property wrapper, so you will need to assign a Item? object to the current property that is why the compiler gives you an error. So as long as the items array you pass is not empty, this should work as expected. Also I encourage you to use randomElement() method on the items array instead of producing a random index.
struct ItemsView: View {
var items: [Item]
#State var current: Item?
init(items: [Item] = []) {
self.items = items
self.current = items.randomElement()
if let item = current {
print("init with", self.items.count, "items. Choose", item)
}
}
// ...
}

Related

SwiftUI: How to display an array of items and have them modifiable programmatically

I have a list of SwiftUI Text instances, and I want to be able to alter their characteristics (in this example, font weight) after initial creation of the body. It looks like SwiftUI doesn't want you to perform modifying functions on View elements after placing them in the body (i.e. Text().fontWeight() doesn't change the weight of the text in the instance Text() gave you, but returns a different instance which has the font weight you want, which means that these modifying methods only make sense when applied within your view's body, I guess).
My attempts to work with this paradigm have failed, however. Consider the following code to display a line of digits and a "Click Me" button which is intended to bold a different digit with every click. It initializes an array of boldable Text instances. In order to make ForEach work, because Text doesn't conform to Hashable or Identifiable, I needed to make a struct BoldableText which has an identifier, a Text instance, and the intended weight of that Text instance.
A function, highlight(), is intended to advance the bolded text to the next digit, wrapping around to 0 when it reaches the end.
struct BoldableText: Identifiable {
var id: Int // So that it works with ForEach in the View
var theWeight: Font.Weight // The weight of this Text item
var theText:Text // The actual Text item
}
let DIGITS = 6 // How many digits to display
var idx_of_bolded:Int = 0 // Which of them is currently bold?
// A view to show an array of Text items, each boldable after user interaction
struct ArrayView: View {
#State var numbers:[BoldableText] = []
// Set up the numbers array with digit texts with regular font weight
init() {
numbers = []
for i in 0..<DIGITS {
numbers.append(BoldableText(id: i, theWeight:Font.Weight.regular, theText: Text(String(i))))
}
}
// Advance the highlighted number and highlight that text
func highlight() {
numbers[idx_of_bolded].theWeight = Font.Weight.regular
idx_of_bolded = (idx_of_bolded + 1) % DIGITS
numbers[idx_of_bolded].theWeight = Font.Weight.heavy
}
var body: some View {
HStack {
// Display the numbers in a line
ForEach(numbers) { number in
Text(String(number.id)).fontWeight(number.theWeight)
}
Button(action: highlight) {
Text("Click to advance the number")
}
}
}
}
The problem is, this won't even build. The two lines in highlight() which try to change the .theWeight property get flagged for trying to mutate numbers[].
If I fix this by marking func highlight() as mutating, Xcode flags the call to it from the Button() action.
Alternately, I can fix it by moving var numbers ... outside of the struct, but then I can't use #State on it.
That gets it to build, but clicking the button doesn't do anything, I'm guessing because nothing is bound to the font weights, Swift thinks they're just static values.
Meanwhile, if I try binding by using fontWeight($number.theWeight), Xcode complains that it "can't find $number in scope". Same goes for ForEach($numbers)
If I bring #State var numbers... back into the struct and keep ForEach($numbers) the error at fontWeight($number.theWeight) changes to a very intriguing "Cannot convert value of type 'Binding<Font.Weight>' to expected argument type 'Font.Weight?'" (which suggests to me that fontWeight doesn't even accept bound parameters, which seems odd).
Is it possible to even do this with SwiftUI, or must I go back to using a ViewController?
Many changes - conceptual mistakes, see inline. (I recommend to take several SwiftUI full-scale tutorials)
Tested with Xcode 13.4 / iOS 15.5
struct BoldableText: Identifiable {
var id: Int
var theWeight: Font.Weight
var theText: String // << Text is a view, so store just string here !!
}
let DIGITS = 6
struct ArrayView: View {
#State private var numbers:[BoldableText] // << initialized once so will do this in init
#State private var idx_of_bolded:Int = 0 // << affects view, so needs to be state
init() {
// << prepare initial model
let numbers = (0..<DIGITS).map {
BoldableText(id: $0, theWeight:Font.Weight.regular, theText: String($0))
}
_numbers = State(initialValue: numbers) // << state initialization !!
}
func highlight() {
numbers[idx_of_bolded].theWeight = Font.Weight.regular
idx_of_bolded = (idx_of_bolded + 1) % DIGITS // << now updates view !!
numbers[idx_of_bolded].theWeight = Font.Weight.heavy
}
var body: some View {
HStack {
// Display the numbers in a line
ForEach(numbers) { number in
Text(String(number.id)).fontWeight(number.theWeight)
}
Button(action: highlight) {
Text("Click to advance the number")
}
}
}
}

Swift & SwiftUI - Conditional global var

I want to make a global variable in Swift, so that its Data is accessible to any view that needs it. Eventually it will be a var so that I can mutate it, but while trying to get past this hurdle I'm just using it as let
I can do that by putting this as the top of a file (seemingly any file, Swift is weird):
let myData: [MyStruct] = load("myDataFile.json)
load() returns a JSONDecoder(). MyStruct is a :Hashable, Codable, Identifiable struct
That data is then available to any view that wants it, which is great. However, I want to be able to specify the file that is loaded based on a condition - I'm open to suggestions, but I've been using an #AppStorage variable to determine things when inside a View.
What I'd like to do, but can't, is do something like:
#AppStorage("appStorageVar") var appStorageVar: String = "Condition1"
if(appStorageVar == "Condition2") {
let myData: [MyStruct] = load("myDataFile2.json")
}
else {
let myData: [MyStruct] = load("myDataFile.json")
}
I can do this inside a View's body, but then it's only accessible to that View and then I have to repeat it constantly, which can't possibly the correct way to do it.
You could change just change the global in an onChange on the AppStorage variable. This is an answer to your question, but you have the problem that no view is going to be updating itself when the global changes.
var myData: [MyStruct] = load("myDataFile.json)
struct ContentView: View {
#AppStorage("appStorageVar") var appStorageVar: String = "Condition1"
var body: some View {
Button("Change value") {
appStorageVar = "Condition2"
}
.onChange(of: appStorageVar) { newValue in
myData = load(newValue == "Condition1" ? "myDataFile.json" : "myDataFile2.json")
}
}
}

Computed Property from Child Struct Not Updating in SwiftUI

I have a data model in my SwiftUI app that looks something like this:
struct Entry: Identifiable{
var id = UUID().uuidString
var name = ""
var duration: Int{
//An SQLite query that returns the total of the "duration" column
let total = try! dbc.scalar(tableFlight.filter(db.entry == id).select(db.duration.total))
return Int(total)
}
}
struct Flight: Identifiable{
var id = UUID().uuidString
var duration = 0
var entry: String?
}
I have an ObservableObject view model that produces the entries like this:
class EntryModel: ObservableObject{
static let shared = EntryModel()
#Published var entries = [Entry]()
init(){
get()
}
func get(){
//Stuff to fetch the entries
entries = //SQLite query that returns an array of Entry objects
}
}
Then finally, in my View, I list all the entry names and their associated duration like this:
ForEach(modelEntry.entries){ entry in
VStack{
Text(entry.name) //<-- Updates fine
Text(entry.duration) //<-- Gets set initially, but no updates
}
}
The issue I'm having is that when I update a Flight for that Entry, the duration in my view doesn't update. I know that won't happen because only the entries will redraw when they are changed.
But even if I manually call the get() function in my EntryModel, the associated duration still doesn't update.
Is there a better way to do this? How do I get the parent's computed properties to recalculate when its child element is updated?
I figured it out. My actual code used a child View inside the ForEach where the VStack is. I was just passing an entry to it, so the values were only getting set initially and were thus not reactive.
By changing that entry to a Binding, it's working:
ForEach($modelEntry.entries){ $entry in
ChildView(entry: $entry)
}
Note that the $ on the $modelEntry and the return $entry is an Xcode 13+ feature (but backward compatible to iOS 14 and macOS 11.0).

is possible to change #State property's value through its structure's method?

at Xcode's play ground I run code below:
import SwiftUI
struct Test {
#State var count: Int = 0 //to avoid "mutating" keyword, I use "#State" property wrapper
func countUp() {
self.count += 1
}
}
var test = Test()
test.countUp() // I want "count" property to be count up. but it did not
print(test.count) // print 0
Q) why test.count is still 0??
This may not be the intent of the question, but I will explain why the code in question does not work.(I believe this is the same thing the comment.)
You are using SwiftUI in a playground and not View.
The #State conforms to DynamicProperty.
https://developer.apple.com/documentation/swiftui/state
https://developer.apple.com/documentation/swiftui/dynamicproperty
The Documents say:
You should only access a state property from inside the view’s body, or from methods called by it.
An interface for a stored variable that updates an external property of a view.
and
The view gives values to these properties prior to recomputing the view’s body.
It has been suggested to use it with View and view’s body.
As mentioned above, your code does not use View or view’s body.
Since #State can only be used in a struct, if you want to do something similar, you can use a class and use #Published as shown below(simple use Combine, not SwiftUI):
import Combine
class Test {
#Published var count: Int = 0
func countUp() {
self.count += 1
}
}
var test = Test()
test.countUp()
print(test.count) // 1
However, your code has no one to subscribe to the changes, so this is just an experimental code.
Expressions are not allowed at the top level.
To test your case I created a View struct called Test1, and within its initialiser I created object for Test view. Check code below.
import SwiftUI
struct Test1:View {
init() {
let test = Test() // concentrate on this
test.countUp()
}
var body: some View {
Test() // Ignore for now
}
}
Test View-:
import SwiftUI
#propertyWrapper struct TestCount:DynamicProperty {
#State private var count = 0
var wrappedValue: Int {
get {
count
}
nonmutating set {
count = newValue
}
}
init(_ count: Int) {
self.count = count
}
}
struct Test: View {
#TestCount var count:Int
init(count:Int) {
_count = TestCount(count)
print(_count)
}
var body: some View {
Button {
countUp()
} label: {
Text("\(count)")
}
}
func countUp() {
count += 1
print(count)
}
}
For better understanding I created a custom propertyWrapper and we will initialise that inside Test view.
Now, when you run this code in simulator Test1 view is called first, and in it’s initialiser it will create object for Test() view.
Test view upon initialisation will create object for it’s property wrapper called TestCount, once created it will print description for TestCount instance, and notice the print output now-:
TestCount(_count: SwiftUI.State<Swift.Int>(_value: 0, _location: nil))
You can clearly see State variable count inside TestCount wrapper was assigned default value 0, but wasn’t allocated any SwiftUI.StoredLocation yet.
Now, this line is executed next test.countUp() in Test1 view, and because we are calling it from outside of body property and Test1 is also initialised outside of body property , compiler is smart enough to detect that and it will not provide SwiftUI storage location for this change to count variable, because there is no view update required for this change, hence you always see Output as 0 (default saved value).
To prove my point, now make following change in Test1 view.
import SwiftUI
struct Test1:View {
// init() {
// let test = Test(count: 0)
// test.countUp() // I want "count" property to be count up. but it did not
// }
var body: some View {
Test(count: 0)
}
}
Now again the initial print statement inside Test view is same -:
TestCount(_count: SwiftUI.State<Swift.Int>(_value: 0, _location: nil))
But, now you have a view with Button to change the count variable value on tap.
When you tap the button, again countUp() will be called, and this time check the output of print statement again, SwiftUI storage location is no more nil.
TestCount(_count: SwiftUI.State<Swift.Int>(_value: 0, _location:
Optional(SwiftUI.StoredLocation<Swift.Int>))).
You can debug the code further to get more better understanding, and I would also suggest you to read about PropertyWrappers in more detail, and how they work internally.

Accessing a lazy property on a struct mutates the struct

I have a lazy property in a struct and every time I access it, it mutates the struct.
var numbers = [1,2,3]
struct MyStruct {
lazy var items = numbers
}
class MyClass {
var myStructPropery: MyStruct = MyStruct() {
didSet {
print(myStructPropery)
}
}
}
var myClass = MyClass()
myClass.myStructPropery.items
myClass.myStructPropery.items
myClass.myStructPropery.items
Result:
Print (didSet) will be called every time.
The ideal behaviour should be first time mutation only (since that's how lazy variables behave). Is this a bug in Swift or am I doing something wrong?
What you are seeing is is that the observed property items has notified its observer that it has been mutated because it was accessed, and a lazy variable is by definition mutating since it will be set at a later time. Therefore didSet gets called to handle this.
The lazy variable is actually only set once even though it signals that it has mutated and the property myStructPropery is only mutated once when the variable is first set but is the same instance after that.
Here is how we can verify this, first change the lazy var declaration so it's more like how we usually declare such a variable
lazy var items: [Int] = { numbers }()
and then add a print statement
lazy var items: [Int] = {
print("inside lazy")
return numbers
}()
If we now run the test code
var myClass = MyClass()
myClass.myStructProperty.items
myClass.myStructProperty.items
myClass.myStructProperty.items
we see that "inside lazy" only prints once. To verify that the property myStructProperty isn't changed we can make the struct conform to Equatable and perform a simple check inside didSet
didSet {
if oldValue != myStructProperty {
print(myStructProperty)
}
}
Now running the test we see that the print inside didSet is never executed so myStructProperty is never changed.
I have no idea if this behaviour is a bug but personally it feels like it might be complicate for the property observer to stop observing a lazy property once it was accessed or for a lazy var to not be defined as mutating once it is set.
I started debugging this with following set up -
var numbers = [1,2,3]
struct MyStruct {
lazy var items = numbers
}
class MyClass {
var myStructPropery: MyStruct = MyStruct() {
didSet {
// Changed this to make sure we are not invoking getter here
print("myStructPropery setter called")
}
}
}
let myClass = MyClass()
myClass.myStructPropery.items
myClass.myStructPropery.items
myClass.myStructPropery.items
I can reproduce the problem on Xcode 12.5 using Swift 5.4.
Attempt 1 : Turn var numbers into let numbers - Does NOT work.
let numbers = [1,2,3]
Attempt 2 : Assign the value inline without using an extra variable - Does NOT work.
struct MyStruct {
lazy var items = [1,2,3]
}
Attempt 3 : Assign the value inline using the full blown getter syntax - Does NOT work.
struct MyStruct {
lazy var items: [Int] = {
return [1,2,3]
}()
}
At this point, we are out of options to try. Even though we can clearly see that return [1,2,3] in the last attempt is executed exactly once, the MyClass.myStructPropery.modify is called repeatedly on access to items.
Maybe Swift Forums is a better place to discuss this.