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

SwiftUI中如何定义泛型KeyPath?用泛型简化同参异构类型代码

用泛型简化SwiftUI代码并实现泛型KeyPath

1. 抽象数据模型(消除重复类型代码)

因为MyCustomType1和MyCustomType2结构完全一致,先定义一个协议统一它们的公共属性,让两个类型遵守这个协议:

// 定义公共协议,包含两个类型共有的属性
protocol HasSize {
    var label: String { get }
    var size: CGSize { get }
}

// 让原有类型遵守协议(无需修改原有属性)
struct MyCustomType1: HasSize {
    var label: String
    var size: CGSize = .zero 
}

struct MyCustomType2: HasSize {
    var label: String
    var size: CGSize = .zero
}

2. 实现泛型View(合并重复的View代码)

把MyView1和MyView2合并成一个泛型View,同时支持泛型KeyPath和不同形状的显示,提供两种方案:

方案一:基于Shape协议的泛型(更灵活)

利用SwiftUI的Shape协议,让View接受任意形状类型,同时约束数据类型遵守HasSize:

struct ShapeDisplayView<T: HasSize, S: Shape>: View {
    // 泛型数据模型,必须遵守HasSize协议
    let value: T
    // 泛型KeyPath:从T类型映射到CGFloat
    let sizeKeyPath: KeyPath<T, CGFloat>
    // 要显示的形状,必须符合Shape协议
    let shape: S
    
    var body: some View {
        // 通过泛型KeyPath获取对应的值
        let targetSize = value[keyPath: sizeKeyPath]
        
        shape
            .fill(Color.black)
            .frame(width: targetSize, height: targetSize)
            .overlay(Text("\(targetSize)").foregroundColor(.white))
    }
}

方案二:用枚举指定形状(更简洁)

如果只需要固定几种形状,用枚举替代泛型Shape参数:

enum ShapeType {
    case circle, rectangle
}

struct ShapeDisplayView<T: HasSize>: View {
    let value: T
    let sizeKeyPath: KeyPath<T, CGFloat>
    let shapeType: ShapeType
    
    var body: some View {
        let targetSize = value[keyPath: sizeKeyPath]
        
        Group {
            switch shapeType {
            case .circle: Circle()
            case .rectangle: Rectangle()
            }
        }
        .fill(Color.black)
        .frame(width: targetSize, height: targetSize)
        .overlay(Text("\(targetSize)").foregroundColor(.white))
    }
}

3. 在ContentView中使用泛型View和KeyPath

现在可以用统一的泛型View替代原来的两个View,直接传入不同的数据、KeyPath和形状:

对应方案一的ContentView:

struct ContentView: View {
    let value1 = MyCustomType1(label: "Circle", size: CGSize(width: 100, height: 200))
    let value2 = MyCustomType2(label: "Rectangle", size: CGSize(width: 100, height: 200))
    
    var body: some View {
        VStack(spacing: 20) {
            // 传入Circle形状、MyCustomType1实例和对应的KeyPath
            ShapeDisplayView(
                value: value1,
                sizeKeyPath: \.size.height,
                shape: Circle()
            )
            
            // 传入Rectangle形状、MyCustomType2实例和对应的KeyPath
            ShapeDisplayView(
                value: value2,
                sizeKeyPath: \.size.width, // 也可以换用width的KeyPath
                shape: Rectangle()
            )
        }
        .padding()
    }
}

对应方案二的ContentView:

struct ContentView: View {
    let value1 = MyCustomType1(label: "Circle", size: CGSize(width: 100, height: 200))
    let value2 = MyCustomType2(label: "Rectangle", size: CGSize(width: 100, height: 200))
    
    var body: some View {
        VStack(spacing: 20) {
            ShapeDisplayView(
                value: value1,
                sizeKeyPath: \.size.height,
                shapeType: .circle
            )
            
            ShapeDisplayView(
                value: value2,
                sizeKeyPath: \.size.height,
                shapeType: .rectangle
            )
        }
        .padding()
    }
}

关于SwiftUI中泛型KeyPath的说明

在泛型View中定义泛型KeyPath的核心要点:

  • 给KeyPath的源类型加上泛型约束(比如T: HasSize),确保编译器知道该类型拥有KeyPath指向的属性
  • 定义KeyPath的类型为KeyPath<T, ValueType>,其中T是泛型参数,ValueType是你要获取的值的类型(这里是CGFloat)
  • 使用时直接传入形如\.size.height的KeyPath即可,编译器会自动推断对应的泛型类型

这样既消除了重复的类型和View代码,又实现了灵活的泛型KeyPath使用。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 21:29:54