小技巧:让任何类型遵循 Equatable
写 Swift 的同学们应该都不陌生 Equatable 吧。
让一个类型遵循 Equatable 需要提供一个静态函数 ==
,在某些情况下,我们无法直接通过计算或者直接比较属性值来返回是否相等时,我们又该如何处理呢?
一般情况
简单实现
对于左右侧值可比较的情况下,可以直接实现 ==
1 | public static func == (lhs: CustomType, rhs: CustomType) -> Bool { |
比对 ID
如果该类型遵循 Identifiable
,可以对比 ID 值来判断是否相等。
1 | public static func == (lhs: IdentifiableType, rhs: IdentifiableType) -> Bool { |
比对哈希值
如果该类型遵循 Hashable
,我们可以利用哈希值的唯一性来判断两个值是否相等。
1 | public static func == (lhs: HashableType, rhs: HashableType) -> Bool { |
某些情况是什么情况?
拿我的开源项目 MarkdownView
举个例子,有人提了一个 Feature Request 希望能够自定义各个层级的字体样式(#20)
参考之前更改字体的 modifier 和实现其实还不难,唯一的不同是 SwiftUI.Font
是一个具体的类型,而样式需要使用 ShapeStyle
,它并不是一个确切的类型,因此对于每一个字段我都使用类型擦除 AnyShapeStyle
来赋值。
在我看来,SwiftUI API 应该是能够根据当前状态实时响应的,也就是说和原生的 .foregroundStyle(some ShapeStyle)
一样,我可以使用 @State
实时切换。
字体和样式都是由 Provider 提供,注入到 Content Renderer 中,由 Content Renderer 统一分配,当样式变化后,需要重新渲染页面,这里就需要让 Providers 都遵循 Equatable,这样就可以判断值是否发生了变化来决定是否重新渲染。
FontProvider 中全是 SwiftUI.Font
,都已经包含了 Equatable。
ForegroundStyleProvider 中全是 SwiftUI.AnyShapeStyle
,没有提供 Equatable,且由于其特殊性,无法直接判断两个 ShapeStyle 是否相等。
这里可能有两种可能的解决方案:
不管实时变化的值,保留原样;
无论三七二十一,刷新页面。
都有取舍,不能满足我的需求。
巧用 CustomStringConvertible
我做的第一个尝试是在 Equatable 比较方法中使用 String(describing: lhs) == String(describing: rhs)
这样可以将 ShapeStyle 转换为一段文本描述,但是,并不解决问题。
1 | private var color = Color.red |
以上代码在程序中的表现为:第一次改变颜色没问题,但是再次改变后就没反应了。
AnyShapeStyle(storage: SwiftUI.AnyShapeStyle.Storage(box: SwiftUI.(unknown context at $1ae807e94).ColorBox<SwiftUI.SystemColorType>))
追根溯源
既然无法直接来判断两个 AnyShapeStyle
是否相同,那么我们就从他们的存储方式下手,任何一种类型都对应着「数据」,任何一个属性不同都会导致数据层面的变化。
如果两个值对应的数据一模一样,便可以认为他们是一致的。
这里可以直接使用 Swift 的 Data
类型,虽然不是 Equatable 的,但是遵循了 Hashable,可以大致判断出两个 Data 是否相同。
1 | public static func == (lhs: AnyShapeStyle, rhs: AnyShapeStyle) -> Bool { |
测试了下,完美!
这样一来,就可以保证页面根据样式配置实时刷新,同时保持按需渲染。
小技巧:让任何类型遵循 Equatable