MVVM SwiftUI / how calculate a amount with a button in the view with a function in my logic business (class) - mvvm

I have a business logic with several class with properties and func
Why there is no action with my button ?
The func is in my business logic (class)
struct ViewSaisie: View {
#ObservedObject var contribuable = VFContribuable()
#ObservedObject var foyer = VFFoyerFiscalFr()
#ObservedObject var modelTaxe = VFModeleTaxe()
#ObservedObject var bareme = VFBaremeIr()
#State private var stateRniSaisi = ""
var body: some View {
Form {
...
Section {
Text("Votre imposition est \(bareme.calcTmiDefinitive()*100, specifier: "%.2f") %")
TextField("Amount", text: $stateRniSaisi)
.keyboardType(.numberPad)
Text("Votre revenu net imposbale est de \(self.contribuable.revenuNetImposableSaisi)")
}
Section {
Button(action: {
print("SwiftUI: Button tapped")
// Call func in SomeView()
self.bareme.calcImpositionNette() // NO ACTION NO RESULT IN MY VIEW
}) {
Text("TOTAL")
...

The Button code you pasted above isn't complete (missing at least the trailing curly brace and wouldn't compile without it), so it's a bit hard to see what's going on.
And also, if you've got a fairly simple label for the button, I think it's a lot more readable to use the following structure for Button() with the trailing closure for the action:
Button(label: Text("TOTAL") {
print("SwiftUI: Button tapped")
// Call func in SomeView()
self.bareme.calcImpositionNette() // NO ACTION NO RESULT IN MY VIEW
}
That should work to print the text and to call your method on bareme.
Your version could work as well, but it's hard to say, because the code you pasted is incomplete.

Related

Save / restore array of booleans to core date

Building my first SwiftUI app, and have some basic knowledge of Swift. So a bit much to chew but I am enjoying learning.
I have a Form with many toggles saving/restoring from core data in my swift app. Works well but the interface is cumbersome with all the toggles.
Instead I want to make an HStack of tappable labels that will be selected / unselected instead. Then when you submit it will map the selected Text objects to the existing State variables I have OR? save an array of selected strings to core data (for restoring later?).
In either case my code for this has been cobbled from a todo list tutorial plus some nice HStack examples I have put in my form. They select/deselect nicely but I do not know how to save their state like I did the toggle switches.
I will paste what I think is relevant code and remove the rest.
#State var selectedItems: [String] = []
#State private var hadSugar = false
#State private var hadGluten = false
#State private var hadDairy = false
let dayvariablesText = [
"Sugar",
"Gluten",
"Dairy"
]
// section 1 works fine
Section {
VStack {
Section(header: Text("Actions")) {
Toggle("Sugar", isOn: $hadSugar)
Toggle("Gluten", isOn: $hadGluten)
Toggle("Dairy", isOn: $hadDairy)
}
}
}
// section 2 trying this
ScrollView(.horizontal) {
LazyHGrid(rows: rows) {
ForEach(0..<dayvariablesText.count, id: \.self) { item in
GridColumn(item: dayvariablesText[item], items: $selectedItems)
}
}
}.frame(width: 400, height: 100, alignment: .topLeading)
// save
Button("Submit") {
DataController().addMood(sugar: hadSugar, gluten: hadGluten, dairy: hadDairy, context: managedObjContext)
dismiss()
}
This works fine with the toggles shown above - how to do this when selecting gridItems in the next section for example?
I think you need to remodel your code. Having multiple sources of truth like in your example (with the vars and the array for the naming) is a bad practice and will hurt you in the long run.
Consider this solution. As there is a lot missing in your question it´s more general. So you need to implement it to fit your needs. But it should get you in the right direction.
//Create an enum to define your items
// naming needs some improvement :)
enum MakroType: String, CaseIterable{
case sugar = "Sugar", gluten = "Gluten", dairy = "Dairy"
}
//This struct will hold types you defined earlier
// including the bool indicating if hasEaten
struct Makro: Identifiable{
var id: MakroType {
makro
}
var makro: MakroType
var hasEaten: Bool
}
// The viewmodel will help you store and load the data
class Viewmodel: ObservableObject{
//define and create the array to hold the Makro structs
#Published var makros: [Makro] = []
init(){
// load the data either here or in the view
// when it appears
loadCoreData()
}
func loadCoreData(){
//load items
// ..... code here
// if no items assign default ones
if makros.isEmpty {
makros = MakroType.allCases.map{
Makro(makro: $0, hasEaten: false)
}
}
}
// needs to be implemented
func saveCoreData(){
print(makros)
}
}
struct ContentView: View {
// Create an instance of the Viewmodel here
#StateObject private var viewmodel: Viewmodel = Viewmodel()
var body: some View {
VStack{
ScrollView(.horizontal) {
LazyHStack {
// Iterate over the items themselves and not over the indices
// with the $ in front you can pass a binding on to the ChildView
ForEach($viewmodel.makros) { $makro in
SubView(makro: $makro)
}
}
}.frame(width: 400, height: 100, alignment: .topLeading)
Spacer()
Button("Save"){
viewmodel.saveCoreData()
}.padding()
}
.padding()
}
}
struct SubView: View{
// Hold the binding to the Makro here
#Binding var makro: Makro
var body: some View{
//Toggle to change the hasEaten Bool
//this will reflect through the Binding into the Viewmodel
Toggle(makro.makro.rawValue, isOn: $makro.hasEaten)
}
}

Why does my SwiftUI View not update on updating of an #State var?

I am having a strange issue with an #State var not updating an iOS SwiftUI view.
I have an edit screen for themes for a small game with a NavigationView with a list of game themes. When in edit mode and I select one of these themes, I open up an editor view, passing the theme as a binding to the editor view struct.
In my editor view I then have sections that allow the user to edit properties of the theme. I do not want to use bindings to the various theme properties in my edit fields because I do not want the changes to take effect immediately. Instead, I have created #State vars for each of these properties and then use bindings to these in the edit fields. That way, I give the user the option to either cancel without and changes taking effect, or select "Done" to assign the changes back to the theme via the binding.
In order to initialise the #State vars I have an onAppear block that assign the #State vars values from the respective theme properties.
The issue I am having is that when the onAppear block is executed and the vars are assigned, the relevant edit fields are not updating!
Here is a cut-down version of my code:
struct EditorView: View {
/// The current presentation mode of the view.
#Environment(\.presentationMode) var presentationMode
#Binding var theme: GameTheme
#State private var name = ""
...
var body: some View {
NavigationView {
Form {
nameSection
...
}
.navigationTitle("Edit \(theme.name)")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", action: cancel)
}
ToolbarItem(placement: .confirmationAction) {
Button("Done", action: saveTheme)
.disabled(!canSaveTheme)
}
}
.onAppear {
name = theme.name
...
}
}
.frame(minWidth: Constants.minViewSize.width, minHeight: Constants.minViewSize.height)
}
var nameSection: some View {
Section(header: Text("Name")) {
TextField(LocalizedStringKey("Name"), text: $name)
}
}
...
}
So the view gets shown an on appearing, the #State var name does correctly get assigned the value from theme.name; however, this allocation does not cause an update of the view and the value of "name" is not entered into the TextField.
Interestingly, and I do not know if this is a good thing to do, if I wrap the contents of the onAppear block in a DispatchQueue.main.async, everything works fine!
i.e.
.onAppear {
DispatchQueue.main.async {
name = theme.name
...
}
}
Does anyone have any idea as to how, within the onAppear, I can force a view refresh? Or, why the assignment to "name" does not force an update?
Thanks.
This isn't the answer per se, but I went ahead and created a new iOS project with the following code (based on your post, but I cleaned it up a bit and came up with the missing GameTheme object myself).
It's more or less the same, and shows that your posted structure does re-render.
I'm wondering if there's more to the code we can't see in your post that could be causing this.
Are you possibly setting the name state variable anywhere else in a way that could be overriding the value on load?
import SwiftUI
#main
struct TestIOSApp: App {
#State var gameTheme: GameTheme = GameTheme(name: "A game theme")
var body: some Scene {
WindowGroup {
ContentView(theme: $gameTheme)
}
}
}
struct GameTheme {
var name:String;
}
struct ContentView: View {
#Binding var theme:GameTheme;
/// The current presentation mode of the view.
#Environment(\.presentationMode) var presentationMode
#State private var name = "DEFAULT SHOULD NOT BE DISPLAYED"
var body: some View {
NavigationView {
Form {
nameSection
}
.navigationTitle("Edit \(theme.name)")
.onAppear {
name = theme.name
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", action: {})
}
ToolbarItem(placement: .confirmationAction) {
Button("Done", action: {})
}
}
.frame(maxWidth:.infinity, maxHeight: .infinity)
}
var nameSection: some View {
Section(header: Text("Name")) {
TextField(LocalizedStringKey("Name"), text: $name)
}
}
}
I seem to have solved my problem with an init(). I created init(theme: Binding<GameTheme>) and then within the init assigned the theme via _theme = theme and then assigned the name via _name = State(initialValue: theme.name.wrappedValue).

unwanted response - navigationlink cycles through all pages

I am doing a project where I've got some codes I am confused about. its an app to track user's habits, ContentView has a page with a list of habits, each has its own page (HabitPage) showing more detail using navigationLink.
navigationLink worked fine to show a habit page at first, but what I intend to do is the following:
user clicks a button to increase the number of times a habit is done
when the number of times its done reaches the goal, I want the app to show an alert, saying the goal's achieved
App to jump back to ContentView
I managed to do 1,2 but not 3, in my HabitPage I added an Environment var to be dismissed and a Bool to toggle. The Bool is tied to an observableobject class on a separate page. I added the Bool to navigationLink on isActive. Now what it does is that it's cycling through all the HabitPage after clicking into one from ContentView. Can someone explain the logic behind to me as I am lost to what caused it. thanks
ContentView
struct ContentView: View {
#ObservedObject var habitsCV = DataShare()
- function for deletion
var body: some View {
NavigationView {
ZStack {
//background
List {
ForEach (habitsCV.allHabitGeneral, id: \.name) { each in
**NavigationLink(destination: habitView(habitHV: self.habitsCV,singleHabit: each), isActive: self.$habitsCV.isShowingHabit) {**
- stuff for describing the habit
}
}
}
}
.onDelete(perform: itemRemove)
}
.frame(maxWidth: .infinity)
}
- buttons, another sheet which works fine and nav bar title
}
}
}
HabitPage
struct habitView : View {
**#ObservedObject var habitHV : DataShare**
var singleHabit : AllHabits
**#Environment(\.self.presentationMode) var presentationMode**
-function for the app
var body : some View {
NavigationView {
VStack(alignment: .leading) {
- text for descriptions of the habit
Button("increase timesDone") {
var o : Int = 0
- function to retrieve array index o
if self.habitHV.allHabitGeneral[o].timesDone > self.habitHV.allHabitGeneral[o].completionGoal {
-lines for alert
**self.habitHV.isShowingHabit = false**
**self.presentationMode.wrappedValue.dismiss()**
}
}
}
}
-alert
}
}
}
}
the class storing the Bool
class DataShare : ObservableObject {
- alert messages and another Boolean for a functional sheet
**#Published var isShowingHabit : Bool = false**
#Published var allHabitGeneral : [AllHabits] {
-didSet function
- initialiser

SwiftUI: How do I avoid modifying state during view update?

I want to update a text label after it is being pressed, but I am getting this error while my app runs (there is no compile error): [SwiftUI] Modifying state during view update, this will cause undefined behaviour.
This is my code:
import SwiftUI
var randomNum = Int.random(in: 0 ..< 230)
struct Flashcard : View {
#State var cardText = String()
var body: some View {
randomNum = Int.random(in: 0 ..< 230)
cardText = myArray[randomNum].kana
let stack = VStack {
Text(cardText)
.color(.red)
.bold()
.font(.title)
.tapAction {
self.flipCard()
}
}
return stack
}
func flipCard() {
cardText = myArray[randomNum].romaji
}
}
If you're running into this issue inside a function that isn't returning a View (and therefore can't use onAppear or gestures), another workaround is to wrap the update in an async update:
func updateUIView(_ uiView: ARView, context: Context) {
if fooVariable { do a thing }
DispatchQueue.main.async { fooVariable = nil }
}
I can't speak to whether this is best practices, however.
Edit: I work at Apple now; this is an acceptable method. An alternative is using a view model that conforms to ObservableObject.
struct ContentView: View {
#State var cardText: String = "Hello"
var body: some View {
self.cardText = "Goodbye" // <<< This mutation is no good.
return Text(cardText)
.onTapGesture {
self.cardText = "World"
}
}
}
Here I'm modifying a #State variable within the body of the body view. The problem is that mutations to #State variables cause the view to update, which in turn call the body method on the view. So, already in the middle of a call to body, another call to body initiated. This could go on and on.
On the other hand, a #State variable can be mutated in the onTapGesture block, because it's asynchronous and won't get called until after the update is finished.
For example, let's say I want to change the text every time a user taps the text view. I could have a #State variable isFlipped and when the text view is tapped, the code in the gesture's block toggles the isFlipped variable. Since it's a special #State variable, that change will drive the view to update. Since we're no longer in the middle of a view update, we won't get the "Modifying state during view update" warning.
struct ContentView: View {
#State var isFlipped = false
var body: some View {
return Text(isFlipped ? "World" : "Hello")
.onTapGesture {
self.isFlipped.toggle() // <<< This mutation is ok.
}
}
}
For your FlashcardView, you might want to define the card outside of the view itself and pass it into the view as a parameter to initialization.
struct Card {
let kana: String
let romaji: String
}
let myCard = Card(
kana: "Hello",
romaji: "World"
)
struct FlashcardView: View {
let card: Card
#State var isFlipped = false
var body: some View {
return Text(isFlipped ? card.romaji : card.kana)
.onTapGesture {
self.isFlipped.toggle()
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return FlashcardView(card: myCard)
}
}
#endif
However, if you want the view to change when card itself changes (not that you necessarily should do that as a logical next step), then this code is insufficient. You'll need to import Combine and reconfigure the card variable and the Card type itself, in addition to figuring out how and where the mutation going to happen. And that's a different question.
Long story short: modify #State variables within gesture blocks. If you want to modify them outside of the view itself, then you need something else besides a #State annotation. #State is for local/private use only.
(I'm using Xcode 11 beta 5)
On every redraw (in case a state variable changes) var body: some View gets reevaluated. Doing so in your case changes another state variable, which would without mitigation end in a loop since on every reevaluation another state variable change gets made.
How SwiftUI handles this is neither guaranteed to be stable, nor safe. That is why SwiftUI warns you that next time it may crash due to this.
Be it due to an implementation change, suddenly triggering an edge condition, or bad luck when something async changes text while it is being read from the same variable, giving you a garbage string/crash.
In most cases you will probably be fine, but that is less so guaranteed than usual.

SwiftUI: Forcing an Update

Normally, we're restricted from discussing Apple prerelease stuff, but I've already seen plenty of SwiftUI discussions, so I suspect that it's OK; just this once.
I am in the process of driving into the weeds on one of the tutorials (I do that).
I am adding a pair of buttons below the swipeable screens in the "Interfacing With UIKit" tutorial: https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
These are "Next" and "Prev" buttons. When at one end or the other, the corresponding button hides. I have that working fine.
The problem that I'm having, is accessing the UIPageViewController instance represented by the PageViewController.
I have the currentPage property changing (by making the PageViewController a delegate of the UIPageViewController), but I need to force the UIPageViewController to change programmatically.
I know that I can "brute force" the display by redrawing the PageView body, reflecting a new currentPage, but I'm not exactly sure how to do that.
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
#State var currentPage = 0
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
VStack {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
HStack(alignment: .center) {
Spacer()
if 0 < currentPage {
Button(action: {
self.prevPage()
}) {
Text("Prev")
}
Spacer()
}
Text(verbatim: "Page \(currentPage)")
if currentPage < viewControllers.count - 1 {
Spacer()
Button(action: {
self.nextPage()
}) {
Text("Next")
}
}
Spacer()
}
}
}
func nextPage() {
if currentPage < viewControllers.count - 1 {
currentPage += 1
}
}
func prevPage() {
if 0 < currentPage {
currentPage -= 1
}
}
}
I know the answer should be obvious, but I'm having difficulty figuring out how to programmatically refresh the VStack or body.
2021 SWIFT 1 and 2 both:
IMPORTANT THING! If you search for this hack, probably you doing something wrong! Please, read this block before you read hack solution!!!!!!!!!!
Your UI wasn't updated automatically because of you miss something
important.
Your ViewModel must be a class wrapped into ObservableObject/ObservedObject
Any field in ViewModel must be a STRUCT. NOT A CLASS!!!! Swift UI does not work with classes!
Must be used modifiers correctly (state, observable/observedObject, published, binding, etc)
If you need a class property in your View Model (for some reason) - you need to mark it as ObservableObject/Observed object and assign them into View's object !!!!!!!! inside init() of View. !!!!!!!
Sometimes is needed to use hacks. But this is really-really-really exclusive situation! In most cases this wrong way! One more time: Please, use structs instead of classes!
Your UI will be refreshed automatically if all of written above was used correctly.
Sample of correct usage:
struct SomeView : View {
#ObservedObject var model : SomeViewModel
#ObservedObject var someClassValue: MyClass
init(model: SomeViewModel) {
self.model = model
//as this is class we must do it observable and assign into view manually
self.someClassValue = model.someClassValue
}
var body: some View {
//here we can use model.someStructValue directly
// or we can use local someClassValue taken from VIEW, BUT NOT value from model
}
}
class SomeViewModel : ObservableObject {
#Published var someStructValue: Bool = false
var someClassValue: MyClass = MyClass() //myClass : ObservableObject
}
And the answer on topic question.
(hacks solutions - prefer do not use this)
Way 1: declare inside of view:
#State var updater: Bool = false
all you need to do is call updater.toggle()
Way 2: refresh from ViewModel
Works on SwiftUI 2
public class ViewModelSample : ObservableObject
func updateView(){
self.objectWillChange.send()
}
}
Way 3: refresh from ViewModel:
works on SwiftUI 1
import Combine
import SwiftUI
class ViewModelSample: ObservableObject {
private let objectWillChange = ObservableObjectPublisher()
func updateView(){
objectWillChange.send()
}
}
This is another solution what worked for me, using id() identifier. Basically, we are not really refreshing view. We are replacing the view with a new one.
import SwiftUI
struct ManualUpdatedTextField: View {
#State var name: String
var body: some View {
VStack {
TextField("", text: $name)
Text("Hello, \(name)!")
}
}
}
struct MainView: View {
#State private var name: String = "Tim"
#State private var theId = 0
var body: some View {
VStack {
Button {
name += " Cook"
theId += 1
} label: {
Text("update Text")
.padding()
.background(Color.blue)
}
ManualUpdatedTextField(name: name)
.id(theId)
}
}
}
Setting currentPage, as it is a #State, will reload the whole body.