David Perez
David Perez

Reputation: 21

Conditional view modifier sometimes doesn't want to update

As I am working on a study app, I'm tring to build a set of cards, that a user can swipe each individual card, in this case a view, in a foreach loop and when flipped through all of them, it resets the cards to normal stack. The program works but sometimes the stack of cards doesn't reset. Each individual card updates a variable in a viewModel which my conditional view modifier looks at, to reset the stack of cards using offset and when condition is satisfied, the card view updates, while using ".onChange" to look for the change in the viewModel to then update the variable back to original state.

I've printed each variable at each step of the way and every variable updates and I can only assume that the way I'm updating my view, using conditional view modifier, may not be the correct way to go about. Any suggestions will be appreciated.

Here is my code: The view that houses the card views with the conditional view modifier

extension View {

    @ViewBuilder func `resetCards`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
        if condition == true {
            transform(self).offset(x: 0, y: 0)
        } else {
            self
        }
    }
}

struct StudyListView: View {

    @ObservedObject var currentStudySet: HomeViewModel
    @ObservedObject var studyCards: StudyListViewModel = StudyListViewModel()
    
    @State var studyItem: StudyModel
    @State var index: Int

    var body: some View {
        ForEach(currentStudySet.allSets[index].studyItem.reversed()) { item in
            StudyCardItemView(currentCard: studyCards, card: item, count: currentStudySet.allSets[index].studyItem.count)
                .resetCards(studyCards.isDone) { view in
                    view
                }
                .onChange(of: studyCards.isDone, perform: { _ in
                    studyCards.isDone = false
                })
            }
     }
}

StudyCardItemView

struct StudyCardItemView: View {

    @StateObject var currentCard: StudyListViewModel
    @State var card: StudyItemModel
    @State var count: Int
    
    @State var offset = CGSize.zero
    
    var body: some View {
        VStack{
            VStack{
                ZStack(alignment: .center){
                    Text("\(card.itemTitle)")
                }
            }
        }
        .frame(width: 350, height: 200)
        .background(Color.white)
        .cornerRadius(10)
        .shadow(radius: 5)
        .padding(5)
        .rotationEffect(.degrees(Double(offset.width / 5)))
        .offset(x: offset.width * 5, y: 0)
        .gesture(
            DragGesture()
                .onChanged { gesture in
                    offset = gesture.translation
                }
                .onEnded{ _ in
                    if abs(offset.width) > 100 {
                        currentCard.cardsSortedThrough += 1
                        if (currentCard.cardsSortedThrough == count) {
                            currentCard.isDone = true
                            currentCard.cardsSortedThrough = 0
                        }
                    } else {
                        offset = .zero
                    }
                }
        )
    }
}

HomeViewModel

class HomeViewModel: ObservableObject {
    @Published var studySet: StudyModel = StudyModel()
    @Published var allSets: [StudyModel] = [StudyModel()]
}

I initialize allSets with one StudyModel() to see it in the preview

StudyListViewModel

class StudyListViewModel: ObservableObject {
    @Published var cardsSortedThrough: Int = 0
    @Published var isDone: Bool = false
}

StudyModel

import SwiftUI

struct StudyModel: Hashable{
    var title: String = ""
    var days = ["One day", "Two days", "Three days", "Four days", "Five days", "Six days", "Seven days"]
    var studyGoals = "One day"
    
    var studyItem: [StudyItemModel] = []
}

Lastly, StudyItemModel

struct StudyItemModel: Hashable, Identifiable{
    let id = UUID()
    var itemTitle: String = ""
    var itemDescription: String = ""
}

Once again, any help would be appreciated, thanks in advance!

Upvotes: 0

Views: 586

Answers (1)

David Perez
David Perez

Reputation: 21

I just found a fix and I put .onChange at the end for StudyCardItemView. Basically, the onChange helps the view scan for a change in currentCard.isDone variable every time it was called in the foreach loop and updates offset individuality. This made my conditional view modifier obsolete and just use the onChange to check for the condition.

I still used onChange outside the view with the foreach loop, just to set currentCard.isDone variable false because the variable will be set after all array elements are iterator through.

The updated code: StudyCardItemView

struct StudyCardItemView: View {

    @StateObject var currentCard: StudyListViewModel
    @State var card: StudyItemModel
    @State var count: Int
    
    @State var offset = CGSize.zero
    
    var body: some View {
        VStack{
            VStack{
                ZStack(alignment: .center){
                    Text("\(card.itemTitle)")
                }
            }
        }
        .frame(width: 350, height: 200)
        .background(Color.white)
        .cornerRadius(10)
        .shadow(radius: 5)
        .padding(5)
        .rotationEffect(.degrees(Double(offset.width / 5)))
        .offset(x: offset.width * 5, y: 0)
        .gesture(
            DragGesture()
                .onChanged { gesture in
                    offset = gesture.translation
                }
                .onEnded{ _ in
                    if abs(offset.width) > 100 {
                        currentCard.cardsSortedThrough += 1
                        if (currentCard.cardsSortedThrough == count) {
                            currentCard.isDone = true
                            currentCard.cardsSortedThrough = 0
                        }
                    } else {
                        offset = .zero
                    }
                }
        )
        .onChange(of: currentCard.isDone, perform: {_ in 
           offset = .zero
        })
    }
}

StudyListView

struct StudyListView: View {

    @ObservedObject var currentStudySet: HomeViewModel
    @ObservedObject var studyCards: StudyListViewModel = StudyListViewModel()
    
    @State var studyItem: StudyModel
    @State var index: Int

    var body: some View {
        ForEach(currentStudySet.allSets[index].studyItem.reversed()) { item in
            StudyCardItemView(currentCard: studyCards, card: item, count: 
            currentStudySet.allSets[index].studyItem.count)
                .onChange(of: studyCards.isDone, perform: { _ in
                    studyCards.isDone = false
                })
            }
     }
}

Hope this helps anyone in the future!

Upvotes: 1

Related Questions