Can't Update Text on Previous View using #EnvironmentObject SwiftUI - ios

i'm new in SwiftUI. So, I am doing my practice and create some kind of cashier app and here I been stuck for a while. In ProductPageView I can increase and decrease the number of item. Then, when I go to DetailOrderView (like a cart view), I can also increase and decrease the quantity number. The print shows the quantity number correctly.
But if I go back to ProductPageView, the label doesn't update it self.
Take a look at this screenshots:
First, I add 2 items.
Then I go to DetailOrderView, the quantity number is the same.:
Then I add 2 items in DetailOrderView (so it's now 4 items) and going back to ProductPageView, notice how the total price has increased but the quantity doesn't change. I feel like I want to "refresh" or "reload data" on this page.
How can I fix this and update the Text when I press the back button?
Here's my code:
Main
import SwiftUI
#main
struct CashierApp: App {
#StateObject var productOder = ProductPresenter()
var body: some Scene {
WindowGroup {
MainPageView()
.environmentObject(productOder)
}
}
}
Product Page View
import SwiftUI
struct ProductPageView: View {
#EnvironmentObject var productOrder: ProductPresenter
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(productOrder.cartItems, id: \.item.idItem) { element in
ProductRow(cartItems: CartItems(item: element.item, quantity: element.quantity))
}
}
}
TotalPaymentRow()
}
}
}
Detail Order View or Cart View
import SwiftUI
struct DetailOrderView: View {
#EnvironmentObject var productOrder: ProductPresenter
var arrayOrdered: [CartItems] = []
var body: some View {
ZStack {
ScrollView {
VStack {
ForEach(arrayOrdered, id: \.item.idItem) { element in
ProductRow(cartItems: CartItems(item: element.item, quantity: element.quantity))
}
}
.padding(.top, 10)
}
CustomModalGrandTotal()
}
.navigationTitle("Cart")
.navigationBarTitleDisplayMode(.inline)
}
}
The Presenter
import SwiftUI
import Combine
class ProductPresenter: ObservableObject {
#Published var cartItems: [CartItems] = []
var totalQuantity = 0
// Increment product
func increment(cartItem: inout CartItems) {
let index: Int = cartItems.firstIndex(where: {$0.item == cartItem.item}) ?? -1
cartItems[index].quantity += 1
totalQuantity += 1
}
// Decrement product
func decrement(cartItem: inout CartItems) {
let index: Int = cartItems.firstIndex(where: {$0.item == cartItem.item}) ?? -1
if cartItems[index].quantity > 0 {
cartItems[index].quantity -= 1
totalQuantity -= 1
}
}
}
Product Row
import SwiftUI
struct ProductRow: View {
#State var cartItems: CartItems
#EnvironmentObject var productOrder: ProductPresenter
var body: some View {
HStack(alignment: .center) {
Image(cartItems.item.image ?? "")
VStack(alignment: .leading, spacing: 8) {
Text(cartItems.item.name ?? "")
Text(getPrice(value: cartItems.item.price ?? 0))
}
Spacer()
VStack {
Spacer()
HStack{
Button(action: {
if cartItems.quantity > 0 {
cartItems.quantity -= 1
productOrder.decrement(cartItem: &cartItems)
productOrder.calculateTotalPrice()
}
}, label: {
imageMinusPlus(name: "minus")
})
Text("\(cartItems.quantity)") // This Text should be updated.
Button(action: {
cartItems.quantity += 1
productOrder.increment(cartItem: &cartItems)
productOrder.calculateTotalPrice()
}, label: {
imageMinusPlus(name: "plus")
})
}
}
}
}
}
PS: I deleted the styling to make the code shorter.
Thankyou in advance

Besides passing "ProductPresenter" instance around as "environment" object (which you are handling correctly) you need to declare your properties as #Published, as in this case for "totalQuantity", which now updates the total overall quantity.
class ProductPresenter: ObservableObject {
#Published var cartItems: [CartItems] = [
CartItems(item: CartItems.Item(image: "system.car", name: "My item", price: 10, idItem: UUID()), quantity: 3)
]
#Published var totalQuantity = 0
}
But this doesnt solve this line:
Text("\(cartItems.quantity)") // This Text should be updated.
for following reasons:
your model (CartItems) has to be a class type and conform to
ObservableObject protocol
your property has to be set using #Published.

I think productOrder need to be defined as ObservableObject with #Published properties and int this case it need to be class not a structure, for example:
import SwiftUI
import Combine
final class ProductOder: ObservableObject {
#Published var cartItems: [CartItem] = []
}

Related

Missing parameter but need parameter to be changed based on user input

