可差异化宏 (Diffable Macro)

概述

@Diffable 宏提供了一种有效的方法来计算类型的两个实例之间的差异,通过自动生成代码来比较属性。它确保您只专注于类型的高级设计,同时利用宏来实现高级功能。

要使用 @Diffable 宏,目标类型必须

  1. classstructenum
  2. 遵循 Equatable 协议。

该宏将为类型中所有 Equatable 属性生成优化的差异计算实现。

应用后,该宏会生成一个 computeDifference 方法,该方法计算两个 Config 实例之间的差异,并返回一个强类型结果,指示已更改的属性。

如何使用

@Diffable
public struct Config: Equatable {
    public var name: String
    public var id: UUID
}

// expandedSource
public struct Config: Equatable {
    public var name: String
    public var id: UUID

    public struct Difference: OptionSet {
        public let rawValue: Int        
        public init(rawValue: Int) {
            self.rawValue = rawValue
        }
        static let name = Difference(rawValue: 1 << 0)
        static let id = Difference(rawValue: 1 << 1)
    }

    public func computeDifference(from other: Self) -> Difference {
        var difference: Difference = []
        var currentCopy: Self? = self
        var otherCopy: Self? = other

        if currentCopy?.name != otherCopy?.name {
            difference.insert(.name)
        }
        
        if currentCopy?.id != otherCopy?.id {
            difference.insert(.id)
        }
        
        currentCopy = nil
        otherCopy = nil
        return difference
    }
}

// usage
let configOne = Config(name: "HEssam", id: UUID())
let configTwo = Config(name: "Alfred", id: UUID())

let differences = configOne.computeDifference(from: configTwo)

使用可差异化宏的示例

/// A custom UIView that displays a person's name and age.
/// This view uses a diffable configuration to detect and apply changes to its properties efficiently.
final class PersonView: UIView {
    
    // MARK: - UI Elements
    
    /// Label to display the person's name.
    private let nameLabel: UILabel = {
        let label = UILabel()
        label.font = .preferredFont(forTextStyle: .headline)
        return label
    }()
    
    /// Label to display the person's age.
    private let ageLabel: UILabel = {
        let label = UILabel()
        label.font = .preferredFont(forTextStyle: .subheadline)
        return label
    }()
    
    // MARK: - Properties
    
    /// The current configuration of the view.
    private var configuration: Configuration
    
    // MARK: - Initializers
    
    /// Initializes the view with the given configuration.
    ///
    /// - Parameter configuration: The initial configuration of the view.
    init(configuration: Configuration) {
        self.configuration = configuration
        super.init(frame: .zero)
    }
    
    @available(*, unavailable, message: "init(frame:) is not supported. Use init(configuration:) instead.")
    override init(frame: CGRect) {
        fatalError("init(frame:) has not been implemented")
    }
    
    @available(*, unavailable, message: "init(coder:) is not supported. Use init(configuration:) instead.")
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Configuration Update
    
    /// Updates the view's configuration and applies changes to the UI.
    ///
    /// - Parameter configuration: The new configuration to apply.
    func updateConfiguration(to configuration: Configuration) {
        // Compute the differences between the old and new configurations.
        let differences = self.configuration.computeDifference(from: configuration)
        
        // Iterate through potential differences and update the UI for changed properties.
        for i in 0 ..< Int.bitWidth {
            let bit = 1 << i
            let difference = Configuration.Difference(rawValue: bit)
            
            if differences.contains(difference) {
                switch difference {
                case .age:
                    // Update the age label if the age has changed.
                    self.ageLabel.text = "\(configuration.age)"
                    
                case .name:
                    // Update the name label if the name has changed.
                    self.nameLabel.text = configuration.name
                    
                default:
                    break
                }
            }
        }
        
        // Update the stored configuration.
        self.configuration = configuration
    }
    
    // MARK: - Configuration Struct
    
    /// Represents the configuration of the `PersonView`.
    /// This struct uses the `@Diffable` macro to enable efficient change detection.
    @Diffable
    struct Configuration: Equatable {
        /// The person's name.
        let name: String
        /// The person's age.
        let age: Int
    }
}

错误处理

DiffableMacroError 枚举提供了详细的错误情况,以帮助开发人员调试应用宏时的问题。

错误情况

shouldBeClassOrStructOrEnum

shouldConformToEquatableProtocol

贡献

我们热烈欢迎您为 BuildableMacro 项目做出贡献!无论是修复错误、改进文档还是添加新功能,我们都感谢您的帮助。以下是如何贡献

  1. Fork 仓库:首先将仓库 Fork 到您自己的 GitHub 帐户。
  2. 创建分支:在新分支中进行更改。
  3. 进行更改:无论是新功能还是错误修复,您的贡献都会有所作为。
  4. 编写测试:确保您的更改按预期工作。
  5. 提交 Pull Request:一旦您满意,请提交 Pull Request 以供审核。

为了明确我们对成员的期望,我们采用了 Contributor Covenant 定义的行为准则。 该文档在许多开源社区中使用。 更多信息请参见 行为准则

Contributor Covenant

许可

请查看 LICENSE 了解详细信息。