Hello everyone. I am new in TCA and exploring it by working on a dummy banking app. I am facing an issue where when I am navigation from screen A -> B -> C, then as soon as API call in C is completed it pops back to B. Now I've searched a lot and possible problems that can cause that issue were that ViewStore was being re-rendered whenever my child state was changing. To stop this, it was advised to observe only relevant states. But after doing all these steps, I am still facing the same issue. Below is the code for Parent View (PaymentDetailView), and Child View (AccountAndTransactionView). Any help would be appreciated.
struct PaymentsDetailView : View {
let store: StoreOf<PaymentDetailCore>
@State var degree = 0.0
struct ViewState: Equatable {
var rotationDegree: CGFloat
var payee: PayeeDetails
init(state: PaymentDetailCore.State) {
self.rotationDegree = state.rotationDegree
self.payee = state.payee
}
}
var body: some View {
WithViewStore(self.store, observe: ViewState.init) { viewstore in
VStack {
VStack(alignment: .leading) {
HStack {
Circle()
.frame(height: 58)
.padding(.top, 22)
Spacer()
EditButton(store: self.store.scope(state: \.editButtonState, action: PaymentDetailCore.Action.editButtonAction))
.padding(.trailing, 22)
}
.padding(.leading, 15)
payeeDetailsView(viewstore)
Spacer()
}
.background(LinearGradient(gradient: Gradient(colors: [Color(uiColor: .skyBlue), Color(uiColor: .heliotrope)]), startPoint: .topLeading, endPoint: .bottomTrailing))
.cornerRadius(42)
.frame(height: 240)
.padding(.horizontal, 15)
.navigationTitle("Payee Details")
.padding(.top, 38)
.rotation3DEffect(.degrees(degree), axis: (x: 0, y: 1, z: 0))
Spacer()
NavigationLink {
AccountAndTransactionView(store: self.store.scope(state: \.accountAndTransactionState, action: PaymentDetailCore.Action.accountAndTransactionAction))
} label: {
Text("Next")
.modifier(PrimaryButton())
.padding(.bottom, 15)
}
}
.onChange(of: viewstore.rotationDegree) { degrees in
withAnimation(.easeOut(duration: 0.5)) {
self.degree = degrees
}
}
}
}
}
extension PaymentsDetailView {
@ViewBuilder
private func payeeDetailsView(_ viewstore: ViewStore<ViewState, PaymentDetailCore.Action>) -> some View {
VStack(alignment: .leading) {
Text(viewstore.payee.name)
.font(.title)
.fontWeight(.semibold)
.padding(.top, 15)
.padding(.bottom, 1)
TwoLabelView(firstText: "Account Number", secondText: viewstore.payee.accountNumber)
.padding(.bottom, 1)
TwoLabelView(firstText: "Bank", secondText: viewstore.payee.bankName)
}
.padding(.leading, 15)
}
}
struct PaymentsDetailView_Previews: PreviewProvider {
static var previews: some View {
PaymentsDetailView(store: Store(initialState: PaymentDetailCore.State(id: UUID()), reducer: PaymentDetailCore()))
}
}
struct PaymentDetailCore: ReducerProtocol {
struct State: Equatable, Identifiable {
var id: UUID
var payee: PayeeDetails = .init(id: "", name: "", accountNumber: "", bankName: "")
var editButtonState = EditButtonCore.State()
var shouldAnimate: Bool = false
var rotationDegree: CGFloat = 0.0
var accountAndTransactionState = AccountAndTransactionCore.State()
}
enum Action: Equatable {
case editButtonAction(EditButtonCore.Action)
case accountAndTransactionAction(AccountAndTransactionCore.Action)
}
var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case .editButtonAction(.didTapEditButton):
state.rotationDegree += 180
return .none
case .accountAndTransactionAction:
return .none
}
}
Scope(state: \.editButtonState, action: /Action.editButtonAction) {
EditButtonCore()
}
Scope(state: \.accountAndTransactionState, action: /Action.accountAndTransactionAction) {
AccountAndTransactionCore()
}
._printChanges()
}
}
struct AccountAndTransactionView : View {
let store: StoreOf<AccountAndTransactionCore>
var body: some View {
WithViewStore(self.store) { viewstore in
Group {
if viewstore.dataLoadingStatus == .loading {
ProgressView()
.frame(width: 100, height: 100)
} else {
VStack {
IfLetStore(self.store.scope(state: \.dropDownState, action: AccountAndTransactionCore.Action.dropDownAction)) {
DropDown(store: $0)
}
}
}
}
.task {
viewstore.send(.fetchPurposes)
}
}
}
}
struct AccountAndTransactionView_Previews: PreviewProvider {
static var previews: some View {
AccountAndTransactionView(store: Store(initialState: AccountAndTransactionCore.State(), reducer: AccountAndTransactionCore()))
}
}
struct AccountAndTransactionCore: ReducerProtocol {
struct State: Equatable {
var dropDownState : DropDownCore.State?
var dataLoadingStatus = DataLoadingStatus.notLoading
var dummyState = 0
}
enum Action: Equatable {
case dropDownAction(DropDownCore.Action)
case fetchPurposes
case fetchPurposesResponse(TaskResult<SelectPurposeAndAccounts>)
}
@Dependency(\.paymentClient) var paymentClient
var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case .dropDownAction:
return .none
case .fetchPurposes:
if state.dataLoadingStatus == .success || state.dataLoadingStatus == .loading {
return .none
}
state.dataLoadingStatus = .loading
return .task {
.fetchPurposesResponse(
await TaskResult {
try await self.paymentClient.fetchPurposesAndAccounts(.init(userId: "3213"))
}
)
}
case .fetchPurposesResponse(.success(let purposeAndAccountsList)):
state.dropDownState = DropDownCore.State(names: purposeAndAccountsList.purposeList)
state.dataLoadingStatus = .success
return .none
case .fetchPurposesResponse(.failure(let error)):
state.dataLoadingStatus = .error
print(error.localizedDescription)
return .none
}
}
.ifLet(\.dropDownState, action: /Action.dropDownAction) {
DropDownCore()
}
._printChanges()
}
}