Calculator Functionality - swiftui

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))
}
}
}
}
}

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)

#State var not changing - SwiftUI picker

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)

Creating OTP page for SwiftUI Using TextField

I am trying to create an OTP page for my app but I don't know how to make the next textfield focus after I input a single digit in the first text field.
I created 6 text field for each digit of OTP. The next text field should be the first responder once I key in one digit from the first text field and so forth untill all 6 digits are complete.
I'm not sure how to do that in Swift UI. So far I manage to create 6 lines only as seen in the screenshot. The expected is only one digit should be per line. So the next text field should be focus once I input a single integer.
I tried other post like the use of #FocusState but it says unknown attribute.
I also tried the custom text field How to move to next TextField in SwiftUI?
but I cannot seem to make it work.
import SwiftUI
struct ContentView: View {
#State private var OTP1 = ""
#State private var OTP2 = ""
#State private var OTP3 = ""
#State private var OTP4 = ""
#State private var OTP5 = ""
#State private var OTP6 = ""
var body: some View {
VStack {
HStack(spacing: 16) {
VStack {
TextField("", text: $OTP1)
Line()
.stroke(style: StrokeStyle(lineWidth: 1))
.frame(width: 41, height: 1)
}
VStack {
TextField("", text: $OTP2)
Line()
.stroke(style: StrokeStyle(lineWidth: 1))
.frame(width: 41, height: 1)
}
VStack {
TextField("", text: $OTP3)
Line()
.stroke(style: StrokeStyle(lineWidth: 1))
.frame(width: 41, height: 1)
}
VStack {
TextField("", text: $OTP4)
Line()
.stroke(style: StrokeStyle(lineWidth: 1))
.frame(width: 41, height: 1)
}
VStack {
TextField("", text: $OTP5)
Line()
.stroke(style: StrokeStyle(lineWidth: 1))
.frame(width: 41, height: 1)
}
VStack {
TextField("", text: $OTP6)
Line()
.stroke(style: StrokeStyle(lineWidth: 1))
.frame(width: 41, height: 1)
}
}
}
}
}
struct Line: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
return path
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.fixed(width: 560, height: 50))
}
}
My OTP Page
Expected field
Here is my answer for iOS 14.
The view.
struct ContentView: View {
#StateObject var viewModel = ViewModel()
#State var isFocused = false
let textBoxWidth = UIScreen.main.bounds.width / 8
let textBoxHeight = UIScreen.main.bounds.width / 8
let spaceBetweenBoxes: CGFloat = 10
let paddingOfBox: CGFloat = 1
var textFieldOriginalWidth: CGFloat {
(textBoxWidth*6)+(spaceBetweenBoxes*3)+((paddingOfBox*2)*3)
}
var body: some View {
VStack {
ZStack {
HStack (spacing: spaceBetweenBoxes){
otpText(text: viewModel.otp1)
otpText(text: viewModel.otp2)
otpText(text: viewModel.otp3)
otpText(text: viewModel.otp4)
otpText(text: viewModel.otp5)
otpText(text: viewModel.otp6)
}
TextField("", text: $viewModel.otpField)
.frame(width: isFocused ? 0 : textFieldOriginalWidth, height: textBoxHeight)
.disabled(viewModel.isTextFieldDisabled)
.textContentType(.oneTimeCode)
.foregroundColor(.clear)
.accentColor(.clear)
.background(Color.clear)
.keyboardType(.numberPad)
}
}
}
private func otpText(text: String) -> some View {
return Text(text)
.font(.title)
.frame(width: textBoxWidth, height: textBoxHeight)
.background(VStack{
Spacer()
RoundedRectangle(cornerRadius: 1)
.frame(height: 0.5)
})
.padding(paddingOfBox)
}
}
This is the viewModel.
class ViewModel: ObservableObject {
#Published var otpField = "" {
didSet {
guard otpField.count <= 6,
otpField.last?.isNumber ?? true else {
otpField = oldValue
return
}
}
}
var otp1: String {
guard otpField.count >= 1 else {
return ""
}
return String(Array(otpField)[0])
}
var otp2: String {
guard otpField.count >= 2 else {
return ""
}
return String(Array(otpField)[1])
}
var otp3: String {
guard otpField.count >= 3 else {
return ""
}
return String(Array(otpField)[2])
}
var otp4: String {
guard otpField.count >= 4 else {
return ""
}
return String(Array(otpField)[3])
}
var otp5: String {
guard otpField.count >= 5 else {
return ""
}
return String(Array(otpField)[4])
}
var otp6: String {
guard otpField.count >= 6 else {
return ""
}
return String(Array(otpField)[5])
}
#Published var borderColor: Color = .black
#Published var isTextFieldDisabled = false
var successCompletionHandler: (()->())?
#Published var showResendText = false
}
Not very reusable but it works....
If you want to change the length don't forget to update the viewModel's otpField's didSet and the views textFieldOriginalWidth.
The idea here is to hide the TextField and make it seem like the user is typing in the boxes.
An Idea could be to shrink the TextField when user is typing by using the isEditing closure from the TextField. You would want to shrink it so the user can't paste text or get that "popup" or the textfield cursor.

