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

如何自动生成暴露@Published属性的只读Swift协议?

问题:自动生成包含@Published属性只读Publisher的协议

我正在进行重构工作,需要限制模型对象的写权限。现有模型是带有@Published属性的引用类型:

public final class Model {
  public enum State { /* Some cases here. */ }
  @Published public var state: State
}

当前通过ViewModelAPI协议暴露视图模型的只读访问权限,但ViewModel仍能修改Model的属性,且手动维护这类协议成本极高(Model更新时需同步修改协议)。希望自动生成协议,将所有@Published属性转为只读的AnyPublisher,想知道动态成员查找是否可行,或是需要脚本生成。


可行解决方案

1. 动态成员查找(Dynamic Member Lookup)

可以用@dynamicMemberLookup实现通用只读包装器,自动暴露所有@Published属性的Publisher,无需手动编写协议。

示例代码:

import Combine

@dynamicMemberLookup
public class ReadOnlyModelWrapper<Model: AnyObject> {
    private let model: Model
    
    public init(model: Model) {
        self.model = model
    }
    
    // 处理@Published属性,返回对应的AnyPublisher
    public subscript<Value>(dynamicMember keyPath: KeyPath<Model, Published<Value>.Publisher>) -> AnyPublisher<Value, Never> {
        model[keyPath: keyPath].eraseToAnyPublisher()
    }
    
    // 可选:支持普通只读属性的访问
    public subscript<Value>(dynamicMember keyPath: KeyPath<Model, Value>) -> Value {
        model[keyPath: keyPath]
    }
}

使用方式:

let model = Model()
let readOnlyWrapper = ReadOnlyModelWrapper(model: model)

// 直接通过动态成员访问Publisher
readOnlyWrapper.$state.sink { state in
    // 处理状态更新逻辑
}

优势:无需维护额外协议,Model更新时自动适配;缺点:编译时类型检查较弱,IDE自动补全体验略差。

2. 代码生成工具(Sourcery/SwiftGen)

如果需要强类型的协议,推荐用代码生成工具自动生成对应代码,以Sourcery为例:

  1. 编写Sourcery模板,扫描目标类的@Published属性,生成包含对应AnyPublisher只读属性的协议及扩展。
  2. 配置Sourcery在编译前自动执行,生成代码文件。

简化版模板示例:

{% for type in types.classes where type.annotations.contains("GenerateReadOnlyProtocol") %}
public protocol {{ type.name }}ReadOnly {
    {% for property in type.properties where property.attributes.contains("Published") %}
    var {{ property.name }}Publisher: AnyPublisher<{{ property.type.genericArguments.first.name }}, Never> { get }
    {% endfor %}
}

extension {{ type.name }}: {{ type.name }}ReadOnly {
    {% for property in type.properties where property.attributes.contains("Published") %}
    public var {{ property.name }}Publisher: AnyPublisher<{{ property.type.genericArguments.first.name }}, Never> {
        ${{ property.name }}.eraseToAnyPublisher()
    }
    {% endfor %}
}
{% endfor %}

给Model添加注解:

// sourcery: GenerateReadOnlyProtocol
public final class Model {
    public enum State { /* ... */ }
    @Published public var state: State
}

Sourcery会自动生成ModelReadOnly协议,ViewModel依赖该协议而非原始Model,既保证强类型安全,又无需手动维护协议内容。

3. 手动协议的简化写法

如果不想引入外部工具,可通过扩展自动实现协议,降低维护成本:

public protocol ModelReadOnly {
    var statePublisher: AnyPublisher<Model.State, Never> { get }
}

extension Model: ModelReadOnly {
    public var statePublisher: AnyPublisher<State, Never> {
        $state.eraseToAnyPublisher()
    }
}

Model更新时,仅需在协议中添加新的Publisher属性,并在扩展中实现对应逻辑即可,维护成本远低于原ViewModel方案。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 03:05:42