如果操作未立即完成,则 SwiftUI 视图在绑定到 @Published var 时没有动画

Posted

技术标签:

【中文标题】如果操作未立即完成,则 SwiftUI 视图在绑定到 @Published var 时没有动画【英文标题】:SwiftUI view not animating when bound to @Published var if action isn't completed immediately 【发布时间】:2020-03-10 11:23:11 【问题描述】:

我有一个 SwiftUI 视图,它根据状态换出某些控件。我正在尝试使用 MVVM,所以我的大部分/所有逻辑都被推到了视图模型上。我发现,在执行修改视图模型上的@Published var 的复杂操作时,View 不会动画。

这是一个示例,其中视图模型中的 1.0 秒计时器模拟在更改 @Published var 值之前正在完成的其他工作:

struct ContentView: View 
    @State var showCircle = true
    @ObservedObject var viewModel = ViewModel()

    var body: some View 

        VStack 
            VStack 
                if showCircle 
                    Circle().frame(width: 100, height: 100)
                

                Button(action: 
                    withAnimation 
                        self.showCircle.toggle()
                    

                ) 
                    Text("With State Variable")
                
            

            VStack 
                if viewModel.showCircle 
                    Circle().frame(width: 100, height: 100)
                
                Button(action: 
                    withAnimation 
                        self.viewModel.toggle()
                    
                ) 
                    Text("With ViewModel Observation")
                
            
        
    


    class ViewModel: ObservableObject 
        @Published var showCircle = true

        public func toggle() 
            // Do some amount of work here. The Time is just to simulate work being done that may not complete immediately.
            Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false)  [weak self] _ in
                self?.showCircle.toggle()
            

        
    

【问题讨论】:

【参考方案1】:

在视图模型工作流的情况下,您的 withAnimation 什么都不做,因为在这种情况下不会更改状态(它只是一个函数调用),只有计时器被安排,所以你宁愿需要它

Button(action: 
    self.viewModel.toggle()  // removed from here
) 
    Text("With ViewModel Observation")


...

Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false)  [weak self] _ in
    withAnimation  // << added here
        self?.showCircle.toggle()
    

但是我更愿意建议重新考虑视图设计......就像

VStack 
    if showCircle2  // same declaration as showCircle
        Circle().frame(width: 100, height: 100)
    
    Button(action: 
        self.viewModel.toggle()
    ) 
        Text("With ViewModel Observation")
    
    .onReceive(viewModel.$showCircle)  value in
        withAnimation 
            self.showCircle2 = value
        
    

使用 Xcode 11.2 / ios 13.2 测试

【讨论】:

我正在尝试构建视图,以便它能够很好地响应视图模型的变化。有没有更好的方法来实现这一点? @lepolt,见更新。动画是 View 的特性,所以最好保持在视图中。查看模型异步属性更新的情况,请参阅更新部分。 我不知道.onReceive...但我认为这可能有效...稍后会测试并报告! 这是我第一个使用 SwiftUI 的复杂应用程序,我认为我需要退后一步重新思考一些架构。 .onReceive 有效,除了我在 .sheet 演示文稿中遇到另一个问题,它的绑定触发不止一次,但这是另一天的话题。【参考方案2】:

父视图以动画方式隐藏和显示其子视图。如果您在第一个 VStack 的末尾放置一个 .animation(.easeIn)(或 .easeOut 或任何您喜欢的),它应该可以按预期工作。

这样……

struct ContentView: View 
@State var showCircle = true
@ObservedObject var viewModel = ViewModel()

var body: some View 
    VStack 
        VStack 
            if showCircle 
                Circle().frame(width: 100, height: 100)
            

            Button(action: 
                withAnimation 
                    self.showCircle.toggle()
                

            ) 
                Text("With State Variable")
            
        

        VStack 
            if viewModel.showCircle 
                Circle().frame(width: 100, height: 100)
            
            Button(action: 
                withAnimation 
                    self.viewModel.toggle()
                
            ) 
                Text("With ViewModel Observation")
            
        
    .animation(.easeIn)
  

【讨论】:

这种方法的缺点是它会将动画应用于所有子视图和大子视图。它可能会解决 OP 提到的这个特定问题,但对于其他具有复杂子视图的一般用例可能不是最佳解决方案。【参考方案3】:

您必须对动画使用延迟,而不是使用计时器来延迟您的动画。

Button(action: 
    withAnimation(Animation.linear.delay(1.0)) 
        self.viewModel.showCircle.toggle()
    
) 
    Text("With ViewModel Observation")

否则,当您不使用 Timer 时,视图模型中的更改将按预期进行。

class ViewModel: ObservableObject 
    @Published var showCircle = true

    public func toggle() 

        /* other operations */

        self.showCircle.toggle()
    


【讨论】:

计时器只是为了模拟“正在进行但可能不会立即完成的工作”。我不能保证知道这项工作需要多长时间,因此添加 1.0 秒的延迟并不是一个好的解决方案。 @lepolt 在这种情况下,当 Timer 中的状态未更改时,您的视图模型将正常工作。顺便说一句,你投反对票了吗?

以上是关于如果操作未立即完成,则 SwiftUI 视图在绑定到 @Published var 时没有动画的主要内容,如果未能解决你的问题,请参考以下文章

在 SwiftUI 中的 ForEach 中绑定

SwiftUI如何让绑定到同一个状态的多个TextField呈现出不同输入行为

SwiftUI如何让绑定到同一个状态的多个TextField呈现出不同输入行为

检查后在 SwiftUI 中将可选绑定设置为 nil 时出现异常

如果它不可见,SwiftUI 如何使 NavigationLink 工作?

SwiftUI NavigationLink:成功运行帐户创建代码后导航到目标视图