#State var not changing - SwiftUI picker - swiftui

I am a real newbie in swift, but I am stuck at this problem...
I am building a picker to change an optical prescription, but I am not able to change SPH and CYL values (doubles), while AX works good. Any help?
Also the way that text comes out, I tried to add .format modifier just to show 2 decimals, but still no luck.
Thanks!
import SwiftUI
struct ContentView: View {
#State var selectedsph = 0.0
#State var selectedcyl = 0.0
#State var selectedax = 0
var sph : [Double] = Array(stride(from: -20.00, through: 20.00, by: 0.25))
var cyl : [Double] = Array(stride(from: -10.00, through: 10.00, by: 0.25))
var ax = [Int](0...180)
var body: some View {
GeometryReader { geometry in
VStack {
Spacer()
HStack(spacing:0) {
Picker(selection: self.$selectedsph, label: Text("")) {
ForEach(0 ..< self.sph.count) { index in
Text("Sph " + "\(self.sph[index])").tag(index)
}
}
.pickerStyle(.wheel)
.frame(width: geometry.size.width/3, height: 150)
.compositingGroup()
.clipped()
Picker(selection: self.$selectedcyl, label: Text("Picker")) {
ForEach(0 ..< self.cyl.count) { index in
Text("Cyl " + "\(self.cyl[index])").tag(index)
}
}
.pickerStyle(.wheel)
.frame(width: geometry.size.width/3.5, height: 150)
.compositingGroup()
.clipped()
Picker(selection: self.$selectedax, label: Text("Picker")) {
ForEach(0 ..< self.ax.count) { index in
Text("AX " + "\(self.ax[index])").tag(index)
}
}
.pickerStyle(.wheel)
.frame(width: geometry.size.width/3, height: 150)
.compositingGroup()
.clipped()
}.padding()
}
HStack(alignment: .center) {
Text("Occhio Destro: SF: \(selectedsph) Cyl: \(selectedcyl) Ax: \(selectedax)")
.fontWeight(.medium).multilineTextAlignment(.center).padding(.all)
}
Spacer()
}
}
}

The picker values are the indices of the selected values. Change your #State vars to hold Int:
#State var selectedsph = 0
#State var selectedcyl = 0
#State var selectedax = 0
And change your Text to use the indices to look up the values. Add specifier: "%.2f" to show just 2 decimal places:
Text("Occhio Destro: SF: \(sph[selectedsph], specifier: "%.2f") Cyl: \(cyl[selectedcyl], specifier: "%.2f") Ax: \(ax[selectedax])")
.fontWeight(.medium).multilineTextAlignment(.center).padding(.all)

Related

Remove Text Over a Placeholder in SwiftUI

