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.
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)")
Related
My #Published string variable in Observableobejct does not update the Text somehow.
Exact same codes works in different view, but I can't figure out why this isn't working in this view.
When I searched Google all I figured out was about checking the initialization of observableobject, setting didSet to variable, but all of them did not work and I just reverted back.
The below is the code that does NOT update the view when string changes in viewdidappear().
struct StorageUpgradeView: View {
#ObservedObject private var storagevm = StorageVM()
#Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
VStack {
ZStack(alignment: .center) {
HStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image("Image_arrowleft")
.resizable()
.frame(width: 30, height: 30, alignment: .leading)
})
Spacer()
}
Text("Storage Upgrade".localized)
.font(.system(size: 24))
}
.padding()
Text(storagevm.month_price)
Text("\(storagevm.halfyear_price)")
Spacer()
}
}
.onAppear(perform: {
storagevm.viewdidappear()
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.foregroundColor(Color("Theme_OnSurface"))
.background(Color("Theme_Surface").ignoresSafeArea())
}
}
class StorageVM: ObservableObject {
#Published var month_price = "plpl"
#Published var halfyear_price: String
init() {
halfyear_price = "asdf"
}
func viewdidappear() {
month_price = "BlahBlah"
halfyear_price = "were"
}
}
But view prints just well when I switch the view by changing tabs. What is the problem here? I couldn't figure out after two days of googling and banging my head.
Whenever you want to observe a #Published attribute, you want to use the $ sign before the element. So try using Text(storagevm.$month_price)
Using StateObject instead of ObservedObject worked well for me! Be sure that your iOS target needs to be 14.0 or higher for this solution to work.
I have create the below contrived example to demonstrate my problem. In this code I am expecting the TextField to be initialised with "Test Company X" where X is the view depth based on the ObservableObject ViewModel. This is true and works fine for the first view only. Subsequent views do not get the published value on initial appearance. However as you back through the views and the onAppear triggers it does get initialised. Why is the TextField not correctly initialised on the second and subsequent views?
class TestViewModel: ObservableObject {
#Published var companyName: String = ""
func load(_ viewDepth: Int) {
debugPrint("load: \(viewDepth)")
// Mimic a network request to get data
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
debugPrint("publish: \(viewDepth)")
self.companyName = "Test Company \(viewDepth)"
}
}
}
struct TestFormView: View {
var viewDepth: Int
#Binding var companyName: String
#State private var navigateToNextView: Bool = false
var body: some View {
VStack(alignment: .leading, spacing: 3) {
TextField("Company name", text: $companyName)
.padding()
.background(
RoundedRectangle(cornerRadius: 5)
.strokeBorder(Color.primary.opacity(0.5), lineWidth: 3)
)
Button.init("NEXT") {
self.navigateToNextView = true
}
.frame(maxWidth: .infinity)
.foregroundColor(Color.primary)
.padding(10)
.font(Font.system(size: 18,
weight: .semibold,
design: .default))
.background(Color.secondary)
.cornerRadius(Sizes.cornerRadius)
NavigationLink(destination: TestView(viewDepth: viewDepth + 1), isActive: $navigateToNextView) {
EmptyView()
}
.isDetailLink(false)
Spacer()
}
}
}
struct TestView: View {
#ObservedObject var viewModel = TestViewModel()
var viewDepth: Int
var body: some View {
VStack {
TestFormView(viewDepth: viewDepth, companyName: $viewModel.companyName)
}
.onAppear(perform: {
self.viewModel.load(self.viewDepth)
})
.navigationBarTitle("View Depth \(viewDepth)")
.padding()
}
}
struct TestNavigationView: View {
#State private var begin: Bool = false
var body: some View {
NavigationView {
VStack {
if begin {
TestView(viewDepth: 1)
} else {
Button.init("BEGIN") {
self.begin = true
}
.frame(maxWidth: .infinity)
.foregroundColor(Color.primary)
.padding(10)
.font(Font.system(size: 18,
weight: .semibold,
design: .default))
.background(Color.secondary)
.cornerRadius(Sizes.cornerRadius)
}
}
}
}
}
Your model is recreated (due to current nature of NavigationLink)
SwiftUI 2.0
Fix is simple - use specially intended for such purpose StateObject
struct TestView: View {
#StateObject var viewModel = TestViewModel()
// .. other code
For my app, I have a welcome screen that intro's what the app does and allows the user to create their first item. When the user clicks the button I'd like to dismiss the 'welcomeScreen' sheet and and then launch the 'newRemindr' sheet.
I tried to achieve this by creating an observable object with an 'addNewTrigger' boolean set to false. When I click the Add New Reminder button on the welcomeScreen, the button's action causes the welcomeScreen to dismiss and toggles the 'addNewTrigger' boolean to True. (I've verified this is working with Print Statements). However content view is listening to that same observed object to launch the 'newRemindr' sheet but that action doesn't seem to be working.
Can somebody please take a look at the code and see where I am going wrong? Or suggest an alternative that can provide the type of functionality.
I really appreciate all the help. Thanks!
Code Below...
welcomeScreen:
import SwiftUI
import Combine
struct welcomeScreen: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#ObservedObject var addNewReminder = showAddScreen()
var body: some View {
NavigationView {
ZStack (alignment: .center) {
LinearGradient(gradient: Gradient(colors: [Color.white, Color.white, Color.gray]), startPoint: .top, endPoint: .bottom)
.edgesIgnoringSafeArea(.all)
Image("Ellipse2")
.offset(y: -475)
VStack {
Spacer()
Text("Welcome to")
.foregroundColor(.white)
.fontWeight(.bold)
Image("RemindrLogoWhite")
Spacer()
Text("What is remindr?")
.font(.title)
.fontWeight(.bold)
.padding(.bottom, 25)
Text("Remindr is a simple app designed to help you schedule random reminders with the goal of clearing your mind.\n\nRemind yourself to check in with your body, set up positive affirmations, set your intentions; Whatever it is, the power is up to you.")
.padding(.horizontal, 25)
.padding(.bottom, 25)
Text("Click below to get started:")
.fontWeight(.bold)
// Add New Reminder Button
Button(action: {
self.mode.wrappedValue.dismiss()
print("Add Reminder Button from Welcome Screen is Tapped")
self.addNewReminder.addNewTrigger.toggle()
print("var addNewTrigger has been changed to \(self.addNewReminder.addNewTrigger)")
}) {
Image("addButton")
.renderingMode(.original)
}.padding(.bottom, 25)
Spacer()
} .frame(maxWidth: UIScreen.main.bounds.width,
maxHeight: UIScreen.main.bounds.height)
}
.navigationBarTitle(Text(""), displayMode: .automatic)
.navigationBarItems(trailing: Button(action: {
self.mode.wrappedValue.dismiss()
}, label: {
Image(systemName: "xmark")
.foregroundColor(.white)
}))
}
}
}
ContentView:
import SwiftUI
import CoreData
class showAddScreen: ObservableObject {
#Published var addNewTrigger = false
}
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: ReminderEntity.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \ReminderEntity.dateCreated, ascending: false)])
var reminder: FetchedResults<ReminderEntity>
// Sheet Control
#ObservedObject var addNewReminder = showAddScreen()
//#State private var showingAddScreen = false
#State var showWelcomeScreen = false
let emojiList = EmojiList()
//Toggle Control
#State var notifyOn = true
// Save Items Function
func saveItems() {
do {
try moc.save()
} catch {
print(error)
}
}
// Delete Item Function
func deleteItem(indexSet: IndexSet) {
let source = indexSet.first!
let listItem = reminder[source]
moc.delete(listItem)
}
// View Controller
var body: some View {
VStack {
NavigationView {
ZStack (alignment: .top) {
// List View
List {
ForEach(reminder, id: \.self) { notification in
NavigationLink(destination: editRemindr(reminder: notification,
notifyOn: notification.notifyOn,
emojiChoice: Int(notification.emojiChoice),
notification: notification.notification ?? "unknown",
notes: notification.notes ?? "unknown")) {
// Text within List View
HStack {
// MARK: TODO
//Toggle("NotifyOn", isOn: self.$notifyOn)
// .labelsHidden() // Hides the label/title
Text("\(self.emojiList.emojis[Int(notification.emojiChoice)]) \(notification.notification!)")
}
}
}
.onDelete(perform: deleteItem)
}.lineLimit(1)
// Navigation Items
.navigationBarTitle("", displayMode: .inline)
.navigationBarItems(
leading:
HStack {
Button(action: {
self.showWelcomeScreen.toggle()
}) {
Image(systemName: "info.circle.fill")
.font(.system(size: 24, weight: .regular))
}.foregroundColor(.gray)
// Positioning Remindr Logo on Navigation
Image("remindrLogoSmall")
.resizable()
.aspectRatio(contentMode: .fit)
//.frame(width: 60, height: 60, alignment: .center)
.padding(.leading, 83)
.padding(.top, -10)
},
// Global Settings Navigation Item
trailing: NavigationLink(destination: globalSettings()){
Image("settings")
.font(Font.title.weight(.ultraLight))
}.foregroundColor(.gray)
)
// Add New Reminder Button
VStack {
Spacer()
Button(action: { self.addNewReminder.addNewTrigger.toggle()
}) {
Image("addButton")
.renderingMode(.original)
}
.sheet(isPresented: $addNewReminder.addNewTrigger) {
newRemindr().environment(\.managedObjectContext, self.moc)
}
}
}
} .sheet(isPresented: $showWelcomeScreen) {
welcomeScreen()
}
}
}
}
First what I see is you use different observable objects in both views, but should use same, so changes made in one view be available for second view as well.
Se here is a way to solve this
struct welcomeScreen: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#ObservedObject var addNewReminder: showAddScreen // << declare to be injected
// ... other code
and in ContentView
} .sheet(isPresented: $showWelcomeScreen) {
welcomeScreen(addNewReminder: self.addNewReminder) // << inject !!
}
Alternate: you can remove addNewReminder from welcomeScreen and work with it only in ContentView by activating on welcome sheet dismiss, like
} .sheet(isPresented: $showWelcomeScreen, onDismiss: {
// it is better to show second sheet with delay to give chance
// for first one to animate closing to the end
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.addNewReminder.addNewTrigger.toggle()
}
}
) {
welcomeScreen()
}
i have made an app in SwiftUI where you can create different classes. The app saves this in an array. I have a textfield and a button in the same view as the scrollview that displays the array. This works perfectly fine and I can easily add new classes. Now I made a new view with a text field and a button. This view can be viewed by pressing a button in the nav bar. It uses the exact same function as the other view, but in the other view adding a item to the array works, in this view it doesn't work. I hope you understand my problem and can help me.
Thank You.
This is the file where I store the array:
import Foundation
import SwiftUI
import Combine
struct Class: Identifiable {
var name: String
var id = UUID()
}
class ClassStore: ObservableObject {
#Published var classes = [Class]()
}
This is the view with the button + textfield that works and the scrollview that displays the array:
import SwiftUI
struct ContentView: View {
#State var showNewClass = false
#ObservedObject var classStore = ClassStore()
#State var newClass: String = ""
#State var toDoColor: Color = Color.pink
func addNewClass() {
classStore.classes.append(
Class(name: newClass)
)
newClass = ""
}
var body: some View {
NavigationView {
VStack {
HStack {
TextField("New Todo", text: $newClass)
Image(systemName: "app.fill")
.foregroundColor(Color.pink)
.padding(.horizontal, 3)
Image(systemName: "books.vertical")
.padding(.horizontal, 3)
if newClass == "" {
Text("Add!")
.foregroundColor(Color.gray)
} else {
Button(action: {
addNewClass()
}) {
Text("Add!")
}
}
}.padding()
ScrollView {
ForEach(self.classStore.classes) { name in
Text(name.name)
}
}
.navigationBarTitle(Text("Schulnoten"))
.navigationBarItems(trailing:
Button(action: {
self.showNewClass.toggle()
}) {
Text("New Class")
}
.sheet(isPresented: $showNewClass) {
NewClass(isPresented: $showNewClass)
})
}
}
}
}
And this is the new view I created, the button and the textfield have the exact same code, but somehow this doesn't work:
import SwiftUI
struct NewClass: View {
#Binding var isPresented: Bool
#State var className: String = ""
#ObservedObject var classStore = ClassStore()
func addNewClass() {
classStore.classes.append(
Class(name: className)
)
}
var body: some View {
VStack {
HStack {
Text("New Class")
.font(.title)
.fontWeight(.semibold)
Spacer()
}
TextField("Name of the class", text: $className)
.padding()
.background(Color.gray)
.cornerRadius(5)
.padding(.vertical)
Button(action: {
addNewClass()
self.isPresented.toggle()
}) {
HStack {
Text("Safe")
.foregroundColor(Color.white)
.fontWeight(.bold)
.font(.system(size: 20))
}
.padding()
.frame(width: 380, height: 60)
.background(Color.blue)
.cornerRadius(5)
}
Spacer()
}.padding()
}
}
Sorry if my English is not that good. I'm not a native speaker.
I assume you should pass same class store from parent view into sheet, ie
struct NewClass: View {
#Binding var isPresented: Bool
#State var className: String = ""
#ObservedObject var classStore: ClassStore // << expect external
and in ContentView
.sheet(isPresented: $showNewClass) {
NewClass(isPresented: $showNewClass, classStore: self.classStore) // << here !!
})
Consider the following code block:
import SwiftUI
struct MeasurementReading: View, Equatable {
#ObservedObject var ble: BluetoothConnectionmanager
#GestureState var isDetectTap = false
#State var MyText:String = "Wait"
static func == (lhs: MeasurementReading, rhs: MeasurementReading)->Bool{
return lhs.MyText == rhs.MyText
}
var body: some View {
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
return HStack {
Spacer()
VStack{
Button(action:{
self.MyText = "\(self.ble.getValue()!) mV"
print("Text is \(self.MyText as NSString)")
}, label: {
Text(MyText)
.font(.system(size: 40))
.bold()
.foregroundColor(Color.black)
.padding(.trailing, 15)
.frame(height: 100)
})
Button(action: {
self.MyText = "\(self.ble.getValue()!) mV"
print("Text is \(self.MyText as NSString)")
}, label: {
Text(MyText)
.font(.system(size: 25))
.padding(.top, -20)
.padding(.bottom, 20)
.foregroundColor(Color.black)
})
}
}.onReceive(timer)
{ _ in // TIMER FUNCTIONALITY HERE
self.MyText = "\(self.ble.getValue()!) mV"
print("Text is \(self.MyText)")
}
}
}
struct MeasurementReading_Previews: PreviewProvider {
static var previews: some View {
MeasurementReading(ble: BluetoothConnectionmanager())
}
}
Every 1 second the correct value read from the BLE system is assigned to MyText and then MyText is printed to the debug output properly with the updated value.
The problem here is that view MeasurementReading does not update. Also, using a closure on any item also has the same behavior (variable is updated, it is output properly but no view update) ex .onTap{....} will have the same behavior or any other .onXXXX closure. The only way I could get the view to update at all with new values for the MyText state is to put the behavior in a Button.
My question is this: Why does the view not update even when the state variable changes via Timer or .onXXXX closure?
You need to be setting the ble value to the updated timer value:
Without testing this properly. I also think your BluetoothConnectionmanager needs to be a #State property for this to work.
#State var ble: BluetoothConnectionmanager
.onReceive(timer) { value in // value is the updated value
self.ble.value = value
self.MyText = "\(self.ble.getValue()!) mV"
print("Text is \(self.MyText)")
}
Take a look at this example to see how a timer works with a Date() object.