How do I use an existing property in a property wrapper when self hasn't been initialized? (SwiftUI) - swift

I have a struct with two variables inside property wrappers. One of the variables is supposed to be computed from the other. When I try to do this, I get the following error:
Cannot use instance member 'name' within property initializer; property initializers run before 'self' is available.
I tried assigning a temporary value to these variables, and then re-assigning them within a custom init() function, but that doesn't seem to work ether. I made a simplified version of the code to see if I could isolate the issue.
import SwiftUI
struct Person {
#State var name: String = ""
#State var nameTag: NameTag = NameTag(words: "")
init(name: String) {
// not changing name and nameTag
self.name = name
nameTag = NameTag(words: "Hi, my name is \(name).")
}
}
class NameTag {
var words: String
init(words: String) {
self.words = words
}
}
var me = Person(name: "Myself")
// still set to initial values
me.name
me.nameTag.words
I noticed that when I changed nameTag to an #ObservedObject, rather than #State, it was able to be re-assigned correctly. Although I don't believe I can change name to #ObservedObject. Could anyone tell me what I'm doing wrong?

To use property wrappers in initializers, you use the variable names with preceding underscores.
And with State, you use init(initialValue:).
struct Person {
#State var name: String
#State var nameTag: NameTag
init(name: String) {
_name = .init(initialValue: name)
_nameTag = .init( initialValue: .init(words: name) )
}
}
Here's what a #State property really looks like, as your tear down levels of syntactic sugar:
name
_name.wrappedValue
$name.wrappedValue
_name.projectedValue.wrappedValue
You can't use the underscore-name outside of the initial type definition.

Related

SwiftUI : #State var - Cannot use instance member 'cercax' within property initializer; property initializers run before 'self' is available [duplicate]

Seems like I'm having a problem with something that shouldn't be the case... But I would like to ask for some help.
There are some explanations here on the Stack I don't get.
Having two simple classes where one refers to another, as per below:
class User {
lazy var name: String = ""
lazy var age: Int = 0
init (name: String, age: Int) {
self.name = name
self.age = age
}
}
class MyOwn {
let myUser: User = User(name: "John", age: 100)
var life = myUser.age
//Cannot use instance member 'myUser' within property initializer
//property initializers run before 'self' is available
}
I get the commented compile error. May someone please tell me what should I do to solve the case?
As correctly pointed out by vadian you should create an init in such scenarios:
class MyOwn {
let myUser: User
var life: Int
init() {
self.myUser = User(name: "John", age: 100)
self.life = myUser.age
}
}
You can't provide a default value for a stored property that depends on another instance property.
You should declare life like this:
lazy var life:Int = {
return self.myUser.age
}()
Because you are trying to initialise one property(variable) with another during initialisation process. At this time variables are not available yet.

SwiftUI: What is the difference between var and let as a parameter in List's Row?

What is the difference between var and let as a parameter in List's Row?
Usually, if I don't change the landmark variable, the compiler will warn me, but there is no warning in that row. I wonder why.
struct LandmarkRow: View {
var landmark: Landmark
var body: some View {
Text(landmark.id)
}
}
struct LandmarkRow: View {
let landmark: Landmark
var body: some View {
Text(landmark.id)
}
}
This looks like a same result:
struct LandmarkList: View {
var body: some View {
List(landmarks, id: \.id) { landmark in
LandmarkRow(landmark: landmark)
}
}
}
var is a variable meaning it is mutable. You can reassign it a value as many time as you wish.
In LandmarkRow and LandmarkList, var body is a computed property that calculates (rather than stores) a value. And it's read-only. Computed properties can only be declared using var.
When you implement a custom view, you must implement a computed
body property to provide the content for your view. Return a view
that's composed of primitive views that SwiftUI provides, plus other composite views that you've already defined. https://developer.apple.com/documentation/swiftui/view/body-swift.property
let is used to declare a constant. You can assign it a value exactly once as you have done in LandmarkList. In other words, you can not reassign it a value.
List(landmarks, id: \.id) { landmark in
LandmarkRow(landmark: landmark)
}
Example
struct Example {
// variable that you can change
var name: String
// constant that can only be assigned a value once.
let fileExtension: String
// A computed property that is read-only
var filename: String {
return name + "." + fileExtension
}
}
var example = Example(name: "example", fileExtension: "swift")
You can change name from example to example_2 since it is a variable.
example.name = "example_2"
You can not change fileExtension from swift to js because fileExtension is a constant (let). If you do so, you will get the following error.
Cannot assign to property: 'fileExtension' is a 'let' constant
example.fileExtension = "js"
You can not change fileName because it is a read-only property. If you try to change, you will get this error.
Cannot assign to property: 'filename' is a get-only property
example.filename = "ex.js"
More info
What is the difference between `let` and `var` in swift?
https://docs.swift.org/swift-book/LanguageGuide/Properties.html
https://www.avanderlee.com/swift/computed-property/
The compiler warns you about local variables which are never been modified or being unused.
But you get never a warning about declared struct members / class properties. And a SwiftUI view is actually a regular struct.
However you get an error if you are going to modify a struct member / class property which is declared as constant.
In a SwiftUI View there is no difference except for semantics.
A struct is immutable therefore having a var vs a let is irrelevant.
let Is semantically correct
I am also new to SwiftUI. I only have backgound in C and C++. I am guessing that it has to do with the fact that you declared landmark but didn't initialized it (or in this case, default value). Here, it assumes that you will initialize landmark when you initialize a LandmarkRow. Getting back to the point, I think the compiler doesn't know if landmark changes or not untill it is run.
var => Variable
By defining var you inform the compiler that this variable will be changed in further execution of code.
var current_day = 6
let => Constant
By defining let you inform the compiler that this is a constant variable and its value stays the same.
Like
let earth_gravity = 9.8
Its just best practise to make unchanging variables constant.
There will be no difference in execution of output.

