Using Variables in Viewbuilder (Swift)

572 Views Asked by At

I'm new to swift and Viewbuilder and I am trying to use variables in the Viewbuilder correctly. The objective of this code is to reveal the answer to a question using an animation. The question is presented on a card and when the button on the card is tapped the answer is revealed. It all works well if I use one card however when I put multiple cards in a scrollView and tap the button, all of the answers are presented at the same time. I believe this is because I am using one show variable for all the cards (therefore it expands all the cards when at once). I need to have a show variable for each card but am struggling to implement this. How would I go about this?

The code for my View is:

struct Home: View {
// MARK: Animation Properties

@State var expandTheCard: Bool = false

@State var bottomLiquidView: AnimationView = AnimationView(name: "LiquidWave", bundle: .main)
@State var topLiquidView: AnimationView = AnimationView(name: "LiquidWave", bundle: .main)

// Avoiding Multitapping
@State var isfinished: Bool = false

var body: some View {
    
    NavigationView {
        
        ScrollView {
            
            LazyVStack(spacing: 200) {
                
                // MARK: Animated Liquid Transition Cards
                LiquidCard(title: "What Is Hello In French?", subTitle: "", detail: "Hello In French Is...", description: "Bonjour!"){
                    if isfinished{return}
                    isfinished = true
        
                    // Animating Lottie View with little Delay
                    DispatchQueue.main.asyncAfter(deadline: .now() + (expandTheCard ? 0 : 0.2)) {
                        // So that it will finish soon...
                        
                        bottomLiquidView.play(fromProgress: expandTheCard ? 0 : 0.45, toProgress: expandTheCard ? 0.6 : 0)
                        topLiquidView.play(fromProgress: expandTheCard ? 0 : 0.45, toProgress: expandTheCard ? 0.6 : 0){status in
                            isfinished = false
                        }
                    }
                    // Toggle Card
                    withAnimation(.interactiveSpring(response: 0.7, dampingFraction: 0.8, blendDuration: 0.8)){
                        expandTheCard.toggle()
                    }
                }
                .frame(maxHeight: .infinity)
            }
            
        }
        
    }
    
}

The code for the ViewBuilder is:

  @ViewBuilder
func LiquidCard(title: String,subTitle: String,detail: String,description: String,color: SwiftUI.Color = Color("Blue"),onExpand: @escaping ()->())->some View{

    ZStack{
        VStack(spacing: 20){
            Text(title)
                .font(.largeTitle.bold())
                .foregroundColor(.white)
            
            HStack(spacing: 10){
            
                Text(subTitle)
                    .fontWeight(.semibold)
                    .foregroundColor(.white)
            }
        }
        .padding()
        .frame(maxWidth: .infinity)
        .frame(height: expandTheCard ? 250 : 350)
        .background{
            GeometryReader{proxy in
                let size = proxy.size
                let scale = size.width / 1000
                
                RoundedRectangle(cornerRadius: 35, style: .continuous)
                    .fill(color)
                
                // To get Custom Color simply use Mask Technique
                RoundedRectangle(cornerRadius: 35, style: .continuous)
                    .fill(color)
                    .mask {
                        ResizableLottieView(lottieView: $bottomLiquidView)
                        // Scaling it to current Size
                            .scaleEffect(x: scale, y: scale, anchor: .leading)
                    }
                    .rotationEffect(.init(degrees: 180))
                    .offset(y: expandTheCard ? size.height / 1.43 : 0)
            }
        }
        // MARK: Expand Button
        .overlay(alignment: .bottom) {
            Button {
                onExpand()
            } label: {
             
                Image(systemName: "chevron.down")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: 30.0, height: 30.0)
                    .font(.title3.bold())
                    .foregroundColor(color)
                    .padding(30)
                    .background(.white,in: RoundedRectangle(cornerRadius: 20, style: .continuous))
                // Shadows
                    .shadow(color: .black.opacity(0.15), radius: 5, x: 5, y: 5)
                    .shadow(color: .black.opacity(0.15), radius: 5, x: -5, y: -5)
            }
            .padding(.bottom,-25)
        }
        .zIndex(1)
        
        // MARK: Expanded Card
        
        VStack(spacing: 20){
            Text(detail)
                .font(.largeTitle.bold())
            
            Text(description)
                .font(.title3)
                .lineLimit(3)
                .padding(.horizontal)
                .multilineTextAlignment(.center)
        }
        .foregroundColor(.white)
        .padding(.vertical,40)
        .padding(.horizontal)
        .frame(maxWidth: .infinity)
        .background{
            GeometryReader{proxy in
                let size = proxy.size
                let scale = size.width / 1000
                RoundedRectangle(cornerRadius: 35, style: .continuous)
                    .fill(color)
                
                // To get Custom Color simply use Mask Technique
                RoundedRectangle(cornerRadius: 35, style: .continuous)
                    .fill(color)
                    .mask {
                        ResizableLottieView(lottieView: $topLiquidView)
                        // Scaling it to current Size
                            .scaleEffect(x: scale, y: scale, anchor: .leading)
                    }
                    .offset(y: expandTheCard ? -size.height / 1.2 : -size.height / 1.4)
            }
        }
        .zIndex(0)
        .offset(y: expandTheCard ? 280 : 0)
    }
    .offset(y: expandTheCard ? -120 : 0)
}
1

There are 1 best solutions below

1
On

You are right in the assessment of the problem: you have only one variable that is governing all instances of LiquidCard. You should have an expandTheCard variable that is specific only to LiquidCard, not connected to the Home view.

To achieve this, a good way is to define LiquidCard as a new view, not as a @ViewBuilder func. By the way, the name is already in initial cap, which shouldn't be used for function names.

Here's how you should change your views (I will only mention here the parts that change):

  1. Remove expandTheCard from the Home view:
struct Home: View {
// MARK: Animation Properties

// Do not use this here: @State var expandTheCard: Bool = false

@State var bottomLiquidView: AnimationView = AnimationView(name: "LiquidWave", bundle: .main)
@State var topLiquidView: AnimationView = AnimationView(name: "LiquidWave", bundle: .main)

...
  1. Replace the func with a struct of type View that contains the @State var expandTheCard:
import SwiftUI

struct LiquidCard: View {
    let title: String
    let subTitle: String,
    let detail: String,
    let description: String,
    let color: Color = .blue
    let onExpand: ()->()   // Or ()->Void, depending on your code

    // Here is where you need the variable, one for each LiquidCard
    @State var expandTheCard = false

    var body: some View {
        ZStack{
            VStack(spacing: 20){

...
  1. Expand the card when you push the Button:
...
        // MARK: Expand Button
        .overlay(alignment: .bottom) {
            Button {
                expandTheCard = true    // Add this here or after onExpand(), depending on your code
                onExpand()
            } label: {
             
                Image(systemName: "chevron.down")

...
  1. You have no such variable in the Home view anymore, delete the toggle:
...
                    // Toggle Card
                    // You do not need this anymore
                    // withAnimation(.interactiveSpring(response: 0.7, dampingFraction: 0.8, blendDuration: 0.8)){
                    //    expandTheCard.toggle()
                    // }

...