skip to Main Content

I’m making a simple task app and using ForEach to populate task rows with the task information from my model. I need a way to animate my task view to open up and reveal some description text and two buttons. I want to turn from A into B on tap, and then back again on tap:

Design Image

I’ve tried a couple things. I successfully got a proof-of-concept rectangle animating in a test project, but there are issues. The rectangle shrinks and grows from the centre point, vs. from the bottom only. When I place text inside it, the text doesn’t get hidden and it looks really bad.

struct ContentView: View {
@State var animate = false
var animation: Animation = .spring()

var body: some View {
    
    VStack {
        Rectangle()
            .frame(width: 200, height: animate ? 60 : 300)
            .foregroundColor(.blue)
            .onTapGesture {
                withAnimation(animation) {
                    animate.toggle()
                }
            }
    }
}

In my main app, I was able to replace my first task view (closed) with another view that’s open. This works but it looks bad and it’s not really doing what I want. It’s effectively replacing the view with another one using a fade animation.

ForEach(taskArrayHigh) { task in
if animate == false {
    TaskView(taskTitle: task.title, category: task.category?.rawValue ?? "", complete: task.complete?.rawValue ?? "", priorityColor: Color("HighPriority"), task: task, activeDate: activeDate)
        .padding(.top, 10)
        .padding(.horizontal)
        .onTapGesture {
            withAnimation(.easeIn) {
                animate.toggle()
            }
        }
        .transition(.move(edge: .bottom))
} else if animate == true {
    TaskViewOpen(task: "Grocery Shopping", category: "Home", remaining: 204, completed: 4)
        .padding(.top, 10)
        .padding(.horizontal)
        .onTapGesture {
            withAnimation(.easeIn) {
                animate.toggle()
            }
        }
}

Is there a way to animate my original closed view to open up and reveal the description text and buttons?

2

Answers


  1. You are on the right track with your .transition line you have, but you want to make sure that the container stays the same and the contents change — right now, you’re replacing the entire view.

    Here’s a simple example illustrating the concept:

    struct ContentView: View {
        
        @State var isExpanded = false
        
        var body: some View {
            VStack {
                Text("Headline")
                if isExpanded {
                    Text("More Info")
                    Text("And more")
                }
            }
            .padding()
            .frame(maxWidth: .infinity)
            .transition(.move(edge: .bottom))
            .background(Color.gray.cornerRadius(10.0))
            .onTapGesture {
                withAnimation {
                    isExpanded.toggle()
                }
            }
        }
    }
    

    Since you’re using it inside a ForEach, you’ll probably want to abstract this into its own component, as it’ll need its own @State to keep track of the expanded state as I’ve shown here.


    Update, based on comments:

    Example of using a PreferenceKey to get the height of the expandable view so that the frame can be animated and nothing fades in and out:

    struct ContentView: View {
        
        @State var isExpanded = false
        @State var subviewHeight : CGFloat = 0
        
        var body: some View {
            VStack {
                Text("Headline")
                VStack {
                    Text("More Info")
                    Text("And more")
                    Text("And more")
                    Text("And more")
                    Text("And more")
                    Text("And more")
                }
            }
            .background(GeometryReader {
                Color.clear.preference(key: ViewHeightKey.self,
                                       value: $0.frame(in: .local).size.height)
            })
            .onPreferenceChange(ViewHeightKey.self) { subviewHeight = $0 }
            .frame(height: isExpanded ? subviewHeight : 50, alignment: .top)
            .padding()
            .clipped()
            .frame(maxWidth: .infinity)
            .transition(.move(edge: .bottom))
            .background(Color.gray.cornerRadius(10.0))
            .onTapGesture {
                withAnimation(.easeIn(duration: 2.0)) {
                    isExpanded.toggle()
                }
            }
        }
    }
    
    struct ViewHeightKey: PreferenceKey {
        static var defaultValue: CGFloat { 0 }
        static func reduce(value: inout Value, nextValue: () -> Value) {
            value = value + nextValue()
        }
    }
    
    Login or Signup to reply.
  2. Using Swift 5 you can use withAnimation and have the view hidden based on state.

    ExpandViewer

    • Has a button to show and hide the inner view
    • Takes in a content view
    struct ExpandViewer <Content: View>: View {
        
        @State private var isExpanded = false
        @ViewBuilder let expandableView : Content
        
        var body: some View {
            VStack {
                
                Button(action: {
                    withAnimation(.easeIn(duration: 0.5)) {
                        self.isExpanded.toggle()
                    }
                    
                }){
                    Text(self.isExpanded ? "Hide" : "View")
                        .foregroundColor(.white)
                        .frame(maxWidth: .infinity, minHeight: 40, alignment: .center)
                        .background(.blue)
                        .cornerRadius(5.0)
                }
                
                if self.isExpanded {
                     self.expandableView
                }
                
            }
           
        }
    }
    
    

    Using the viewer

    ExpandViewer {
      Text("Hidden Text")
      Text("Hidden Text")
    }
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search