0

Im trying to create a simple tip Calculator but i am having a problem once a user has added in the information in the app instead of calculating the tip, the function is returning as if there is an issue somewhere.

Im trying to make it so that when a user inputs a number in the text field and selects a percentage the total tip is displayed underneath.

How do i fix this problem? Ive added a print statement to print the word "returning" and it keeps printing this word so i think the problem is somewhere in the calculateTip function:

This is my Code for my ContentView class:

struct ContentView: View {

//MARK: - PROPERTIES
@ObservedObject public var tipVM = TipViewModel()

//MARK: - FUNCTIONS
private func endEditing() {
    hideKeyboard()
}
     
//MARK: - BODY
var body: some View {
    Background {
        NavigationView {
            ZStack {
                VStack {
                    HStack(spacing: 10) {
                        Text("Tip Calculator")
                            .font(.system(.largeTitle, design: .rounded))
                            .fontWeight(.heavy)
                            .padding(.leading, 4)
                            .foregroundColor(.blue)
                        Spacer()
                        Button(action: {
//                                tipVM.clearFields()
                        }, label: {
                            Text("Clear")
                                .font(.system(size: 16, weight: .semibold, design: .rounded))
                                .padding(.horizontal, 10)
                                .frame(minWidth: 70, minHeight: 24)
                                .background(
                                    Capsule().stroke(lineWidth: 2)
                                )
                                .foregroundColor(.blue)
                        }) //: BUTTON
                    } //: HSTACK
                    .padding()
                    Spacer(minLength: 80)
                    TextField("Enter Amount: ", text: $tipVM.amount)
                        .padding()
                        .background(Color.secondary)
                        .foregroundColor(.white)
                        .font(.system(.title3, design: .rounded))
                    Picker(selection: $tipVM.tipPercentage, label: Text("Picker"), content: {
                        ForEach(0 ..< tipVM.tipChoices.count) { index in
                            Text("\(self.tipVM.tipChoices[index])%").tag(index)
                                .font(.system(.body, design: .rounded)).padding()
                        }.padding()
                        .background(subtitleColor)
                        .foregroundColor(.white)
                    }).onTapGesture(perform: {
                        tipVM.calculateTip()
                    })
                    .pickerStyle(SegmentedPickerStyle())
                    Text(tipVM.tip == nil ? "£0" : "\(tipVM.tip!)")
                        .font(.system(.largeTitle, design: .rounded))
                        .fontWeight(.bold)
                        .foregroundColor(.blue)
                        .padding()
                    
                } //: VSTACK
            } //: ZSTACK
            .navigationBarHidden(true)
        } //: NAVIGATION VIEW
        .navigationViewStyle(StackNavigationViewStyle())
    }.onTapGesture {
        self.endEditing()
    }
    .ignoresSafeArea(.keyboard, edges: .all)
} //: BACKGROUND
}

And here is my code for my TipViewModel Class:

import Foundation
import SwiftUI
import Combine

class TipViewModel: ObservableObject {

var amount: String = ""
var tipPercentage: Int = 0
var tip: Double?

let tipChoices = [10,15,20,25,30]

let didChange = PassthroughSubject<TipViewModel, Never>()

func calculateTip() {

    guard let amount = Double(amount) else {
        print("returning")
        return
    }
    
    self.tip = amount * (Double(tipPercentage)/100)
    self.didChange.send(self)
}
}

I would appreciate any help thanks.

Gino Sesia
  • 383
  • 2
  • 4
  • 14

1 Answers1

1

The usual way is to mark the properties with @Published whose changes are going to be monitored. The extra Combine subject is not needed.

And declare tip as non-optional

import Foundation
import SwiftUI
// import Combine
    
class TipViewModel: ObservableObject {
    
    var amount: String = ""
    var tipPercentage: Int = 0
    @Published var tip = 0.0
    
    let tipChoices = [10,15,20,25,30]
    
    func calculateTip() {
    
        guard let numAmount = Double(amount) else {
            print("returning")
            return
        }
        self.tip = numAmount * (Double(tipPercentage)/100)
    }
}

Secondly if the current view struct creates (aka owns) the observable object use always @StateObject rather than @ObservedObject. The latter is for objects which are initialized at higher levels in the view hierarchy and just passed through.

struct ContentView: View {

    //MARK: - PROPERTIES
    @StatedObject private var tipVM = TipViewModel()

    ...

        Text("£\(tipVM.tip)")
vadian
  • 274,689
  • 30
  • 353
  • 361