Swift 访问宏

@Access 宏旨在简化您 Swift 类型的细粒度访问级别控制。

@Access(emit: .fileprivate)
enum Action {
    case didStart
    case didFinish
}

// generates ⬇️

public struct ActionAccessor {
    let value: Action
    fileprivate init(_ value: Action) {
        self.value = value
    }
    fileprivate static let didStart = Self(.didStart)
    fileprivate static let didFinish = Self(.didFinish)
}

动机

The Composable Architecture 中,有一些关于 action 边界的讨论

想法是我们想要避免创建一个包含所有 action 的扁平 Action 枚举,而是选择一个更嵌套的结构

public enum Action {
    public enum Public: Equatable {
        case load(URL)
    }

    public enum Delegate: Equatable {
        case didFinishLoading
    }

    public enum Internal {
        case progressChanged(Double)
        case loadingFinished
    }

    case `public`(Public)
    case delegate(Delegate)
    case `internal`(Internal)
}

这样我们就可以对可理解的 action 子集编写详尽的 switch 语句,而不是退回到 default

我看到的这种方法的问题是,它本身并不能防止误用,任何 action 仍然可以在任何地方发出和读取,只是更难意外地做到这一点。通过自定义 lint 规则可以实现更好的情况,但我相信类型系统可以被利用来获得更好的解决方案。

@Access

@Access 宏创建一个 public struct,包装带注解的类型,并允许您分别指定 reademit 的访问级别。read 影响应用程序的哪个部分可以读取类型的实际值(例如,对 action 进行 switch)。emit 影响应用程序的哪个部分可以创建类型的实例。请参阅改进后的先前示例

public enum Action {
    // Public action can be created anywhere, can be read only in the file scope
    @Access(read: .fileprivate)
    public enum Public: Equatable {
        case load(URL)
    }

    // Delegate action can only be created in the file scope, but can be 
    // accessed anywhere
    @Access(emit: .fileprivate)
    public enum Delegate: Equatable {
        case didFinishLoading
    }

    // Fileprivate action inherits `fileprivate` modifier for both reading and emitting,
    // forbidding both outside of the file scope, but still letting the action 
    // be a part of a public enum
    @Access
    fileprivate enum Fileprivate {
        case progressChanged(Double)
        case loadingFinished
    }

    case `public`(PublicAccessor)
    case delegate(DelegateAccessor)
    case `fileprivate`(FileprivateAccessor)
}

通过将 action 声明放在与 TCA Reducer 相同的文件中,我们可以限制 Fileprivate action 仅在该文件中可见,同时允许父 reducer 读取 Delegate action 并发出 Public action,禁止其余的。这种方法的缺点是整个 action 无法使用单个 switch 进行切换,并且需要在每个 action.value 上使用单独的 switch 语句,但这可能有利于确保更少的 catch all default 语句,并且也可以通过对 Equatable 类型使用生成的 is 函数来缓解。

请参阅 AccessMacroClientAccessTests 以获取更易读的示例。

功能

case didStart
// of a wrapped type
// yields
fileprivate static let didStart = Self(.didStart)
// on a wrapper, keeping the action creation syntax intact
case didStart(at: Date)
// of a wrapped type
// yields
fileprivate static func didStart(at: Date) -> Self {
    return Self(.didStart(at: at))
}
// on a wrapper, keeping the action creation syntax intact