SwiftUI withAnimation 导致意外行为

len*_*nny 3 xcode swift swiftui

我注意到 XCode 使用 swiftUI 的奇怪行为withAnimation{}

我创建了以下工作示例:

import SwiftUI

class Block: Hashable, Identifiable {
    // Note: this needs to be a class rather than a struct
    
    var id = UUID()
    
    static func == (lhs: Block, rhs: Block) -> Bool {
        lhs.id == rhs.id
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(self.id)
    }
}

struct ContentView: View {
    @State private var blocks: [Block]
    
    init() {
        var blocks = [Block]()
        
        /// generate some blocks here
        blocks.append(Block())
        blocks.append(Block())
        blocks.append(Block())
        
        self._blocks = State(initialValue: blocks)
    }
    
    var body: some View {
        VStack {
            ForEach(blocks, id: \.self) { block in
                BlockRow(block: block) { b in
                    print("COMMENT THIS LINE OUT") // try commenting out this line -> XCODE won't build anymore
                    
                    withAnimation {
                        self.blocks.remove(at: self.blocks.firstIndex(of: b)!)
                    }
                }
            }
        }
    }
}

struct BlockRow: View {

    @State var block: Block
    var onDelete: (Block) -> Void = {_ in}
    
    var body: some View {
        Text("<Block>")
            .onTapGesture {
                print("Tap block \(block.id)")
                self.onDelete(block)
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

上面的示例按预期工作,如果您单击其中一个块,它就会被删除,并且以下块将很好地滑动到空闲位置。

但 XCode 在这里产生警告,我不明白:

调用“withAnimation”的结果未使用

事情变得更加令人困惑:通过事先注释掉 print Stamenet,withAnimation 块/* print("COMMENT THIS LINE OUT") */XCode 将不再构建项目:

未能产生表达诊断;请提交错误报告

这是一个错误还是我的方法完全偏离了道路?

vac*_*ama 11

问题的根源在于self.blocks.remove(at:)返回了您未处理的值。如果您明确忽略该值,那么无论该print语句是否存在,您的代码都会按预期工作。

withAnimation {
    _ = self.blocks.remove(at: self.blocks.firstIndex(of: b)!)
}
Run Code Online (Sandbox Code Playgroud)

Swift 有一个功能,即使用一行代码的闭包会返回该语句的值作为闭包的返回值。所以在这种情况下,如果不忽略remove(at:)它的返回值,它会返回a BlockBlock然后成为闭包的返回类型withAnimation

withAnimation定义如下:

func withAnimation<Result>(_ animation: Animation? = .default, _ body: () throws -> Result) rethrows -> Result
Run Code Online (Sandbox Code Playgroud)

它是一个带有闭包的通用函数。该闭包的返回类型决定了通用占位符的类型,而通用占位符又决定了其withAnimation自身的返回类型。

因此,如果您不忽略 的返回类型remove(at:),该withAnimation<Block>函数将返回一个Block.

如果忽略 的返回值remove(at:),则该语句将变成没有返回值的语句(即,它返回()Void)。因此,withAnimation函数变为withAnimation<Void>并返回Void

现在,因为当你删除 print 语句时,闭包 toBlockRow只有一行代码,所以它的返回值是单个语句的返回值,现在是Void

BlockRow(block: block) { b in
    withAnimation {
        _ = self.blocks.remove(at: self.blocks.firstIndex(of: b)!)
    }
}
Run Code Online (Sandbox Code Playgroud)

BlockRow这与闭包期望的类型相匹配onDelete

在您的原始代码中,该print语句导致闭包BlockRow有两行代码,从而避免了 Swift 使用单行代码来确定闭包返回类型的功能。您确实收到一条警告,表明您没有使用Block从函数返回的withAnimation<Block>。@Asperi 的答案通过将Block返回的分配给withAnimation<Block>来修复该警告_。这与我建议的解决方案具有相同的效果,但它是在更高级别上处理问题,而不是从问题的根源处处理。


为什么编译器不会抱怨您忽略了 的返回值remove(at:)

remove(at:)明确设计为允许您丢弃返回的结果,这就是为什么您不会收到它返回您未处理的值的警告。

@discardableResult mutating func remove(at i: Self.Index) -> Self.Element
Run Code Online (Sandbox Code Playgroud)

但正如您所看到的,这会导致您遇到令人困惑的结果。您使用时remove(at:)就好像它返回了Void,但实际上它正在返回Block从数组中删除的 。这就会导致导致您的问题的整个事件链。