如何为@ObservedObject的更改制作动画?

时间:2020-01-08 12:46:02

标签: 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()
            }
        }
    }
}

如果我删除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
                }
            }
        }
    }
}

2 个答案:

答案 0 :(得分:11)

使用.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
    }
}

结果将是下一个:

Animation modifier on body

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

使用.onReceive()

假设我们要在第一个API调用(fetchArticle)之后对视图进行动画处理,但是在第二个API(fetchContent)上进行动画处理-仅重绘不带动画的视图。换句话说-在收到title时为视图添加动画,但在收到content时不为视图添加动画。

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

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

因此,title更新时,我们将制作动画。在收到新的content的ViewModel值时,我们的视图只会更新而不会产生动画。

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
            }
        }
    }
}

结果将是下一个:

Useing onReceive

答案 1 :(得分:1)

在异步闭包中使用withAnimation似乎使颜色没有动画,而是立即改变。

要么删除包装的asyncAfter,要么删除withAnimation调用并在animation的{​​{1}}中添加body修饰符(如下所示),解决问题:

ContentView

在本地进行了测试(iOS 13.3,Xcode 11.3),它也可以根据需要从蓝色变为红色。