swift 如何设置对@ObservedObject的更改的动画?

fquxozlt  于 2023-03-28  发布在  Swift
关注(0)|答案(3)|浏览(123)

我的视图是由存储在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并将状态存储在视图本身中,一切都按预期工作。

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

omqzjyyz1#

使用.animation()

您可以在主体内容或任何子视图上使用.animation(...),但它将对视图的所有更改进行动画处理。
让我们考虑一个例子,当我们通过ViewModel进行两次API调用并在body内容上使用.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)之后动画视图,但是在第二次API调用(fetchContent)时-只重绘视图而不动画。换句话说-在title接收时动画视图,但在content接收时不动画视图。
为了实现这一点,我们需要:
1.在视图中创建一个单独的属性@State var title = ""
1.在整个视图中使用这个新属性,而不是viewModel.title
1.声明.onReceive(viewModel.$title) { newTitle in ... }。这个闭包将在发布者(viewModel.$title)发送新值时执行。在这一步,我们可以控制View中的属性。在我们的例子中,我们将更新View的title属性。
1.在闭包中使用withAnimation {...}来动画化更改。
因此,当title更新时,我们会有动画。当接收到ViewModel的新content值时,我们的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
            }
        }
    }
}

结果如下:

vawmfj5a

vawmfj5a2#

看起来好像在异步闭包中使用withAnimation会导致颜色没有动画,而是立即改变。
删除 Package asyncAfter,或者删除withAnimation调用并在ContentViewbody中添加animation修饰符(如下所示)应该可以解决这个问题:

Color(viewModel.color).onAppear {
    self.viewModel.change()
}.animation(.easeInOut(duration: 1))

在本地测试(iOS 13.3,Xcode 11.3),这也似乎是溶解/褪色从蓝色到红色,因为你的意图。

enxuqcxy

enxuqcxy3#

更新@Seb Jachec对Xcode 14.2的回答,以修复已弃用的警告:

Color(viewModel.color)
            .onAppear {
                viewModel.change()
            }
            .animation(.easeInOut(duration: 1), value: viewModel.color)

相关问题