'Modifying state during view update, this will cause undefined behavior.' error when typing on a textfield (SwiftUI)

I have two textfields, assigned to:
#State private var emailAddress: String = ""
#State private var password: String = ""
Now whenever I am typing on it, the app seems to get stuck and gives me this error:
'Modifying state during view update, this will cause undefined behavior.'
I have a StartView():
class UserSettings: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
#Published var loggedIn : Bool = false {
didSet {
didChange.send(())
}
}
}
struct StartView: View {
#EnvironmentObject var settings: UserSettings
var body: some View {
if settings.loggedIn {
return AnyView(TabbarView())
}else {
return AnyView(ContentView())
}
}
}
I have created a ObservableObject class of UserSettings that has loggedIn bool value. When the user taps on 'Log In' button in LogInView(), this bool value becomes true and a new view appears (TabbarView())
This is LogInView():
struct LogInView: View {
#EnvironmentObject var settings: UserSettings
#State private var emailAddress: String = ""
#State private var password: String = ""
var body: some View {
GeometryReader { geometry in
VStack (alignment: .center){
HStack {
Image("2")
.resizable()
.frame(width: 20, height: 20)
Text("Social App")
.font(.system(size: 12))
}.padding(.top, 30)
.padding(.bottom, 10)
Text("Log In to Your Account")
.font(.title)
.font(.system(size: 14, weight: .bold, design: Font.Design.default))
.padding(.bottom, 50)
TextField("Email", text: self.$emailAddress)
.frame(width: geometry.size.width - 45, height: 50)
.textContentType(.emailAddress)
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
.accentColor(.red)
.background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
.cornerRadius(5)
TextField("Password", text: self.$password)
.frame(width: geometry.size.width - 45, height: 50)
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
.foregroundColor(.gray)
.background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
.textContentType(.password)
.cornerRadius(5)
Button(action: {
self.settings.loggedIn = true
}) {
HStack {
Text("Log In")
}
.padding()
.frame(width: geometry.size.width - 40, height: 40)
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(5)
}
.padding(.bottom, 40)
Divider()
Button(action: {
print("Take to forget password VC")
}) {
Text("Forgot your password?")
}
Spacer()
}
.padding(.bottom, 90)
}
}
}
I know this error appears if I am updating the view while state is being modified (when typing in textfield). But I am not updating the view anywhere in the Log In screen. Then why this error occurs. Help will be appreciated!
This works for me, you don't even need to import Combine! When you use #Published, SwiftUI will automatically synthesize the objectWillChange subject, and will call send whenever the property is mutated. You can still call .send() manually if you need to, but in most cases you won't.
class UserSettings: ObservableObject {
#Published var loggedIn : Bool = false
}
Excerpt from beta 5 release notes:
You can manually conform to ObservableObject by defining an
objectWillChange publisher that emits before the object changes.
However, by default, ObservableObject automatically synthesizes
objectWillChange and emits before any #Published properties change.
This is the full code that is working fine for me (both iPhone Xr and real device, iPad 6th Gen):
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(UserSettings()))
import SwiftUI
struct ContentView: View {
var body: some View {
StartView()
}
}
class UserSettings: ObservableObject {
#Published var loggedIn : Bool = false
}
struct StartView: View {
#EnvironmentObject var settings: UserSettings
var body: some View {
if settings.loggedIn {
return AnyView(Text("LOGGED IN"))
} else {
return AnyView(LogInView())
}
}
}
struct LogInView: View {
#EnvironmentObject var settings: UserSettings
#State private var emailAddress: String = ""
#State private var password: String = ""
var body: some View {
GeometryReader { geometry in
VStack (alignment: .center){
HStack {
Image(systemName: "2.circle.fill")
.resizable()
.frame(width: 20, height: 20)
Text("Social App")
.font(.system(size: 12))
}.padding(.top, 30)
.padding(.bottom, 10)
Text("Log In to Your Account")
.font(.title)
.font(.system(size: 14, weight: .bold, design: Font.Design.default))
.padding(.bottom, 50)
TextField("Email", text: self.$emailAddress)
.frame(width: geometry.size.width - 45, height: 50)
.textContentType(.emailAddress)
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
.accentColor(.red)
.background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
.cornerRadius(5)
TextField("Password", text: self.$password)
.frame(width: geometry.size.width - 45, height: 50)
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
.foregroundColor(.gray)
.background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
.textContentType(.password)
.cornerRadius(5)
Button(action: {
self.settings.loggedIn = true
}) {
HStack {
Text("Log In")
}
.padding()
.frame(width: geometry.size.width - 40, height: 40)
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(5)
}
.padding(.bottom, 40)
Divider()
Button(action: {
print("Take to forget password VC")
}) {
Text("Forgot your password?")
}
Spacer()
}
.padding(.bottom, 90)
}
}
}
I guess this is a bug. This message you got is also happening on this simple view which filters out list entries by user input. Just typing fast in the text field causes this issue. If you enter the first character into the text field, the UI stuck for some time.
struct ContentView: View {
#State private var list: [String] = (0..<500).map { "Text \($0)" }
#State private var searchText: String = ""
var filteredList: [String] {
guard !searchText.isEmpty else { return list }
return list.filter({ $0.contains(self.searchText) })
}
var body: some View {
VStack {
TextField("Search", text: $searchText)
List(filteredList, id: \String.self) { t in Text(t) }
}
.padding()
}
}
A workaround is to move the #State variables into a model. So this seems to be an issue with #State:
class Model: ObservableObject {
#Published var list: [String] = (0..<500).map { "Text \($0)" }
#Published var searchText: String = ""
var filteredList: [String] {
guard !searchText.isEmpty else { return list }
return list.filter({ $0.contains(self.searchText) })
}
}
struct ContentView: View {
#ObservedObject var model: Model
var body: some View {
VStack {
TextField("Search", text: $model.searchText)
List(model.filteredList, id: \String.self) { t in Text(t) }
}
.padding()
}
}
This may not be related to your issue, but in Xcode 11 Beta 4, Apple changed "didset" to "willset" and "didChange" to "willChange"
In Xcode 11 Beta 5, apple changed "willChange" to "objectWillChange".
Thus the StartView() should be:
class UserSettings: ObservableObject {
var objectWillChange = PassthroughSubject<Void, Never>()
#Published var loggedIn : Bool = false {
willSet {
objectWillChange.send(())
}
}
}
Don't branch with if, use .opacity(_:)
#ViewBuilder
var body: some View {
// if settings.loggedIn {
TabbarView().opacity(settings.loggedIn ? 1 : 0)
// } else {
ContentView().opacity(settings.loggedIn ? 0 : 1)
// }
}

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