如何对@ObservedObject 的更改进行动画处理?

sam*_*m-w 14 swift swiftui

我的观点由存储在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()
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我删除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
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

And*_*kyi 23

使用 .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
    }
}
Run Code Online (Sandbox Code Playgroud)

结果将是下一个:

身体上的动画修改器

你可以看到我们在两个动作上都有动画。这可能是首选行为,但在某些情况下,您可能希望单独控制每个操作

使用 .onReceive()

假设我们希望在第一次 API 调用 ( fetchArticle)后为视图设置动画,但在第二次 ( fetchContent) 上 - 只需重绘没有动画的视图。换句话说 - 在title接收时动画视图但在接收时不动画视图content

为了实现这一点,我们需要:

  1. @State var title = ""在视图中创建一个单独的属性。
  2. 在整个视图中使用这个新属性而不是 viewModel.title.
  3. 声明.onReceive(viewModel.$title) { newTitle in ... }。当发布者 ( viewModel.$title) 发送新值时,将执行此闭包。在这一步中,我们可以控制视图中的属性。在我们的例子中,我们将更新titleView的属性。
  4. withAnimation {...}在闭包内部使用以动画化更改。

所以当title更新时我们会有动画。在接收contentViewModel的新值时,我们的 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
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

结果将是下一个:

使用 onReceive

  • 这是一个非常彻底且写得好的答案。竖起大拇指! (2认同)

Seb*_*hec 5

看起来好像withAnimation在异步闭包内部使用会导致颜色不产生动画,而是立即改变。

删除包装asyncAfter,或删除调用并在您的中withAnimation添加修饰符(如下所示)应该可以解决该问题:animationbodyContentView

Color(viewModel.color).onAppear {
    self.viewModel.change()
}.animation(.easeInOut(duration: 1))
Run Code Online (Sandbox Code Playgroud)

在本地测试(iOS 13.3,Xcode 11.3),这似乎也按照您的意愿从蓝色溶解/淡化为红色。