This file is for the main structure of the application. This is where the error is coming from which is "Missing argument for parameter 'numberOfDoors' in call". This is because it wants me to add
ContentView(numberOfDoors: <#Int#>)
but im having trouble finding out how I can get what the user chooses to be the int instead of me putting a number in there statically.
import SwiftUI
#main
struct NumOfDoorsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
This is my project file.
import SwiftUI
struct ContentView: View {
#State var numberOfDoors: Int
#State var multiOptions: Array<String>
init(numberOfDoors: Int) {
self.numberOfDoors = numberOfDoors
self.multiOptions = [String](repeating: "", count: numberOfDoors)
}
var body: some View {
NavigationView {
Form{
Section {
Picker("Number of doors", selection: $numberOfDoors) {
ForEach(1 ..< 64) {
Text("\($0) doors")
}
}
ForEach(multiOptions.indices, id: \.self) { index in
TextField("Enter your option...", text: $multiOptions[index])
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
Section {
Text("\(numberOfDoors + 1)")
}
}
}
}
}
One of the most important parts of SwiftUI programming is creating an appropriate model for your views.
#State properties are OK for local, often independent properties, but they typically aren't a good solution for your main data model or where a model needs to be manipulated by the user.
In your case you want the size of the array to change based on the selected number of doors, so you need somewhere for that procedural code to live; The model is that place.
Here is a simple model object you can use
class DoorModel: ObservableObject {
#Published var numberOfDoors: Int {
didSet {
self.adjustArray(newSize: numberOfDoors)
}
}
#Published var doors:[String]
init(numberOfDoors: Int) {
self.numberOfDoors = numberOfDoors
self.doors = [String](repeating: "", count: numberOfDoors)
}
private func adjustArray(newSize: Int) {
let delta = newSize - doors.count
print("new size = \(newSize) Delta = \(delta)")
if delta > 0 {
doors.append(contentsOf:[String](repeating: "", count: delta))
} else if delta < 0 {
doors.removeLast(-delta)
}
}
}
Note that you still need to supply a starting number of doors via the initialiser for your model. Whenever that value changes, the didSet property observer calls a function to add or remove elements from the end of the array.
You can use this model in your view with an #StateObject decorator. This ensures that a single instance is created and reused as your view is redrawn
struct ContentView: View {
#StateObject var model = DoorModel(numberOfDoors: 1)
var body: some View {
NavigationView {
Form{
Section {
Picker("Number of doors", selection: $model.numberOfDoors) {
ForEach(1 ..< 64) { index in
Text("\(index) doors").tag(index)
}
}
ForEach($model.doors.indices, id: \.self) { index in
TextField("Enter your option...", text: $model.doors[index])
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
}
}
}
I added the .tag modifier to ensure that the picker works correctly with your 1-based list; By default the tag will be 0 based.
There are, to my opinion, two issues with your code.
Don't initialize ContentView with a parameter. Especially, since you want to start with a value of 0 doors and let the user choose how many doors.
You can't initialize an #State value directly like: self.numberOfDoors = numberOfDoors.
I suggest the following code, it worked for me.
import SwiftUI
struct ContentView: View {
#State private var numberOfDoors: Int
#State private var multiOptions: Array<String>
init() {
_numberOfDoors = State(wrappedValue: 0)
_multiOptions = State(wrappedValue: [String](repeating: "", count: 1))
}
var body: some View {
NavigationView {
Form{
Section {
Picker("Number of doors", selection: $numberOfDoors) {
ForEach(1 ..< 64) {
Text("\($0) doors")
}
}
ForEach(multiOptions.indices, id: \.self) { index in
TextField("Enter your option...", text: $multiOptions[index])
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
Section {
Text("\(numberOfDoors + 1)")
}
}
}
}
}
The issue is, that when you initialize ContentView, the #State or #StateObject variables don't yet exist. You first need to initialize the View, before you can assign an #State variable.
An #State variable is wrapped in a property wrapper, where the original variable at initialization is _variable. Therefore, you need to initialize an #State variable like this:
_myVariable = State(wrappedValue: myContent)
If you don't so it is like this, you get the error as you mentioned.

SwiftUI NavigationView pop itself when a datasource is finished loading

Let say you are loading some data from network when you are at screen A that it takes sometime. While you are waiting you can navigate to other screens by using NavigationLink. So, at the moment you are at the screen B then the data from network is finished loading and send value back to datasource in screen A. The NavigationView pop itself automatically so you back to screen A by unintentionally. Do you have any idea? Thanks.
Example
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
NavigationView {
List(viewModel.dataSource, id: \.self) { item in
NavigationLink(destination: Text("\(item)")) {
Text("\(item)")
.padding()
}
}
}
}
}
class ViewModel: ObservableObject {
#Published private(set) var dataSource = [1, 2, 3, 4, 5]
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { // simulate calling webservice
self.dataSource[0] = 99
}
}
}
This happens because you're specifying id as item itself, and when list updated there's no original item anymore, so it closes
If you just wanna modify items without adding/removing/reordering, you can make index your item id:
NavigationView {
List(viewModel.dataSource.indices, id: \.self) { i in
let item = viewModel.dataSource[i]
NavigationLink(destination: Text("\(item)")) {
Text("\(item)")
.padding()
}
}
}
But with more complex data you need to have your items Identifiable with unique ids, and you won't have such problem. Check out this example:
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
NavigationView {
List(viewModel.dataSource) { item in
NavigationLink(destination: Text("\(item.value)")) {
Text("\(item.value)")
.padding()
}
}
}
}
}
class ViewModel: ObservableObject {
#Published private(set) var dataSource: [Item] = [1, 2, 3, 4, 5]
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [self] in // simulate calling webservice
// you're modifying value but id stays the same
self.dataSource[0].value = 99
}
}
}
struct Item: Identifiable, ExpressibleByIntegerLiteral {
let id = UUID()
var value: Int
init(integerLiteral value: IntegerLiteralType) {
self.value = value
}
}

