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

SwiftUI视图全元素截图及复用组件独立状态实现问题

SwiftUI 牙齿组件独立状态管理方案

问题说明

之前完成了SwiftUI视图全元素截图功能,但现在所有复用的牙齿组件共享同一状态:点击任意组件的按钮(比如切换颜色、显示划线),同类型的所有组件状态都会同步变化。需要实现每个组件状态独立,比如修改18号组件的中心按钮颜色时,其他组件不受影响。

问题根源

当前代码中,所有方形组件共享odontogramShapeColorProperties,所有圆形组件共享odontogramCircularColorProperties,导致状态联动。每个组件需要拥有独立的OdontogramColorsProperties实例。

解决方案

通过字典存储每个牙齿编号对应的独立状态,让每个组件绑定自己专属的状态实例:

1. 修改OdontogramView的状态定义

将单个状态变量替换为字典,键为牙齿编号字符串,值为对应组件的状态:

@State private var squareComponentStates: [String: OdontogramColorsProperties] = [:]
@State private var circleComponentStates: [String: OdontogramColorsProperties] = [:]

2. 初始化所有牙齿的状态

在视图初始化时,为所有需要的牙齿编号创建独立的状态实例:

init() {
    // 初始化方形组件的所有牙齿编号状态
    let squareNumbers = getIntervalNumbers(starts: 18, end: 11, ascend: false) +
                        getIntervalNumbers(starts: 21, end: 28, ascend: true) +
                        getIntervalNumbers(starts: 48, end: 41, ascend: false) +
                        getIntervalNumbers(starts: 31, end: 38, ascend: true)
    for number in squareNumbers {
        squareComponentStates[number] = OdontogramColorsProperties()
    }
    
    // 初始化圆形组件的所有牙齿编号状态
    let circleNumbers = getIntervalNumbers(starts: 55, end: 51, ascend: false) +
                        getIntervalNumbers(starts: 61, end: 65, ascend: true) +
                        getIntervalNumbers(starts: 85, end: 81, ascend: false) +
                        getIntervalNumbers(starts: 71, end: 75, ascend: true)
    for number in circleNumbers {
        circleComponentStates[number] = OdontogramColorsProperties()
    }
}

3. 传递独立状态给组件

在创建组件时,从字典中取出对应编号的状态绑定:
比如方形组件的遍历部分修改为:

ForEach(getIntervalNumbers(starts: 18, end: 11, ascend: false), id: \.self) { number in
    if let state = $squareComponentStates[number] {
        OdontogramSquareComponenteView(buttonState: state, topNumber: number, bottomNumber: nil)
            .frame(width: 250, height: 250, alignment: .center)
            .padding(.all, 20)
    }
}

圆形组件同理,使用$circleComponentStates[number]获取绑定。

完整修改后的OdontogramView代码

import SwiftUI

struct OdontogramView: View {
    
    @Environment(\.dismiss) var dismiss
    @Environment(\.displayScale) var displayScale
    
    @State private var sheetPresented : Bool = false
    @State private var showSavingDataAlert = false
    
    // 用字典存储每个牙齿编号的独立状态
    @State private var squareComponentStates: [String: OdontogramColorsProperties] = [:]
    @State private var circleComponentStates: [String: OdontogramColorsProperties] = [:]
    
    @ObservedObject private var viewModel = AddRecordViewModel()
    
    init() {
        // 初始化所有方形组件的状态
        let squareNumbers = getIntervalNumbers(starts: 18, end: 11, ascend: false) +
                            getIntervalNumbers(starts: 21, end: 28, ascend: true) +
                            getIntervalNumbers(starts: 48, end: 41, ascend: false) +
                            getIntervalNumbers(starts: 31, end: 38, ascend: true)
        for number in squareNumbers {
            squareComponentStates[number] = OdontogramColorsProperties()
        }
        
        // 初始化所有圆形组件的状态
        let circleNumbers = getIntervalNumbers(starts: 55, end: 51, ascend: false) +
                            getIntervalNumbers(starts: 61, end: 65, ascend: true) +
                            getIntervalNumbers(starts: 85, end: 81, ascend: false) +
                            getIntervalNumbers(starts: 71, end: 75, ascend: true)
        for number in circleNumbers {
            circleComponentStates[number] = OdontogramColorsProperties()
        }
    }
    
    var body: some View {
        NavigationView {
            ScrollView([.vertical,.horizontal], showsIndicators: true) {
                odontogramElements()
            }
            .navigationBarTitle(Text(NSLocalizedString("odontogramTitle", comment: "title")), displayMode: .inline)
            .navigationBarItems(trailing: Button(action: {
                self.sheetPresented.toggle()
            }) {
                Text(NSLocalizedString("addrecords_view_buttonTitleLeft", comment: ""))
                    .bold()
            })
            .navigationBarItems(trailing: Button(action: {
                print("Help")
            }, label: {
                Image(systemName: "questionmark.circle")
                    .resizable()
                    .frame(width: 25, height: 25)
            }))
            .navigationBarItems(leading: Button(action: {
                dismiss()
            }) {
                Text(NSLocalizedString("addrecords_view_buttonTitleRight", comment: ""))
                    .bold()
                    .foregroundColor(.red)
            })
            
            .sheet(isPresented: $sheetPresented, content: {
                if let data = render() {
                    CaptureScrollViewViewController(activityItems: [data])
                }
            })
        }
        .navigationViewStyle(.stack)
    }
    
