
概览
小伙伴们肯定都惊叹过 SwiftUI 在界面设计中摧枯拉朽和辞简理博的强大,也一定被 SwiftUI 开发中各种"恢诡谲怪"的奇葩问题搞的"五内俱崩"、欲哭无泪。
在这些种种问题中,动画效果不尽如人意是很常见的一类。

如上图所示,我们的 DisclosureGroup 在收缩时并没有对应"可耐"的动画,显得很生硬!这是怎么回事呢?
在本篇博文中,您将学到如下内容:
- SwiftUI 中的 DisclosureGroup 视图
- "动画是个好东西,可惜你没有"
- 彗汜画涂,一发入魂!
在 SwiftUI 中,动画效果不能如愿大概率是秃头码农们自己撸码的不是。我们必须知其然同时知其所以然才能泰然处之的将它们"全面瓦解"。
那还等什么呢?Let's fix it!!!;)
SwiftUI 中的 DisclosureGroup 视图
从 SwiftUI 2.0(iOS 14)开始,苹果增加了全新的 DisclosureGroup 原生视图:

DisclosureGroup 的作用很简单:就是节省空间。我们可以通过它来有的放矢的展开和收起内容视图,从而合理的精简界面布局。
swift
struct ContentView: View {
@State var isExpanding = false
@State var sltingValue: Int?
var body: some View {
NavigationStack {
Form {
DisclosureGroup(
isExpanded: $isExpanding.animation(),
content: {
List {
ForEach(0...10, id: \.self) { i in
Text("Value \(i)")
.font(.title2.weight(.bold))
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button("选择") {
sltingValue = i
isExpanding = false
}
.tint(.green)
}
}
Button("取消选择") {
sltingValue = nil
isExpanding = false
}
}
},
label: {
VStack(alignment: .leading) {
if let sltingValue {
Text("当前选择值:\(sltingValue)")
.font(.title)
}
Spacer()
Text("当前收展状态:[\(isExpanding ? "展开" : "收起")]")
}
}
)
}
.toolbar {
Text("大熊猫侯佩 @ \(Text("CSDN").foregroundStyle(.red))")
.font(.headline)
.foregroundStyle(.gray)
}
.animation(.bouncy, value: isExpanding)
}
}
}
在如上代码中,我们利用 DisclosureGroup 视图控制着冗长列表内容的显示和隐藏。可以看到,我们实际是用 isExpanding 状态来反映且驱动 DisclosureGroup 收展状态的。

"动画是个好东西,可惜你没有"
现在我们已初步知晓了 DisclosureGroup 的作用,那么回到博文开头那个例子中去:为什么示例中 DisclosureGroup 的收缩没有动画点缀呢?
要探索和回答这个问题,我们有必要先来看看示例的源代码:
swift
DisclosureGroup(isExpanded: $isCheckInSelectPanelExpanding.animation(.bouncy), content: {
HStack {
DatePicker("", selection: $pickingTime, displayedComponents: [.hourAndMinute])
Button(action: {
isCheckInSelectPanelExpanding = false
withAnimation {
checkInTime = pickingTime
}
}) {
Text("确定")
}
.buttonStyle(.borderedProminent)
Button(action: {
isCheckInSelectPanelExpanding = false
withAnimation {
checkInTime = nil
}
}) {
Text("取消打卡")
}
.buttonStyle(.borderedProminent)
.tint(Color.red)
.disabled(checkInTime == nil)
}
}, label: {
HStack {
Text("规定打卡时间")
Spacer()
Text("\(checkInTime == nil ? "无" : V2_Model.hmFormater.string(from: checkInTime!))")
.foregroundStyle(checkInTime == nil ? .gray : .primary)
}
})
可以看到上面代码和之前几乎如出一辙:我们是用 isCheckInSelectPanelExpanding 状态来驱动 DisclosureGroup 的展开和收缩。我们在 DisclosureGroup 展开按钮的 action 中知趣的将 isCheckInSelectPanelExpanding 设置为 false,这样在完成按钮的功能之后立即收起 DisclosureGroup 有助于节省用户宝贵的时间。

但是为什么 DisclosureGroup 收起动画却不翼而飞了呢?
彗汜画涂,一发入魂!
仔细观察示例代码中的逻辑可以发现,当 isCheckInSelectPanelExpanding 被置为 false 时,就会导致 DisclosureGroup 的内容视图(Content View)从界面上被"清除"掉。
如果在一个即将被"删除"的视图上应用动画,那么多半人家不会鸟你。
所以,我们要做的就是让 SwiftUI 系统明确知道我们需要 isCheckInSelectPanelExpanding 状态的改变产生动画效果。
怎么操作呢?其实很简单:我们只需将 isCheckInSelectPanelExpanding 放到显式动画块中即可。
swift
DisclosureGroup(isExpanded: $isCheckInSelectPanelExpanding.animation(.bouncy), content: {
HStack {
DatePicker("", selection: $pickingTime, displayedComponents: [.hourAndMinute])
Button(action: {
withAnimation {
checkInTime = pickingTime
isCheckInSelectPanelExpanding = false
}
}) {
Text("确定")
}
.buttonStyle(.borderedProminent)
Button(action: {
withAnimation {
checkInTime = nil
isCheckInSelectPanelExpanding = false
}
}) {
Text("取消打卡")
}
.buttonStyle(.borderedProminent)
.tint(Color.red)
.disabled(checkInTime == nil)
}
}, label: {
HStack {
Text("规定打卡时间")
Spacer()
Text("\(checkInTime == nil ? "无" : V2_Model.hmFormater.string(from: checkInTime!))")
.foregroundStyle(checkInTime == nil ? .gray : .primary)
}
})
更多关于 SwiftUI 显式和隐式动画的介绍,请小伙伴们移步如下链接观赏:
在将 isCheckInSelectPanelExpanding 状态的改变改为显式动画后,我们再来运行看一下效果:

现在丝般顺滑的动画又一次"大圣归来",我们可以继续我们的 App 开发"仲夏夜"之梦啦,棒棒哒!💯
总结
在本篇博文中,我们介绍了 SwiftUI 2.0 新增的 DisclosureGroup 原生视图,并接着讨论了为什么它的收起操作没有动画,并最后给出解决方案。

感谢观赏,再会!8-)