@Observable not working exactly as required

Viewed 72

I am stuck in a problem while using @Observable. I am using it to format a textfield for credit card. I created a textfield formatter and a custom textfield using a lengthy code that I have uploaded in a gist: https://gist.github.com/amrit42087/98750b2e22b299368b07ed6e78d530a8

The below code is using for creating the UI:

import SwiftUI

  struct Person: Identifiable {
  var id = UUID()
  var name: String
  var age: Int
}

struct View1: View {
  @ObservedObject var personVM = PersonViewModel()

  var body: some View {

    NavigationView {
        HStack {
            Spacer()
            VStack {
                CreditCardTextfield()
                    .offset(y: 100)
                Spacer()
            }
            Spacer()

        }
        .onAppear {
            self.personVM.addPerson()
        }
    }
}
}

struct CreditCardTextfield: View {
   @ObservedObject var cardFormatter = TextFieldFormatter(limit: 19, type: .cardNumber)
   @State var isFirstResponder = false

   var body: some View {
        CustomTextField(text: $cardFormatter.text, isFirstResponder: $isFirstResponder, placeholder: "XXXX XXXX XXXX XXXX", font: .monospacedSystemFont(ofSize: 25, weight: .bold), keyboardType: .numberPad)
            .frame(height: 40)
  }
}

class PersonViewModel: ObservableObject {

@Published var people: [Person] = []

func addPerson() {
    people.append(Person(name: "Test", age: (0...30).randomElement() ?? 0))
}
}

The problem is that formatter does not work if I use this line self.personVM.addPerson() in onAppear(). Commenting out this line eradicates the problem. I just need to understand the reason behind this issue. Any help would be more than appreciated.

2 Answers

Try to make CreditCardTextfield equatable, so it won't recreate on personVM update.

struct CreditCardTextfield: View, Equatable {
    static func == (lhs: CreditCardTextfield, rhs: CreditCardTextfield) -> Bool {
        true
    }

    // ... other code here

and in usage place

VStack {
    CreditCardTextfield().equatable()
        .offset(y: 100)
    Spacer()
}

Views are copied and created a lot -- every time the view changes, you are constructing a new version. So you don't want code like this in a View

@ObservedObject var personVM = PersonViewModel()

That will create a brand new view model every time the view changes. So, adding a person to the view, adds it to the view model and then makes a new view with an empty view model.

You want to construct it outside of the view and pass it into the init. The future View values will copy it from the property (not recall that init). So, use:

@ObservedObject var personVM: PersonViewModel

And make View1 like this

View1(personVM: personViewModel) // personViewModel was constructed beforehand

Be careful constructing it there too -- because that line is in a View struct as well, and will keep constructing the view model.

Related