首页 > 解决方案 > 如何为@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
                }
            }
        }
    }
}

标签: swiftswiftui

解决方案


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

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

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

结果将是下一个:

使用 onReceive


推荐阅读