    private func getIntervalNumbers(starts: Int, end: Int, ascend: Bool) -> [String] {
        var result: [String] = []
        if ascend {
            for num in starts...end {
                result.append(String(num))
            }
        } else {
            for num in (end...starts).reversed() {
                result.append(String(num))
            }
        }
        return result
    }
}

extension OdontogramView {
    private func odontogramElements() -> some View {
        VStack {
            // MARK: - First Section
            HStack(spacing: 20) {
                // Left section | Shape
                ForEach(getIntervalNumbers(starts: 18, end: 11, ascend: false), id: \.self) { number in
                    if let state = $squareComponentStates[number] {
                        OdontogramSquareComponenteView(buttonState: state, topNumber: number, bottomNumber: nil)
                            .frame(width: 250, height: 250, alignment: .center)
                            .padding(.all, 20)
                    }
                }
                Divider()
                    .frame(width: 5)
                    .overlay(.black)
                // Right | Shape
                ForEach(getIntervalNumbers(starts: 21, end: 28, ascend: true), id: \.self) { number in
                    if let state = $squareComponentStates[number] {
                        OdontogramSquareComponenteView(buttonState: state, topNumber: number, bottomNumber: nil)
                            .frame(width: 250, height: 250, alignment: .center)
                            .padding(.all, 20)
                    }
                }
            }
            // MARK: - Second section
            HStack(spacing: 20) {
                // Left | Circle
                ForEach(getIntervalNumbers(starts: 55, end: 51, ascend: false), id: \.self) { number in
                    if let state = $circleComponentStates[number] {
                        OdontogramCircleComponentView(buttonState: state, topNumber: number, bottomNumber: nil)
                            .frame(width: 250, height: 250, alignment: .center)
                            .padding(.all, 20)
                    }
                }
                Divider()
                    .frame(width: 5)
                    .overlay(.black)
                // Right - Circle
                ForEach(getIntervalNumbers(starts: 61, end: 65, ascend: true), id: \.self) { number in
                    if let state = $circleComponentStates[number] {
                        OdontogramCircleComponentView(buttonState: state, topNumber: number, bottomNumber: nil)
                            .frame(width: 250, height: 250, alignment: .center)
                            .padding(.all, 20)
                    }
                }
            }
            Divider()
                .frame(height: 5)
                .overlay(.black)
            // MARK: - Third section
            HStack(spacing: 20) {
                // Left | Shape
                ForEach(getIntervalNumbers(starts: 85, end: 81, ascend: false), id: \.self) { number in
                    if let state = $circleComponentStates[number] {
                        OdontogramCircleComponentView(buttonState: state, topNumber: nil, bottomNumber: number)
                            .frame(width: 250, height: 250, alignment: .center)
                            .padding(.all, 20)
                    }
                }
                Divider()
                    .frame(width: 5)
                    .overlay(.black)
                // Right | Shape
                ForEach(getIntervalNumbers(starts: 71, end: 75, ascend: true), id: \.self) { number in
                    if let state = $circleComponentStates[number] {
                        OdontogramCircleComponentView(buttonState: state, topNumber: nil, bottomNumber: number)
                            .frame(width: 250, height: 250, alignment: .center)
                            .padding(.all, 20)
                    }
                }
            }
            // MARK: - Fourth section
            HStack(spacing: 20) {
                // Left | Circle
                ForEach(getIntervalNumbers(starts: 48, end: 41, ascend: false), id: \.self) { number in
                    if let state = $squareComponentStates[number] {
                        OdontogramSquareComponenteView(buttonState: state, topNumber: nil, bottomNumber: number)
                            .frame(width: 250, height: 250, alignment: .center)
                            .padding(.all, 20)
                    }
                }
                Divider()
                    .frame(width: 5)
                    .overlay(.black)
                // Right | Circle
                ForEach(getIntervalNumbers(starts: 31, end: 38, ascend: true), id: \.self) { number in
                    if let state = $squareComponentStates[number] {
                        OdontogramSquareComponenteView(buttonState: state, topNumber: nil, bottomNumber: number)
                            .frame(width: 250, height: 250, alignment: .center)
                            .padding(.all, 20)
                    }
                }
            }
        }
    }
}

extension OdontogramView {
    @MainActor
    private func render() -> UIImage? {
        let odontogramElements = odontogramElements()
        let renderer = ImageRenderer(content: odontogramElements)
        renderer.scale = displayScale
        return renderer.uiImage
    }
}

组件代码无需修改

OdontogramSquareComponenteView和OdontogramCircleComponentView的代码保持原样即可,因为它们只是接收独立的状态绑定,内部逻辑不需要调整。

效果说明

修改后,每个牙齿组件都拥有独立的状态实例,点击任意组件的按钮只会改变当前组件的颜色、划线等状态,不会影响其他组件,同时截图功能依然正常工作。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 19:52:01