swift - 如何为@ObservedObject 的更改设置动画?
问题描述
我的视图由存储在ViewModel
. 有时视图可能会在其上调用一个函数ViewModel
,从而导致异步状态更改。
如何在 中设置状态变化的动画效果View
?
这是一个人为的示例,其中调用viewModel.change()
将导致视图改变颜色。
- 预期行为:从蓝色缓慢溶解到红色。
- 实际行为:立即从蓝色变为红色。
class ViewModel: ObservableObject {
@Published var color: UIColor = .blue
func change() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.color = .red
}
}
}
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
var body: some View {
Color(viewModel.color).onAppear {
withAnimation(.easeInOut(duration: 1.0)) {
self.viewModel.change()
}
}
}
}
如果我删除ViewModel
并将状态存储在视图本身中,一切都会按预期工作。然而,这不是一个很好的解决方案,因为我想将状态封装在ViewModel
.
struct ContentView: View {
@State var color: UIColor = .blue
var body: some View {
Color(color).onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
withAnimation(.easeInOut(duration: 1.0)) {
self.color = .red
}
}
}
}
}
解决方案
使用 .animation()
您可以.animation(...)
在正文内容或任何子视图上使用,但它会为视图的所有更改设置动画。
让我们考虑一个示例,当我们通过 ViewModel 调用两个 API 并.animation(.default)
在正文内容上使用时:
import SwiftUI
import Foundation
class ArticleViewModel: ObservableObject {
@Published var title = ""
@Published var content = ""
func fetchArticle() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.title = "Article Title"
}
}
func fetchContent() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.content = "Content"
}
}
}
struct ArticleView: View {
@ObservedObject var viewModel = ArticleViewModel()
var body: some View {
VStack(alignment: .leading) {
if viewModel.title.isEmpty {
Button("Load Article") {
self.viewModel.fetchArticle()
}
} else {
Text(viewModel.title).font(.title)
if viewModel.content.isEmpty {
Button("Load content...") {
self.viewModel.fetchContent()
}
.padding(.vertical, 5)
.frame(maxWidth: .infinity, alignment: .center)
} else {
Rectangle()
.foregroundColor(Color.blue.opacity(0.2))
.frame(height: 80)
.overlay(Text(viewModel.content))
}
}
}
.padding()
.frame(width: 300)
.background(Color.gray.opacity(0.2))
.animation(.default) // animate all changes of the view
}
}
结果将是下一个:
你可以看到我们在这两个动作上都有动画。这可能是首选行为,但在某些情况下,您可能希望单独控制每个操作。
使用 .onReceive()
假设我们希望在第一次 API 调用 ( fetchArticle
) 之后为视图设置动画,但在第二次 ( fetchContent
) 上 - 只是重绘视图而不使用动画。换句话说 - 在收到时为视图设置动画,title
但在收到时不为视图设置动画content
。
为了实现这一点,我们需要:
- 在视图中创建一个单独的属性
@State var title = ""
。 - 在整个视图中使用这个新属性,而不是
viewModel.title
. - 声明
.onReceive(viewModel.$title) { newTitle in ... }
。viewModel.$title
当发布者 ( ) 发送新值时,将执行此闭包。在这一步中,我们可以控制视图中的属性。在我们的例子中,我们将更新title
视图的属性。 - 在闭包内使用
withAnimation {...}
动画更改。
所以我们会在title
更新时有动画。在接收content
到 ViewModel 的新值时,我们的 View 只是在没有动画的情况下更新。
struct ArticleView: View {
@ObservedObject var viewModel = ArticleViewModel()
// 1
@State var title = ""
var body: some View {
VStack(alignment: .leading) {
// 2
if title.isEmpty {
Button("Load Article") {
self.viewModel.fetchArticle()
}
} else {
// 2
Text(title).font(.title)
if viewModel.content.isEmpty {
Button("Load content...") {
self.viewModel.fetchContent()
}
.padding(.vertical, 5)
.frame(maxWidth: .infinity, alignment: .center)
} else {
Rectangle()
.foregroundColor(Color.blue.opacity(0.2))
.frame(height: 80)
.overlay(Text(viewModel.content))
}
}
}
.padding()
.frame(width: 300)
.background(Color.gray.opacity(0.2))
// 3
.onReceive(viewModel.$title) { newTitle in
// 4
withAnimation {
self.title = newTitle
}
}
}
}
结果将是下一个:
推荐阅读
- unit-testing - 使用 Jest 单元测试访问 React Hooks 状态
- python - 为什么我的四舍五入到两位小数不适用于我的代码?
- python - .join() TypeError, 'expected str instance' 传入整数列表时
- javascript - 选择时如何使所选行转到表格顶部
- android - Kotlin 中带圆角的 ImageView
- ios - 在 UITableView 的编辑模式下进行多选时,单元格不会自动突出显示
- python-3.x - 如何使用 pywinauto 等待应用程序打开?
- blazor - 我如何在另一个组件中立即多次动态呈现 Blazor 组件(例如:每个按钮单击)
- java - Spring中本机@Query中的转义参数
- python - PostgreSQL 与 docker compose 和 sqlalchemy