Skip to content

Instantly share code, notes, and snippets.

@Gernot
Created January 5, 2022 18:51
Show Gist options
  • Save Gernot/72a37e52d24d2b5d0c051cc1767bc63a to your computer and use it in GitHub Desktop.
Save Gernot/72a37e52d24d2b5d0c051cc1767bc63a to your computer and use it in GitHub Desktop.
List like transitions for VStacks?
import Foundation
import SwiftUI
import PlaygroundSupport
let allTexts = ["one", "two", "three", "four", "five"]
let selectedTexts = ["two", "four"]
struct TestView: View {
@State private var expanded = false
var body: some View {
VStack { // <- Replace with "List" to see what I want
ForEach(expanded ? allTexts : selectedTexts, id:\.self) { text in
Text(text)
}
}
.animation(.default, value: expanded)
.onTapGesture { expanded.toggle() }
.frame(width: 300, height: 600)
}
}
PlaygroundPage.current.setLiveView(TestView())
@Gernot
Copy link
Author

Gernot commented Jan 5, 2022

Here is a slightly different version that shows what the issue is: In a clipped view, it is irritating that the views on the edges don't move.

import Foundation
import SwiftUI
import PlaygroundSupport

let allTexts = ["one", "two", "three", "four", "five"]
let selectedTexts = ["two", "four"]

struct TestView: View {
    
    @State private var expanded = false
    
    var body: some View {
        VStack {
            ForEach(expanded ? allTexts : selectedTexts, id:\.self) { text in
                Text(text)
                    .transition(.opacity)
            }
        }
        .listStyle(.plain)
        .onTapGesture { expanded.toggle() }
        .border(.red)
        .clipped() // <- Using `clipped` shows what the issue is. It is irritating that "One" and "Five" don't move.
        .animation(.easeOut(duration: 2), value: expanded)
        .frame(width: 300, height: 600)
    }
}

PlaygroundPage.current.setLiveView(TestView())

@uberbruns
Copy link

uberbruns commented Jan 5, 2022

I think I know what you mean … sadly this approach only works for appearance and not not the disappearance.

import Foundation
import SwiftUI
import PlaygroundSupport


let allTexts = ["one", "two", "three", "four", "five"]
let selectedTexts = ["two", "four"]


struct CollapseModifier: ViewModifier {
  let isCollapsed: Bool
  func body(content: Content) -> some View {
    content
      .clipped()
      .opacity(isCollapsed ? 0 : 1)
      .frame(height: isCollapsed ? 0 : nil)
  }
}


struct TestView: View {

  @State private var expanded = false

  var body: some View {
    VStack(spacing: 0) {
      ForEach(expanded ? allTexts : selectedTexts, id:\.self) { text in
        Text(text).transition(
          .modifier(
            active: CollapseModifier(isCollapsed: true),
            identity: CollapseModifier(isCollapsed: false)
          )
        )
      }
    }
    .onTapGesture { expanded.toggle() }
    .border(.red)
    .clipped() // <- Using `clipped` shows what the issue is. It is irritating that "One" and "Five" don't move.
    .animation(.easeOut(duration: 2), value: expanded)
    .frame(width: 300, height: 600)
  }
}


PlaygroundPage.current.setLiveView(TestView())

@Gernot
Copy link
Author

Gernot commented Jan 6, 2022

I think I got a solution that (sort of) does what I want without rewriting VStack. The idea is to not have SwiftUI show/hide elements based on ID, but to always show them and alter their size/opacity. That way, the origin position is not static in the animation but close to the nearest visible element.

import Foundation
import SwiftUI
import PlaygroundSupport

let allTexts = ["zero", "one", "two", "three", "four", "five", "six"]
let selectedTexts = ["two", "four"]

struct TestView: View {
    
    @State private var expanded = false
    
    var body: some View {
        VStack(spacing: 0) {
            ForEach(allTexts, id:\.self) { text in
                let visible = selectedTexts.contains(text)
                Text(text)
                    .frame(height: expanded||visible ? nil : 0)
                    .opacity(expanded||visible ? 1 : 0)
                    .transition(.opacity)
            }
        }
        .listStyle(.plain)
        .onTapGesture { expanded.toggle() }
        .border(.red)
        .clipped() // <- Using `clipped` shows what the issue is. It is irritating that "One" and "Five" don't move.
        .animation(.spring(), value: expanded)
        .frame(width: 300, height: 600)
    }
}

PlaygroundPage.current.setLiveView(TestView())

@uberbruns
Copy link

I wanted to propose that but thought that if allTexts and selectedText change with a common addition the common addition would animate in in the undesired way. But if that won't happen, then I think that solution looks really nice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment