2

I am wondering how I can animate the content size of a ViewBuilder view. I have this:

struct CardView<Content>: View where Content: View {
    
    private let content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        VStack(spacing: 0) {
            content
                .padding(16)
        }
        .background(.white)
        .cornerRadius(14)
        .shadow(color: .black.opacity(0.07), radius: 12, x: 0, y: 2)
    }
}

I would like to animate any size changes to content, but I can't find a nice way of doing this. I found two ways that work:

  • Using animation(.linear) in CardView works, but is deprecated and discouraged since I have no value to attach the animation to.
  • Using withAnimation inside content when changing the content works, too, but I would like to encapsulate this behaviour in CardView. CardView is heavily reused and doing it in content is easy to forget and also not where this behaviour belongs in my opinion.

I also tried using GeometryReader but could not find a good way of doing it.

BlackWolf
  • 5,239
  • 5
  • 33
  • 60
  • Please add also a demo of view using this CardView including sizes you want to animate, `cause it is not very clear what exactly here should be animatable. – Asperi May 23 '22 at 17:58
  • It is possible but you already made your frame for maxWidth = .infinity, So may your question corrected to height or you should remove the frame modifier. – ios coder May 23 '22 at 17:59
  • @Asperi I will add a demo for clarification as soon as I can, but I'm not sure what is unclear here. The size of content changes, and that should be animated by enlarging or shrinking the CardView to fit the new size. – BlackWolf May 24 '22 at 07:13
  • @swiftPunk I removed it, you are correct, but I think the general approach/question is the same regardless if you want to animate one or two dimensions. – BlackWolf May 24 '22 at 07:13

2 Answers2

0

Here is an approach for you:

You may take look at this link as well:

How to replace deprecated .animation() in SwiftUI?


struct ContentView: View {

    @State private var cardSize: CGSize = CGSize(width: 150, height: 200)
    
    var body: some View {
        
        VStack {

            CardView(content: {
                
                Color.red
                    .overlay(Image(systemName: "dollarsign.circle").resizable().scaledToFit().padding())
                    .onTapGesture {
                        cardSize = CGSize(width: cardSize.width + 50, height: cardSize.height + 50)
                    }
                
            }, cardSize: cardSize)
  
        }

    }
}

struct CardView<Content>: View where Content: View {
    
    let content: Content
    let cardSize: CGSize
    
    init(@ViewBuilder content: () -> Content, cardSize: CGSize) {
        self.content = content()
        self.cardSize = cardSize
    }

    var body: some View {
        
        content
            .frame(width: cardSize.width, height: cardSize.height)
            .cornerRadius(14)
            .padding(16)
            .background(.white)
            .shadow(color: .black.opacity(0.07), radius: 12, x: 0, y: 2)
            .animation(.easeInOut, value: cardSize)
    }
}
ios coder
  • 1
  • 4
  • 31
  • 91
  • Thank you, but I don't think hardcoding the size is a reliable or desired approach. The whole idea of the ViewBuilder is to put in "some" content and make it work. Also this will not work for a lot of scenarios, like when the user enabled accessibility font sizes, etc. – BlackWolf May 24 '22 at 07:12
  • I didn’t mean to hard coding the size! It is just a simple example of how you can animate size change, the part that sends size to the custom view builder can be coded in the way you like, there is no other way than letting SwiftUI know the current size and incoming new size like I did. But no problem if you can’t use this answer. – ios coder May 24 '22 at 07:47
0

You might find this useful.

It uses a looping animation and a user gesture for add size and resting.

struct PilotTestPage: View {
    
    @State private var cardSize = CGSize(width: 150, height: 200)

    var body: some View {
        return ZStack {
            Color.yellow
                        
            CardView() {
                Color.clear
                    .overlay(
                        Image(systemName: "dollarsign.circle")
                            .resizable()
                            .scaledToFit()
                            .padding()
                    )
            }
            .frame(
                width: cardSize.width
                ,height: cardSize.height
            )
            .onTapGesture {
                withAnimation {
                    cardSize = CGSize(
                        width: cardSize.width + 50
                        ,height: cardSize.height + 50
                    )
                }
            }
            
            RoundedRectangle(cornerRadius: 12, style: .continuous)
                .fill(.red)
                .frame(
                    width: 200
                    ,height: 44
                )
                .offset(y: -300)
                .onTapGesture {
                    withAnimation {
                        cardSize = CGSize(
                            width: 150
                            ,height: 200
                        )
                    }
                }

        }
        .ignoresSafeArea()
    }
    
    struct CardView<Content>: View where Content: View {
        let content: Content
        
        init(
            @ViewBuilder content: () -> Content
        ) {
            self.content = content()
        }
        
        @State private var isAtStart = true
        
        var body: some View {
            ZStack {
                content
                    .background(
                        RoundedRectangle(cornerRadius: 12)
                            .fill(.white)
                            .shadow(
                                color: .black.opacity(0.25)
                                ,radius: 12
                                ,x: 0
                                ,y: 2
                            )
                    )
            }
            .scaleEffect(isAtStart ? 0.9 : 1.0)
            .rotationEffect(.degrees(isAtStart ? -2 : 2))
            .onAppear {
                withAnimation(
                    .easeInOut(duration: 1)
                    .repeatForever()
                ) {
                    self.isAtStart.toggle()
                }
            }
        }
    }
}