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

SwiftUI自定义视图中状态变更时按钮标签不更新问题

问题:SwiftUI动态生成视图中Toggle按钮状态不更新

我正在开发一款SwiftUI应用,通过模板字符串动态生成视图,模板中的占位符会被替换为SwiftUI视图(如Button)。核心结构是主视图RuleInputContentView包含动态构建器RuleInputExpressionBuilder,负责解析模板字符串并插入对应视图。

目前除TOGGLE按钮外,其他功能均正常工作。点击TOGGLE按钮时,预期标签会在“ON”和“OFF”间切换,但实际标签并未更新,@State属性toggleButton的变更没有触发视图重渲染。

相关代码

struct RuleInputContentView: View {
    @State private var showPopover = false
    @State private var toggleButton = false
    
    var body: some View {
        RuleInputExpressionBuilder(
            pattern: String(localized: "The great toggle {{ONOFF}} and {{TOGGLE}} so let's go."),
            contents:
                RuleInputExpressionParameter(label: "ONOFF") {
                    Button("Button") {
                        showPopover.toggle()
                    }
                    .buttonStyle(.link)
                    .popover(isPresented: $showPopover, content: {
                        // TODO: view to switch between on and off
                    })
                    .buttonStyle(.link)
                },
                RuleInputExpressionParameter(label: "TOGGLE") {
                    Button(action: {
                        toggleButton.toggle()
                    }, label: {
                        if toggleButton {
                            Text("ON")
                        } else {
                            Text("OFF")
                        }
                    })
                    .buttonStyle(.link)
                }
        )
    }
}

// MARK: Expression Parameter

struct RuleInputExpressionParameter {
    let label: String
    let view: AnyView
    
    init<Content: View>(label: String, _ content: () -> Content) {
        self.label = label
        self.view = AnyView(content())
    }
}

// MARK: Expression Builder

struct RuleInputExpressionBuilder: View {
    enum Component {
        case text(String)
        case view(AnyView)
    }
    
    let parameters: [RuleInputExpressionParameter]
    let pattern: String
    @State private var components: [Component] = []
    
    init(pattern: String, contents: RuleInputExpressionParameter...) {
        self.pattern = pattern
        self.parameters = contents
    }
    
    var body: some View {
        /// relevant code base:
        HStack {
            buildExpression()
        }
        .onAppear {
            renderPattern()
        }
    }
    
    private func renderPattern() {
        components.removeAll()
        
        let regex = /\{\{([A-Z]+)\}\}/
        var lastEndIndex: String.Index = pattern.startIndex
        
        for match in pattern.matches(of: regex) {
            // string before tag
            var preMatchString = String(pattern[lastEndIndex..<match.0.startIndex])
            preMatchString = preMatchString.trimmingCharacters(in: .whitespaces)
            components.append(.text(preMatchString))
            
            // matched tag
            var matchString = String(match.output.1)
            matchString = matchString.trimmingCharacters(in: .whitespaces)
            // find corresponding view
            if let view = parameters.first(where: { $0.label == matchString })?.view {
                components.append(.view(view))
            }
            
            lastEndIndex = match.0.endIndex
        }
        
        // potential string after last found tag
        if lastEndIndex < pattern.endIndex {
            var postMatchString = String(pattern[lastEndIndex...])
            postMatchString = postMatchString.trimmingCharacters(in: .whitespaces)
            components.append(.text(postMatchString))
        }
    }
    
    @ViewBuilder
    private func buildExpression() -> some View {
        ForEach(Array(components.enumerated()), id: \.offset) { _, component in
            switch component {
                case .text(let string):
                    Text(string)
                case .view(let view):
                    view
            }
        }
    }
}

#Preview {
    RuleInputContentView()
        .frame(width: 300)
        .padding()
}

解决方案

问题根源

RuleInputExpressionParameter初始化时,直接将闭包返回的视图包装成AnyView并存储为常量。当父视图的toggleButton状态变化时,这个提前创建好的AnyView不会重新生成——它没有绑定到状态的更新流中,自然无法响应状态变化。

方案1:存储视图构建闭包(推荐)

修改参数结构,存储视图的构建闭包而非已创建的视图,这样每次状态变化时都能重新计算生成最新视图:

  1. 更新RuleInputExpressionParameter:
struct RuleInputExpressionParameter {
    let label: String
    let content: () -> AnyView
    
    init<Content: View>(label: String, _ content: @escaping () -> Content) {
        self.label = label
        self.content = { AnyView(content()) }
    }
}
  1. 修改RuleInputExpressionBuilder的Component枚举:
enum Component {
    case text(String)
    case content(() -> AnyView)
}
  1. 更新renderPattern方法中添加视图的逻辑:
// 替换原有的view添加逻辑
if let param = parameters.first(where: { $0.label == matchString }) {
    components.append(.content(param.content))
}
  1. 修改buildExpression方法,调用闭包生成视图:
@ViewBuilder
private func buildExpression() -> some View {
    ForEach(Array(components.enumerated()), id: \.offset) { _, component in
        switch component {
            case .text(let string):
                Text(string)
            case .content(let content):
                content()
        }
    }
}

方案2:监听状态变化(不推荐)

如果不想修改参数结构,可以让RuleInputExpressionBuilder接收父视图的状态绑定,在状态变化时重新调用renderPattern。但这种方式会让构建器和具体状态耦合,灵活性较差,仅适用于简单场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 19:14:52