I'm trying to replicate an accordion component that should work like the one in the Custom Control Label from this documentation. To do this I created a Collapsable view as follows:
struct Collapsable<Content>: View where Content: View {
internal var content: () -> Content
@Binding private var isCollapsed: Bool
@State private var isCollapsedForAnimation: Bool
init(isCollapsed: Binding<Bool>, @ViewBuilder content: @escaping () -> Content) {
self._isCollapsed = isCollapsed
self.content = content
self.isCollapsedForAnimation = isCollapsed.wrappedValue
}
var body: some View {
content()
.background(.ultraThinMaterial)
.frame(maxHeight: isCollapsedForAnimation ? 0 : nil, alignment: .top)
.contentShape(Rectangle())
.clipped()
.onChange(of: self.isCollapsed) { isCollapsed in
withAnimation {
self.isCollapsedForAnimation = isCollapsed
}
}
}
}
And it works as in it allows me to show and hide the content of the content parameter, but the animation is terribly inconsistent, since they way it expands/contracts depends on the container. I would like the view to always expand and contract from bottom to top, or even better, allow the user to specify whether he wants it to animate from top to bottom or the other way around.
Unfortunately I couldn't find a way to get this behavior, so I'm looking for hints in the right direction.
The first thing to understand is that you must test on a real device, not on the simulator. The iOS simulator often has display bugs that real devices don't.
The second thing to understand is that SwiftUI animations can be flaky and difficult even on devices.
Anyway, let's reproduce the accordion component you linked to, in SwiftUI. Here's my result:
I recorded this on my iPhone 12 Mini running iOS 16.4. On the device it looks much smoother, but I generated the animated GIF at 15 fps to get it down to an almost reasonable file size.
Anyway, here's my implementation of
CustomControlLabel:Here are two key differences between my code and yours:
I use the
fixedSize(horizontal: false, vertical: true)modifier on the collapsing content, inside theframemodifier that sets its height to zero when it's collapsed. ThisfixedSizemodifier hides the zero height from the content, so it doesn't try to change its own layout to fit in a zero height.I don't perform the animation at this level. Expanding one
CustomControlLabelshould simultaneously collapse any other expandedCustomControlLabel. To get the overall layout to animate correctly, I apply ananimationmodifier to the container that contains all of theCustomControlLabels.It's also important that the
VStackin there hasspacing: 0. The default is that SwiftUI picks a spacing it thinks is appropriate, but that spacing changes when the content is entirely collapsed! So without thespacing: 0, the collapsible content moves slightly as its visibility changes. To prevent that, I setspacing: 0and addedpadding(.top, 16)to the collapsible content.Here is the rest of my code:
You'll need to scroll down but at the bottom you'll find the single
animationmodifier that animates everything.