You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI:withAnimation与inout参数配合失效,如何实现可复用Inspector动画?

修复SwiftUI可复用Inspector过渡动画问题

问题根源

原代码中使用inout参数传递@State变量时,破坏了SwiftUI的状态跟踪上下文——inout本质是值拷贝后写回,导致withAnimation无法正确绑定到视图的重绘逻辑,动画因此失效。

解决方案

1. 替换inout为闭包+当前值参数

将状态更新逻辑通过闭包传递,同时传入当前的selection值,确保withAnimation能关联到正确的视图状态上下文。我们可以封装一个全局泛型函数实现可复用的动画判断逻辑:

// 可复用的Inspector状态更新工具函数
func inspect<Selection: RawRepresentable<Int>>(
    currentSelection: Selection,
    updateSelection: @escaping (Selection) -> Void,
    to target: Selection
) where Selection.RawValue == Int {
    // 仅在显示/隐藏Inspector时触发动画
    let shouldAnimate = target.rawValue == 0 || currentSelection.rawValue == 0
    if shouldAnimate {
        withAnimation {
            updateSelection(target)
        }
    } else {
        updateSelection(target)
    }
}

2. 优化视图结构

用switch替代原有的if-else判断,明确selection == .none时显示EmptyView,让SwiftUI能准确识别视图的添加/移除状态,确保过渡动画触发:

struct ContentView: View {
    enum Selection: Int {
        case none
        case view1
        case view2
        case view3
    }
    
    @State var selection: Selection = .none
    
    var body: some View {
        HStack {
            VStack {
                Button("None") {
                    inspect(currentSelection: selection, updateSelection: { selection = $0 }, to: .none)
                }
                Button("View 1") {
                    inspect(currentSelection: selection, updateSelection: { selection = $0 }, to: .view1)
                }
                Button("View 2") {
                    inspect(currentSelection: selection, updateSelection: { selection = $0 }, to: .view2)
                }
                Button("View 3") {
                    inspect(currentSelection: selection, updateSelection: { selection = $0 }, to: .view3)
                }
            }
            
            // Switch结构明确视图状态
            switch selection {
            case .view1:
                Text("Hello 1")
            case .view2:
                Text("Hello 2")
            case .view3:
                Text("Hello 3")
            case .none:
                EmptyView()
            }
            .transition(.move(edge: .trailing))
        }
    }
}

3. 进阶:封装为可复用视图容器

如果需要更高的复用性,可以将Inspector的容器和更新逻辑封装成自定义View,进一步简化代码:

struct InspectorContainer<Selection: RawRepresentable<Int>, Content: View>: View where Selection.RawValue == Int {
    let selection: Selection
    let content: (Selection) -> Content
    
    init(selection: Selection, @ViewBuilder content: @escaping (Selection) -> Content) {
        self.selection = selection
        self.content = content
    }
    
    var body: some View {
        if selection.rawValue != 0 {
            content(selection)
        }
        .transition(.move(edge: .trailing))
    }
    
    // 静态方法封装更新逻辑
    static func update<Selection: RawRepresentable<Int>>(
        currentSelection: Selection,
        to target: Selection,
        update: @escaping (Selection) -> Void
    ) where Selection.RawValue == Int {
        let shouldAnimate = target.rawValue == 0 || currentSelection.rawValue == 0
        if shouldAnimate {
            withAnimation {
                update(target)
            }
        } else {
            update(target)
        }
    }
}

// 使用示例
struct ContentView: View {
    enum Selection: Int {
        case none
        case view1
        case view2
        case view3
    }
    
    @State var selection: Selection = .none
    
    var body: some View {
        HStack {
            VStack {
                Button("None") {
                    InspectorContainer.update(currentSelection: selection, to: .none, update: { selection = $0 })
                }
                Button("View 1") {
                    InspectorContainer.update(currentSelection: selection, to: .view1, update: { selection = $0 })
                }
                Button("View 2") {
                    InspectorContainer.update(currentSelection: selection, to: .view2, update: { selection = $0 })
                }
                Button("View 3") {
                    InspectorContainer.update(currentSelection: selection, to: .view3, update: { selection = $0 })
                }
            }
            
            InspectorContainer(selection: selection) { selection in
                switch selection {
                case .view1:
                    Text("Hello 1")
                case .view2:
                    Text("Hello 2")
                case .view3:
                    Text("Hello 3")
                default:
                    EmptyView()
                }
            }
        }
    }
}

复用验证

对于其他需要Inspector的视图,只需定义自己的Selection枚举,直接复用工具函数或自定义容器即可:

enum AnotherSelection: Int {
    case none
    case details
    case settings
}

struct AnotherView: View {
    @State var selection: AnotherSelection = .none
    
    var body: some View {
        HStack {
            VStack {
                Button("None") {
                    inspect(currentSelection: selection, updateSelection: { selection = $0 }, to: .none)
                }
                Button("Details") {
                    inspect(currentSelection: selection, updateSelection: { selection = $0 }, to: .details)
                }
                Button("Settings") {
                    inspect(currentSelection: selection, updateSelection: { selection = $0 }, to: .settings)
                }
            }
            
            switch selection {
            case .details:
                Text("Details View")
            case .settings:
                Text("Settings View")
            case .none:
                EmptyView()
            }
            .transition(.move(edge: .trailing))
        }
    }
}

内容的提问来源于stack exchange,提问作者Yiming Designer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 23:25:59