Why are there parenthesis after the value of this variable in Swiftui?

Why are there parenthesis after the value of the entries variable in Swiftui? What does this mean?
func barChartItems() -> [ChartDataEntry] {
var entries = [ChartDataEntry]()
...
}
This is just a syntax to create an object of an empty typed array of ChartDataEntry type.
Creating an Empty Array
var someInts = [Int]() // an empty array of Int
Specifically the parenthesis following the declaration is the init() method call for the class or type. Because an Array type can be created without any init variables they are not needed to initialize the Array type, in-fact Swift best practice is not to add them if you aren't passing anything:
https://developer.apple.com/documentation/swift/array
In the case of a class, you always need the parenthesis to create an object instance from the class.
I built a small SwiftUI example with an #Observable class that has an multiple init() options. You can see how the init() parameters can change the properties of the object by calling the different initializers:
class MultipleInitOptions:ObservableObject {
#Published var name = "hard coded name"
init(){} // has no parameters, just use empty parenthesis
init(name:String) { // has parameters, pass in a name parameter inside parenthesis
self.name = name
}
}
struct TesterView: View {
#StateObject var hardCoded = MultipleInitOptions()
#StateObject var custom = MultipleInitOptions(name:"custom name")
var body: some View {
VStack {
Text("\(hardCoded.name)")
.padding()
Text("\(custom.name)")
.padding()
}
}
}

How can I generate state variables from my database in Swift?

My solution requires that I hard-code placeholder state vars field_1, field_2 that are then passed to my TextField dynamically via an array.
How can I achieve this more dynamically? I would like to have arbitrary Firestore documents from my database map to matching arbitrary TextFields. I use this to generate a settings screen that varies by user.
Here is my current solution, referenced above:
import SwiftUI
struct ParentView: View {
var data = [
["field": "Account Number", "title": "Account Number"],
["field": "Account Number", "title": "Account Number"]
]
#State var settings: [Dictionary<String,Any>] = []
var fields: [Binding<String>] = []
#State var field_1: String = "1"
#State var field_2: String = "2"
init(){
self.fields.append(self.$field_1)
self.fields.append(self.$field_2)
}
func getSettingsObjects() { for item in data { settings.append(item)}}
var body: some View {
VStack {
ForEach(self.$settings.wrappedValue.indices, id: \.self) { i in
TextField(self.settings[i]["title"] as! String, text: self.fields[i])
}
}
.padding(.all,20)
.navigationBarTitle("Settings")
.onAppear {
_ = self.getSettingsObjects()
}
}
}
To handle firebase fields, I like to set up a global struct. Then, every time instead of typing the string, you can user variable DatabaseField.field1 instead.
struct DatabaseField { //Titles for fields in database
static let field1 = "field_one_title"
static let field2 = "field_two_title"
}
Here, you can access the String by calling...
let field: String = DatabaseField.field1
Separately, you could also create a global enum to handle states, items, etc. with a rawValue of type String. I would use this if the State is going to work like a Bool in your viewController.
enum FieldState: String { //ViewController states can be used in functions
case field1 = "field_one_title"
case field2 = "field_two_title"
}
Here, you can set variables to this new type...
var currentState: FieldState = .field1
You can access the String value of this type with...
let currentField: String = currentState.rawValue
As you have realized, you should try to avoid hard-coding variable Strings and database field names whenever possible. I usually make a separate Swift File and store all of the global Structs or Enums for my project there. This way you can hard code it once and never have to touch it again. If you ever have to change the field title, you only need to change it within the Struct/Enum! Side note: your project may include several Structs and Enums. If this is the case, try not to make both a Struct/Enum for the same field/string or it might get confusing to decipher which is which.

Understanding struct initialiser if it contain private property

I want to understand how initialiser work if struct contains private properties. I have following code:
struct Doctor {
var name: String
var location: String
private var currentPatient = "No one"
}
let drJones = Doctor(name: "Esther Jones", location: "Bristol")
This throws an error:
Cannot invoke initializer for type 'Doctor' with an argument list of
type '(name: String, location: String)'
My Assumption is: Default Memeberwise initialiser contains private property which can't be called from outside.
But I am confused by following code:
struct Doctor {
private var currentPatient = "No one"
}
let drJones = Doctor()
How this is working?, it is not throwing any error.
You can't use default memberwise initialiser for assigning struct's property with private access level modifier.
Your second example works, because you gave your property default value so there is no need to assign it while you're initalizing it.
If you need to assign your private property using initializer, you have to write your own
init(name: String, location: String, currentPatient: String) {
self.name = name
self.location = location
self.currentPatient = currentPatient
}