I have a dilution calculator that works with no issues. However, there is always a "0" over the placeholder in the textfield for Container Size and Dilution Ratio. I don't mind the "0", I actually want a "0" there. But I have to erase it every time I tap on the textfield to input a number. Even if I remove the placeholder it's still there. How do I make it so that I don't have to keep erasing the "0" every time I want to input a number but keep the placeholder.
struct CalculatorView: View {
#State private var containerSize = 0
#State private var dilutionRatio = 0
#State private var totalProduct = 0.0
#State private var totalWater = 0.0
#FocusState private var amountIsFocused: Bool
#FocusState private var focusedInput: Field?
func totalProductAmount() -> Double {
let firstValue = Double(containerSize)
let secondValue = Double(dilutionRatio + 1)
let totalProduct = Double(firstValue / secondValue)
return totalProduct
}
func totalWaterAmount() -> Double {
let firstValue = Double(containerSize)
let secondValue = Double(dilutionRatio + 1)
let totalProduct = Double(firstValue / secondValue)
let totalWater = Double(firstValue - totalProduct)
return totalWater
}
var body: some View {
NavigationView {
VStack(alignment: .center) {
Image("Logo")
.padding(.horizontal, 30)
HStack {
//Container Size
ZStack {
Image("Container Size (Oz)")
.padding(.vertical, -15)
TextField("", value: $containerSize, format: .number)
.frame(width: 200.0, height: 60.0)
.multilineTextAlignment(.center)
.font(Font.system(size: 50, design: .default))
.foregroundColor(.white)
.keyboardType(.decimalPad)
.focused($amountIsFocused)
.focused($focusedInput, equals: .containerSize)
}
}
//Dilution Ratio
ZStack {
Image("Dilution Ratio - 2")
.padding(.vertical, -10)
TextField("", value: $dilutionRatio, format: .number)
.frame(width: 200.0, height: 60.0)
.multilineTextAlignment(.center)
.font(Font.system(size: 50, design: .default))
.foregroundColor(.white)
.keyboardType(.decimalPad)
.focused($amountIsFocused)
.focused($focusedInput, equals: .dilutionRatio)
}
//Go Button
Button(action: {
totalProduct = totalProductAmount()
totalWater = totalWaterAmount()
amountIsFocused = false
}, label: {
Image("Go Button")
})
//Results
HStack{
ZStack {
Image("Total Product (Oz)")
Text("\(totalProduct, specifier: "%.1f")")
.font(Font.system(size: 60, design: .default))
.foregroundColor(.white)
}
ZStack {
Image("Total Water (Oz)")
Text("\(totalWater, specifier: "%.1f")")
.font(Font.system(size: 60, design: .default))
.foregroundColor(.white)
}
Make containerSize and dilutionRatio an optional Int with no default value.
#State private var containerSize: Int?
#State private var dilutionRatio: Int?
TextField("0", value: $containerSize ?? "", format: .number)

Calculator Functionality

I’m working on a dilution calculator. I have it 98% working, however, I want it to work a certain way and I’m not sure to do that. This is my first app so I’m new at this.
So I want the user to be able to input the numbers and hit a button to get the calculation. I’ve been using #State and through my research and understanding, using that instantly updates any changes the user makes.
So how do I go about making the app wait till the user hits the “Go” button.
Hers my code so far.
#State private var ContainerSize = 0
#State private var DilutionRatio = 0
#State private var Go = ""
#State private var TotalProduct = 0.0
#State private var TotalWater = 0.0
#FocusState private var amountIsFocused: Bool
var totalProductAmount: Double {
let firstValue = Double(ContainerSize)
let secondValue = Double(DilutionRatio + 1)
let totalProduct = Double(firstValue / secondValue)
return totalProduct
}
var totalWaterAmount: Double {
let firstValue = Double(ContainerSize)
let secondValue = Double(DilutionRatio + 1)
let totalWater = Double(firstValue - secondValue)
return totalWater
}
//Container Size
ZStack {
Image("Container Size (Oz)")
.padding(.vertical, -15)
TextField("", value: $ContainerSize, format: .number)
.frame(width: 200.0, height: 60.0)
.multilineTextAlignment(.center)
.font(Font.system(size: 50, design: .default))
.keyboardType(.decimalPad)
.focused($amountIsFocused)
}
}
//Dilution Ratio
ZStack {
Image("Dilution Ratio - 2")
.padding(.vertical, -10)
TextField("", value: $DilutionRatio, format: .number)
.frame(width: 200.0, height: 60.0)
.multilineTextAlignment(.center)
.font(Font.system(size: 50, design: .default))
.keyboardType(.decimalPad)
.focused($amountIsFocused)
}
//Go Button
Button(action: {}, label: {
Image("Go Button")
})
//Results
HStack{
ZStack {
Image("Total Product (Oz)")
Text("\(totalProductAmount, specifier: "%.1f")")
.font(Font.system(size: 60, design: .default))
}
ZStack {
Image("Total Water (Oz)")
Text(totalWaterAmount, format: .number)
.font(Font.system(size: 60, design: .default))
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer(
Button("Done") {
amountIsFocused = false
}
}
struct CalculatorIView_Previews: PreviewProvider {
static var previews: some View {
CalculatorIView()
}
}
The calculator works as is but I want the user to input numbers, hit the “Go” button, and the results are shown.
You can create func for calculation and call it from button action. You should remove calculated properties, var totalProductAmount: Double and var totalWaterAmount: Double and do the calculation inside the function. You can check the example below.
https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-a-tappable-button
var body: some View{
Button(action: {
someCalculation()
}, label: {
Image("Go Button")
})
}
func someCalculation(){
// do some calculation and you can set #State variables or you can return some value. For example 'func someCalculation()->Double'
}
So far you do the calculation in calculated properties and display them directly. So they'll update every time one of their underlying #State values change.
If you only want to show results on button press, you should display your #State result vars, and update inside the button action.
Side note: property names should start lowercase.
struct ContentView: View {
// side note: var names should start lowerCase
#State private var containerSize = 0
#State private var dilutionRatio = 0
#State private var totalProduct = 0.0
#State private var totalWater = 0.0
// for clarity change calculations to funcs
func totalProductAmount() -> Double {
let firstValue = Double(containerSize)
let secondValue = Double(dilutionRatio + 1)
let totalProduct = Double(firstValue / secondValue)
return totalProduct
}
func totalWaterAmount() -> Double {
let firstValue = Double(containerSize)
let secondValue = Double(dilutionRatio + 1)
let totalWater = Double(firstValue - secondValue)
return totalWater
}
var body: some View {
VStack {
//Container Size
Text("Container Size (Oz)")
.padding(.vertical, -15)
TextField("", value: $containerSize, format: .number)
.frame(width: 200.0, height: 60.0)
.multilineTextAlignment(.center)
.font(Font.system(size: 50, design: .default))
.keyboardType(.decimalPad)
//Dilution Ratio
Text("Dilution Ratio - 2")
.padding(.vertical, -10)
TextField("", value: $dilutionRatio, format: .number)
.frame(width: 200.0, height: 60.0)
.multilineTextAlignment(.center)
.font(Font.system(size: 50, design: .default))
.keyboardType(.decimalPad)
//Go Button
Button(action: {
// Calculate here, and set State vars with results
totalProduct = totalProductAmount()
totalWater = totalWaterAmount()
}, label: {
Text("Go Button")
})
.buttonStyle(.borderedProminent)
.padding()
//Results
// Show the state vars, not the calculation vars!
HStack{
VStack {
Text("Total Product (Oz)")
Text("\(totalProduct, specifier: "%.1f")")
.font(Font.system(size: 60, design: .default))
}
VStack {
Text("Total Water (Oz)")
Text(totalWater, format: .number)
.font(Font.system(size: 60, design: .default))
}
}
}
}
}

SwiftUI: Updating a View to include a Custom Subview based on a user action is a separate view

so I am trying to have a view update to display a custom view based on a user selection from another view. This is a simple task app project I started to get a better understanding of SwiftUI and have hit my first major roadblock. The custom view is generated from a Tag object from Core Data, so it would be this information that is passed from View 2 to View 1.
I've marked where the update would take place as well as where the action is performed with TODOs. Hopefully I did a good job at explaining what I am hoping to accomplish, nothing I have tried seems to work. I am sure it's something simple but the solution is evading me.
View 1: View that needs to be updated when user returns
View 2: View where selection is made
The View that needs to be updated and its ViewModel.
struct AddTaskView: View {
//MARK: Variables
#Environment(\.managedObjectContext) var coreDataHandler
#Environment(\.presentationMode) var presentationMode
#StateObject var viewModel = AddTaskViewModel()
#StateObject var taskListViewModel = TaskListViewModel()
#State private var title: String = ""
#State private var info: String = ""
#State private var dueDate = Date()
var screenWidth = UIScreen.main.bounds.size.width
var screenHeight = UIScreen.main.bounds.size.height
var body: some View {
VStack(spacing: 20) {
Text("Add a New Task")
.font(.title)
.fontWeight(.bold)
//MARK: Task.title Field
TextField("Task", text: $title)
.font(.headline)
.padding(.leading)
.frame(height: 55)
//TODO: Update to a specific color
.background(Color(red: 0.9, green: 0.9, blue: 0.9))
.cornerRadius(10)
//MARK: Task.tag Field
HStack {
Text("Tag")
Spacer()
//TODO: UPDATE TO DISPLAY TAG IF SELECTED OTHERWISE DISPLAY ADDTAGBUTTONVIEW
NavigationLink(
destination: TagListView(),
label: {
AddTagButtonView()
}
)
.accentColor(.black)
}
//MARK: Task.info Field
TextEditor(text: $info)
.frame(width: screenWidth - 40, height: screenHeight/4, alignment: .center)
.autocapitalization(.sentences)
.multilineTextAlignment(.leading)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.black, lineWidth: 0.5)
)
//MARK: Task.dateDue Field
DatePicker(
"Due Date",
selection: $dueDate,
in: Date()...
)
.accentColor(.black)
.font(.headline)
Spacer()
Button(action: {
viewModel.addTask(taskTitle: title, taskInfo: info, taskDueDate: dueDate)
//Dismiss View if successful
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Add Task")
.frame(width: 150, height: 60)
.font(.headline)
.foregroundColor(.black)
.background(Color.yellow)
.cornerRadius(30)
})
}
.padding()
.navigationBarTitleDisplayMode(.inline)
}
}
final class AddTaskViewModel : ObservableObject {
var coreDataHandler = CoreDataHandler.shared
#Published var tag : Tag?
func addTask(taskTitle: String, taskInfo: String, taskDueDate: Date) {
let newTask = Task(context: coreDataHandler.container.viewContext)
newTask.title = taskTitle
newTask.info = taskInfo
newTask.dateCreated = Date()
newTask.dateDue = taskDueDate
newTask.completed = false
newTask.archived = false
coreDataHandler.save()
}
}
The View where the selection is made and its ViewModel
struct TagListView: View {
#FetchRequest(entity: Tag.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Tag.title, ascending: true)]) var tagList : FetchedResults<Tag>
#Environment(\.presentationMode) var presentationMode
#StateObject var viewModel = TagListViewModel()
var body: some View {
VStack {
HStack {
Text("Create a Tag")
.font(.system(size: 20))
.fontWeight(.medium)
Spacer()
NavigationLink(
destination: CreateTagView(),
label: {
Image(systemName: "plus.circle")
.font(.system(size: 25))
})
}
Divider()
.padding(.bottom, 10)
ScrollView(.vertical, showsIndicators: false, content: {
if tagList.count != 0 {
LazyVStack(spacing: 20) {
ForEach(tagList, id: \.self) { tag in
let tagColour = Color(red: tag.colourR, green: tag.colourG, blue: tag.colourB, opacity: tag.colourA)
Button {
//TODO: UPDATE ADDTASKVIEW TO DISPLAY THE SELECTED TAG
//Dismiss view
self.presentationMode.wrappedValue.dismiss()
} label: {
TagView(title: tag.title ?? "Tag", color: tagColour, darkText: false)
}
}
}
} else {
Text("Add your first tag.")
}
})
}
.padding()
}
}
final class TagListViewModel : ObservableObject {
}

SwiftUI: How to center multi column picker on screen

I created a multi column picker with SwiftUI that I want to center on the screen.
However, whatever I try it remains left outlined as shows on the picture.
What I've tried:
Adding (alignment: .center) on the GeometryReader, HStack and VStack.
Trying to center the picker itself
putting the pickers in a container and center that
So the question is how do center the 3 columned picker on the screen.
Thanks for your support!
Paul
import SwiftUI
import Combine
struct ContentView: View {
#State var initial = "n"
#State var final = "iu"
#State var tone = 2
#State var pinyin = ""
var initials = ["b","c","ch","d","f","g","h","k","l","m","n","p","q","r","s","sh","t","w","x","z","zh"]
var finals = ["a","ai","an","ang","ao","e","ei","en","eng","er","i","ia","ian","iang","iao","ie","in","iong","iu","o","ong","u","ua","uan","uang","uai","ui","un","uo","ü","üan","üe","ün"]
var tones = [Int](1..<6)
var body: some View {
VStack{
Text("你")
.font(/*#START_MENU_TOKEN#*/.title/*#END_MENU_TOKEN#*/)
GeometryReader { geometry in
HStack{
Picker(selection: self.$initial, label: Text("")) {
ForEach(0 ..< self.initials.count) { index in
Text("\(self.initials[index])").tag(self.initials[index])
}
}
.onReceive(Just(initial), perform: { value in
updatePinyin()
})
.frame(width: geometry.size.width/6, height: 200).clipped()
Picker(selection: self.$final, label: Text("")) {
ForEach(0 ..< self.finals.count) { index in
Text("\(self.finals[index])").tag(self.finals[index])
}
}
.onReceive(Just(final), perform: { value in
updatePinyin()
})
.frame(width: geometry.size.width/6, height: 200).clipped()
Picker(selection: self.$tone, label: Text("")) {
ForEach(0 ..< self.tones.count) { index in
Text("\(self.tones[index])").tag(self.tones[index])
}
}
.onReceive(Just(tone), perform: { value in
updatePinyin()
})
.frame(width: geometry.size.width/6, height: 200).clipped()
}
}
Text(pinyin)
}
}
func updatePinyin() {
pinyin = initial + final + String(tone+1)
print(pinyin)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
}
}
}
Not really sure the final goal, but for provided code it can be done just by making Stack consume all space provided by GeometryReader, like
HStack{
// ... other code here
}
.frame(maxWidth: .infinity) // << this one !!
Tested with Xcode 12.4 / iOS 14.4
Just add spacer in both side of Picker(s), Like that,
HStack {
Spacer()
// Other Codes
Spacer()
}

Error Cannot use instance member 'xxx' within property initializer

26-07-19
I'll update my code as I'm making progress watching the WWDC video's. My data model is:
struct Egg: Identifiable {
var id = UUID()
var thumbnailImage: String
var day: String
var date: String
var text: String
var imageDetail: String
var weight: Double
}
#if DEBUG
let testData = [
Egg(thumbnailImage: "Dag-1", day: "1.circle", date: "7 augustus 2019", text: "Kippen leggen iedere dag een ei.", imageDetail: "Day-1", weight: 35.48),
Egg(thumbnailImage: "Dag-2", day: "2.circle", date: "8 augustus 2019", text: "Kippen leggen iedere dag een ei.", imageDetail: "Day-2", weight: 35.23),
Egg(thumbnailImage: "Dag-3", day: "3.circle", date: "9 augustus 2019", text: "Kippen leggen iedere dag een ei.", imageDetail: "Day-3", weight: 34.92)
Etc, etc
]
I've a TabbedView, a ContentView, a ContentDetail and a couple of other views (for settings etc). The code for the ContentView is:
struct ContentView : View {
var eggs : [Egg] = []
var body: some View {
NavigationView {
List(eggs) { egg in
EggCell(egg: egg)
}
.padding(.top, 10.0)
.navigationBarTitle(Text("Egg management"), displayMode: .inline)
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView(eggs: testData)
}
}
#endif
struct EggCell : View {
let egg: Egg
var body: some View {
return NavigationLink(destination: ContentDetail(egg: egg)) {
ZStack {
HStack(spacing: 8.0) {
Image(egg.thumbnailImage)
.resizable()
.aspectRatio(contentMode: .fit)
.padding(.leading, -25)
.padding(.top, -15)
.padding(.bottom, -15)
.padding(.trailing, -25)
.frame(width: 85, height: 61)
VStack {
Image(systemName: egg.day)
.resizable()
.frame(width: 30, height: 22)
.padding(.leading, -82)
Spacer()
}
.padding(.leading)
VStack {
Text(egg.date)
.font(.headline)
.foregroundColor(Color.gray)
Text(egg.weight.clean)
.font(.title)
}
}
}
}
}
}
extension Double {
var clean: String {
return self.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(format: "%.2f", self)
}
}
The code for the ContentDetail is:
struct ContentDetail : View {
let egg: Egg
#State private var photo = true
#State private var calculated = false
#Binding var weight: Double
var body: some View {
VStack (alignment: .center, spacing: 10) {
Text(egg.date)
.font(.title)
.fontWeight(.medium)
.navigationBarTitle(Text(egg.date), displayMode: .inline)
ZStack (alignment: .topLeading) {
Image(photo ? egg.imageDetail : egg.thumbnailImage)
.resizable()
.aspectRatio(contentMode: .fill)
.background(Color.black)
.padding(.trailing, 0)
.tapAction { self.photo.toggle() }
VStack {
HStack {
Image(systemName: egg.day)
.resizable()
.padding(.leading, 10)
.padding(.top, 10)
.frame(width: 50, height: 36)
.foregroundColor(.white)
Spacer()
Image(systemName: photo ? "photo" : "wand.and.stars")
.resizable()
.padding(.trailing, 10)
.padding(.top, 10)
.frame(width: 50, height: 36)
.foregroundColor(.white)
}
Spacer()
HStack {
Image(systemName: "arrow.left.circle")
.resizable()
.padding(.leading, 10)
.padding(.bottom, 10)
.frame(width: 50, height: 50)
.foregroundColor(.white)
Spacer()
Image(systemName: "arrow.right.circle")
.resizable()
.padding(.trailing, 10)
.padding(.bottom, 10)
.frame(width: 50, height: 50)
.foregroundColor(.white)
}
}
}
Text("the weight is: \(egg.weight) gram")
.font(.headline)
.fontWeight(.bold)
ZStack {
RoundedRectangle(cornerRadius: 10)
.padding(.top, 45)
.padding(.bottom, 45)
.border(Color.gray, width: 5)
.opacity(0.1)
HStack {
Spacer()
DigitPicker(digitName: "tens", digit: $weight.tens)
DigitPicker(digitName: "ones", digit: $weight.ones)
Text(".")
.font(.largeTitle)
.fontWeight(.black)
.padding(.bottom, 10)
DigitPicker(digitName: "tenths", digit: $weight.tenths)
DigitPicker(digitName: "hundredths", digit: $weight.hundredths)
Spacer()
}
}
Toggle(isOn: $calculated) {
Text(calculated ? "This weight is calculated." : "This weight is measured.")
}
Text(egg.text)
.lineLimit(2)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 6)
Spacer()
}
.padding(6)
}
}
#if DEBUG
struct ContentDetail_Previews : PreviewProvider {
static var previews: some View {
NavigationView { ContentDetail(egg: testData[0]) }
}
}
#endif
struct DigitPicker: View {
var digitName: String
#Binding var digit: Int
var body: some View {
VStack {
Picker(selection: $digit, label: Text(digitName)) {
ForEach(0 ... 9) {
Text("\($0)").tag($0)
}
}.frame(width: 60, height: 110).clipped()
}
}
}
fileprivate extension Double {
var tens: Int {
get { sigFigs / 1000 }
set { replace(tens: newValue) }
}
var ones: Int {
get { (sigFigs / 100) % 10 }
set { replace(ones: newValue) }
}
var tenths: Int {
get { (sigFigs / 10) % 10 }
set { replace(tenths: newValue) }
}
var hundredths: Int {
get { sigFigs % 10 }
set { replace(hundredths: newValue) }
}
private mutating func replace(tens: Int? = nil, ones: Int? = nil, tenths: Int? = nil, hundredths: Int? = nil) {
self = Double(0
+ 1000 * (tens ?? self.tens)
+ 100 * (ones ?? self.ones)
+ 10 * (tenths ?? self.tenths)
+ (hundredths ?? self.hundredths)) / 100.0
}
private var sigFigs: Int {
return Int((self * 100).rounded(.toNearestOrEven))
}
}
The compiler errors I'm still getting are:
in ContentView, beneath NavigationLink: Missing argument for
parameter 'weight' in call
in ContentDetail, at NavigationView: Missing argument for parameter
'weight' in call
in ContentDetail, after #endif: Missing argument for parameter
'weight' in call
25-07-19
The following code is part of a List detail view. The var 'weight' is coming from the List through a 'NavigationLink' statement. In this code I declare it as '35.48', but the NavigationLink fills in its real value.
I want to make an array [3, 5, 4, 8] with the compactMap statement. That works okay in Playground. The values go to 4 different pickers (within a HStack).
import SwiftUI
import Foundation
struct ContentDetail : View {
var weight : Double = 35.48
var weightArray = "\(weight)".compactMap { Int("\($0)") }
#State var digit1 = weightArray[0] // error
#State var digit2 = weightArray[1] // error
#State var digit3 = weightArray[2] // error
#State var digit4 = weightArray[3] // error
var body: some View {
VStack (alignment: .center, spacing: 10) {
Text(weight)
.font(.title)
.fontWeight(.medium)
etc etc
I get an error 'Cannot use instance member 'weightArray' within property initializer; property initializers run before 'self' is available'.
If I use the following code it works fine (for the first list element):
import SwiftUI
import Foundation
struct ContentDetail : View {
var weight : Double = 35.48
var weightArray = [3, 5, 4, 8]
#State var digit1 = 3
#State var digit2 = 5
#State var digit3 = 4
#State var digit4 = 8
var body: some View {
VStack (alignment: .center, spacing: 10) {
Text(weight)
.font(.title)
.fontWeight(.medium)
etc etc
What is the correct SwiftUI approach and why?
Indeed, a property initializer cannot refer to another property in the same container. You have to initialize your properties in an init instead.
struct ContentDetail: View {
var weight: Double
var weightArray: [Int]
#State var digit1: Int
#State var digit2: Int
#State var digit3: Int
#State var digit4: Int
init(weight: Double) {
self.weight = weight
weightArray = "\(weight)".compactMap { Int("\($0)") }
_digit1 = .init(initialValue: weightArray[0])
_digit2 = .init(initialValue: weightArray[1])
_digit3 = .init(initialValue: weightArray[2])
_digit4 = .init(initialValue: weightArray[3])
}
But I suspect you're breaking out the digits because you want to let the user edit them individually, like this:
If that's what you want, you should not have a separate #State property for each digit. Instead, weight should be a #Binding and it should have a separate mutable property for each digit.
First, extend Double to give you access to the digits:
fileprivate extension Double {
var tens: Int {
get { sigFigs / 1000 }
set { replace(tens: newValue) }
}
var ones: Int {
get { (sigFigs / 100) % 10 }
set { replace(ones: newValue) }
}
var tenths: Int {
get { (sigFigs / 10) % 10 }
set { replace(tenths: newValue) }
}
var hundredths: Int {
get { sigFigs % 10 }
set { replace(hundredths: newValue) }
}
private mutating func replace(tens: Int? = nil, ones: Int? = nil, tenths: Int? = nil, hundredths: Int? = nil) {
self = Double(0
+ 1000 * (tens ?? self.tens)
+ 100 * (ones ?? self.ones)
+ 10 * (tenths ?? self.tenths)
+ (hundredths ?? self.hundredths)) / 100.0
}
private var sigFigs: Int {
return Int((self * 100).rounded(.toNearestOrEven))
}
}
Then, change ContentDetail's weight property to be a #Binding and get rid of the other properties:
struct ContentDetail: View {
#Binding var weight: Double
var body: some View {
HStack {
DigitPicker(digitName: "tens", digit: $weight.tens)
DigitPicker(digitName: "ones", digit: $weight.ones)
DigitPicker(digitName: "tenths", digit: $weight.tenths)
DigitPicker(digitName: "hundredths", digit: $weight.hundredths)
}
}
}
struct DigitPicker: View {
var digitName: String
#Binding var digit: Int
var body: some View {
VStack {
Picker(selection: $digit, label: Text(digitName)) {
ForEach(0 ... 9) {
Text("\($0)").tag($0)
}
}.frame(width: 60, height: 110).clipped()
}
}
}
Here's the rest of the code needed to test this in a playground, which is how I generated that image above:
import PlaygroundSupport
struct TestView: View {
#State var weight: Double = 35.48
var body: some View {
VStack(spacing: 0) {
Text("Weight: \(weight)")
ContentDetail(weight: $weight)
.padding()
}
}
}
let host = UIHostingController(rootView: TestView())
host.preferredContentSize = .init(width: 320, height: 240)
PlaygroundPage.current.liveView = host