How to update ParentView after updating SubView #ObservedObject SwiftUI

This is a simple example for my case.
I have a #ObservedObject viewModel (Object1), pass a property (Object2) to another view (View2) . Change value in View 2, and when i go back to View 1, i wish the value is updated too. What is the best solution?
In this Example, when i press the blue number, i wish black number is also updated.
Actually I don't know why do the black number is updated after pressing button "Show".
I would really appreciate if you could help me. Thanks.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var object1: Object1 = Object1(ob: Object2(n: 0))
#State var isShow = false
var body: some View {
NavigationView {
VStack {
Text("\(object1.object2.n)")
//NavigationLink(destination: View2(object2: object1.object2)) {
// Text("Go to view 2")
//}
View2(object2: object1.object2)
Button {
isShow = true
} label: {
Text("Show")
}.alert(isPresented: $isShow, content: {
Alert(title: Text("\(object1.object2.n)"))
})
}
}
}
}
struct View2: View {
#ObservedObject var object2: Object2
var body: some View {
Button {
object2.n += 1
} label: {
Text("\(object2.n)")
}
}
}
class Object1: ObservableObject {
#Published var object2: Object2
init(ob: Object2) {
self.object2 = ob
}
}
class Object2: ObservableObject {
#Published var n: Int = 0
init(n: Int) {
self.n = n
}
}
Here is possible solution:
var body: some View {
NavigationView {
VStack {
Text("\(object1.object2.n)")
.onChange(of: object1.object2.n) { _ in
object1.objectWillChange.send()
}
// .. other code
Alternate is to move every object2 dependent part into separated subview observed object2 explicitly.

How does one use a Stepper to update a value in the model in SwiftUI?

I am trying to increment or decrement a value using Stepper, however I cannot fathom how to take the new value and update my model.
I've also tried to use the onIncrement, onDecrement version, but here there appears to be no option to disable the + or - when the range has been reached.
Here's my example code:
import SwiftUI
struct ContentView: View {
var body: some View {
MainView()
}
}
struct MainView: View {
#ObservedObject var updateAge = UpdateAge()
#State private var showAgeEditor = false
#State var age: Int = 0
var body: some View {
VStack {
/// Show the current age.
Text("Age \(age)")
Image(systemName: "keyboard")
.onTapGesture {
self.showAgeEditor = true
}
/// Present the sheet to update the age.
.sheet(isPresented: $showAgeEditor) {
SheetView(showAgeEditor: self.$showAgeEditor)
.environmentObject(self.updateAge)
.frame(minWidth: 300, minHeight: 400)
}
}
/// Calls the viewModel (UpdateAge) to fetch the age from the model.
.onAppear(perform: {self.age = self.updateAge.withAge})
}
}
struct SheetView: View {
#EnvironmentObject var updateAge: UpdateAge
#Binding var showAgeEditor: Bool
/// Steppers state.
#State private var age: Int = 18
let maxAge = 21
let minAge = 18
var body: some View {
return VStack {
Text("Age of person = \(age)")
/// When I increment or Decrement the stepper I want the Age to increase or decrease.
Stepper<Text>(value: $age, in: minAge...maxAge, step: 1) {
/// And when it does, to store the value in the model via the viewModel.
/// However I appear to have created an infinite loop. Just uncomment the line below.
// self.updateAge.with(age: age)
/// Then display the value in the label.
return Text("\(age)")
}
}.onAppear(perform: {self.age = self.updateAge.withAge})
}
}
class UpdateAge: ObservableObject {
#Published var model = Model()
func with(age value: Int) {
print("Value passed \(value)")
self.model.setDrinkingAge(with: value)
}
var withAge: Int {
get { model.drinkingAge }
}
}
struct Model {
var drinkingAge: Int = 18
mutating func setDrinkingAge(with value: Int) {
drinkingAge = value
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can bind to model directly via view model projected value, like below (so local state age is redundant)
Stepper(value: $updateAge.model.drinkingAge, in: minAge...maxAge, step: 1) {
Text("\(updateAge.model.drinkingAge)")
}

SwiftUI View not updating based on #ObservedObject

In the following code, an observed object is updated but the View that observes it is not. Any idea why?
The code presents on the screen 10 numbers (0..<10) and a button. Whenever the button is pressed, it randomly picks one of the 10 numbers and flips its visibility (visible→hidden or vice versa).
The print statement shows that the button is updating the numbers, but the View does not update accordingly. I know that updating a value in an array does not change the array value itself, so I use a manual objectWillChange.send() call. I would have thought that should trigger the update, but the screen never changes.
Any idea? I'd be interested in a solution using NumberLine as a class, or as a struct, or using no NumberLine type at all and instead rather just using an array variable within the ContentView struct.
Here's the code:
import SwiftUI
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(0 ..< numberLine.visible.count) { number in
if self.numberLine.visible[number] {
Text(String(number)).font(.title).padding(5)
}
}
}.padding()
Button(action: {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.objectWillChange.send()
self.numberLine.visible[index].toggle()
print("\(index) now \(self.numberLine.visible[index] ? "shown" : "hidden")")
}) {
Text("Change")
}.padding()
}
}
}
class NumberLine: ObservableObject {
var visible: [Bool] = Array(repeatElement(true, count: 10))
}
With #ObservedObject everything's fine... let's analyse...
Iteration 1:
Take your code without changes and add just the following line (shows as text current state of visible array)
VStack { // << right below this
Text("\(numberLine.visible.reduce(into: "") { $0 += $1 ? "Y" : "N"} )")
and run, and you see that Text is updated so observable object works
Iteration 2:
Remove self.numberLine.objectWillChange.send() and use instead default #Published pattern in view model
class NumberLinex: ObservableObject {
#Published var visible: [Bool] = Array(repeatElement(true, count: 10))
}
run and you see that update works the same as on 1st demo above.
*But... main numbers in ForEach still not updated... yes, because problem in ForEach - you used constructor with Range that generates constant view's group by-design (that documented!).
!! That is the reason - you need dynamic ForEach, but for that model needs to be changed.
Iteration 3 - Final:
Dynamic ForEach constructor requires that iterating data elements be identifiable, so we need struct as model and updated view model.
Here is final solution & demo (tested with Xcode 11.4 / iOS 13.4)
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(numberLine.visible, id: \.id) { number in
Group {
if number.visible {
Text(String(number.id)).font(.title).padding(5)
}
}
}
}.padding()
Button("Change") {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.visible[index].visible.toggle()
}.padding()
}
}
}
class NumberLine: ObservableObject {
#Published var visible: [NumberItem] = (0..<10).map { NumberItem(id: $0) }
}
struct NumberItem {
let id: Int
var visible = true
}
I faced the same issue.
For me, replacing #ObservedObject with #StateObject worked.
Using your insight, #Asperi, that the problem is with the ForEach and not with the #ObservableObject functionality, here's a small modification to the original that does the trick:
import SwiftUI
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(Array(0..<10).filter {numberLine.visible[$0]}, id: \.self) { number in
Text(String(number)).font(.title).padding(5)
}
}.padding()
Button(action: {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.visible[index].toggle()
}) {
Text("Change")
}.padding()
}
}
}
class NumberLine: ObservableObject {
#Published var visible: [Bool] = Array(repeatElement(true, count: 10))
}
There is nothing Wrong with observed object, you should use #Published in use of observed object, but my code works without it as well. And also I updated your logic in your code.
import SwiftUI
struct ContentView: View {
#ObservedObject var model = NumberLineModel()
#State private var lastIndex: Int?
var body: some View {
VStack(spacing: 30.0) {
HStack {
ForEach(0..<model.array.count) { number in
if model.array[number] {
Text(String(number)).padding(5)
}
}
}
.font(.title).statusBar(hidden: true)
Group {
if let unwrappedValue: Int = lastIndex { Text("Now the number " + unwrappedValue.description + " is hidden!") }
else { Text("All numbers are visible!") }
}
.foregroundColor(Color.red)
.font(Font.headline)
Button(action: {
if let unwrappedIndex: Int = lastIndex { model.array[unwrappedIndex] = true }
let newIndex: Int = Int.random(in: 0...9)
model.array[newIndex] = false
lastIndex = newIndex
}) { Text("shuffle") }
}
}
}
class NumberLineModel: ObservableObject {
var array: [Bool] = Array(repeatElement(true, count: 10))
}
The problem is with the function, do not forget to add id: \.self in your ForEach function, and make your Model Hashable, Identifiable.

Resources