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
相关产品推荐
相关产品推荐

