Home > Back-end >  How do I offset each View for it's own when generated in a foreach loop SwiftUI
How do I offset each View for it's own when generated in a foreach loop SwiftUI

Time:08-12

I am creating a Cardview stored with user data for each user in the usersList. Now I want to offset each card for itself by pressing on either button. If I press the button now I will offset every card created by the foreach. How can I make the button only affect the first card? It is probably a better Idea to offset the card in the CardView itself, like I did with the Draggesture and not in the ContentView

struct ContentView: View {
    @State var usersList: [User] = []
    @State var fetchingComplete: Bool = false
    @State var offset = CGSize.zero
    @State var buttonPushed: Bool = false
    
    var body: some View {
            VStack {
                NavBar().onAppear()  {
                    fetchUsers()
                }
                if fetchingComplete {
                    ZStack {
                        ForEach(usersList, id: \.id) { user in
                            CardView(user: user).offset(x: offset.width, y: offset.height * 0.4)
                                .rotationEffect(.degrees(Double(offset.width / 40)))
                                .animation(.spring(), value: buttonPushed)
                        }
                    }

                }
                ButtonsBar(offset: $offset, buttonPushed: $buttonPushed)
                }
        }
        
        }

 public func fetchUsers() -> [User]{
        FirebaseManager.shared.firestore.collection("users").getDocuments { snapshot, error in
            if let error = error {
                print("Failed to fetch users: ", error)
                return
            }
            for document in snapshot!.documents {
                let user = User(dictionary: document.data())
                usersList.append(user)
            }
            fetchingComplete.toggle()
        }
        return usersList
    }
}

struct ButtonsBar:View {
    @Binding var offset: CGSize
    @Binding var buttonPushed: Bool
    
    var body: some View {
        HStack {
            Button {
                print("perform dislike")
                offset = CGSize(width: -500, height: 0)
                buttonPushed.toggle()
            } label: {
                Image("dismiss_circle")
            }.frame(width: 65)
          
            Button {
                print("perform like")
                offset = CGSize(width: 500, height: 0)
                buttonPushed.toggle()
            } label: {
                Image("like_circle")
            }.frame(width: 65)
        }
    }
}

struct CardView: View {
    @State private var offset = CGSize.zero

    var user: User
    var body: some View {
        VStack{
            Image("lady")
                .resizable()
        }.cornerRadius(12)
            .frame(width: 310, height: 392)
            .scaledToFit()
            .overlay(ImageOverlay(name: self.user.name ?? "", age: self.user.age ?? 0, profession: self.user.profession ?? ""), alignment: .bottomLeading)
            .offset(x: offset.width, y: offset.height * 0.4)
            .rotationEffect(.degrees(Double(offset.width / 40)))
            .gesture(
            DragGesture()
                .onChanged { gesture in
                    offset = gesture.translation
                } .onEnded { _ in
                    withAnimation {
                        swipeCard(width: offset.width)
                    }
                }
            )
    }

    func swipeCard(width: CGFloat) {
        switch width {
        case -500...(-150):
            offset = CGSize(width: -500, height: 0)
        case 150...500:
            offset = CGSize(width: 500, height: 0)
        default:
            offset = .zero
        }
    }
}

CodePudding user response:

Right now, your current design will be unable to achieve what you're saying. How you have designed offset makes it so there's only the single source of truth being a single var offset. Thus, there's no way to give an offset to each card uniquely. Likewise, your design of a single ButtonBar will also be unable to achieve the goal of modifying each unique entry. How would this ButtonBar as it is choose any particular CardView? I know how to implement this, but I think you can figure it out once I explain more on how to uniquely associate values to each CardView.

I know you asked for how to just indent the first card only, but I don't think the easiest answer will be your desired outcome.

What you should do is that you have to track an offset and/or a buttonPressed for EACH user in your UserList. The easiest way is to create a CardViewModel so at the top of your CardView add

@StateObject var cardVM = CardViewModel()

and for the CardViewModel,

class CardViewModel : ObservableObject {
    @Published var offset = CGSize.zero
    @Published var buttonPushed: Bool = false
}

Now we have a unique offset and buttonPushed for each CardView. From here I would remove the modifiers

.offset(x: offset.width, y: offset.height * 0.4)
.rotationEffect(.degrees(Double(offset.width / 40)))
.animation(.spring(), value: buttonPushed)

and inside the CardView place your ButtonBar like so

struct CardView: View {
    @StateObject var cardVM = CardViewModel()

    var user: User
    var body: some View {
        VStack{
            ButtonBar(offset: $cardVM.offset, buttonPushed: $cardVM.buttomPushed)
            Image("lady")
                .resizable()
        }
        .cornerRadius(12)
        .frame(width: 310, height: 392)
        .scaledToFit()
        .overlay(ImageOverlay(name: self.user.name ?? "", age: self.user.age ?? 0, profession: self.user.profession ?? ""), alignment: .bottomLeading)
        .offset(x: offset.width, y: offset.height * 0.4)
        .rotationEffect(.degrees(Double(offset.width / 40)))
        .gesture(
            DragGesture()
                .onChanged { gesture in
                    offset = gesture.translation
                }
                .onEnded { _ in
                    withAnimation {
                        swipeCard(width: offset.width)
                    }
                }
            )
// the offset and effects based on button push
        .offset(x: cardVM.offset.width, y: cardVM.offset.height * 0.4)
        .rotationEffect(.degrees(Double(cardVM.offset.width / 40)))
        .animation(.spring(), value: cardVM.buttonPushed)
    }

    func swipeCard(width: CGFloat) {
        switch width {
        case -500...(-150):
            offset = CGSize(width: -500, height: 0)
        case 150...500:
            offset = CGSize(width: 500, height: 0)
        default:
            offset = .zero
        }
    }
}

Because this isn't an MRE, I'm not able to actually test this, but in general, this should follow the design of creating a CardViewModel for each CardView which contains unique to each view variables offset and buttonPressed and placing the ButtonBar in each CardView allows us to modify the CardViewModel uniquely too.

  • Related