小技巧:让任何类型遵循 Equatable

写 Swift 的同学们应该都不陌生 Equatable 吧。

让一个类型遵循 Equatable 需要提供一个静态函数 ==,在某些情况下,我们无法直接通过计算或者直接比较属性值来返回是否相等时,我们又该如何处理呢?

一般情况

简单实现

对于左右侧值可比较的情况下,可以直接实现 ==

1
2
3
public static func == (lhs: CustomType, rhs: CustomType) -> Bool {
return lhs.property1 == rhs.property1 && lhs.property2 == rhs.property2
}

比对 ID

如果该类型遵循 Identifiable,可以对比 ID 值来判断是否相等。

1
2
3
public static func == (lhs: IdentifiableType, rhs: IdentifiableType) -> Bool {    
return lhs.id == rhs.id
}

比对哈希值

如果该类型遵循 Hashable,我们可以利用哈希值的唯一性来判断两个值是否相等。

1
2
3
public static func == (lhs: HashableType, rhs: HashableType) -> Bool {
return lhs.hashValue == rhs.hashValue
}

某些情况是什么情况?

拿我的开源项目 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 是否相等。

这里可能有两种可能的解决方案:

  1. 不管实时变化的值,保留原样;

  2. 无论三七二十一,刷新页面。

都有取舍,不能满足我的需求。

巧用 CustomStringConvertible

我做的第一个尝试是在 Equatable 比较方法中使用 String(describing: lhs) == String(describing: rhs)

这样可以将 ShapeStyle 转换为一段文本描述,但是,并不解决问题。

1
2
3
4
5
6
7
8
9
@State private var color = Color.red

MarkdownView(...)
.foregroundStyle(color, for: .h1)
.onTapGesture {
color = Color(red: Double.random(in: 0...1),
green: Double.random(in: 0...1),
blue: Double.random(in: 0...1))
}

以上代码在程序中的表现为:第一次改变颜色没问题,但是再次改变后就没反应了。

AnyShapeStyle(storage: SwiftUI.AnyShapeStyle.Storage(box: SwiftUI.(unknown context at $1ae807e94).ColorBox<SwiftUI.SystemColorType>))

追根溯源

既然无法直接来判断两个 AnyShapeStyle 是否相同,那么我们就从他们的存储方式下手,任何一种类型都对应着「数据」,任何一个属性不同都会导致数据层面的变化。

如果两个值对应的数据一模一样,便可以认为他们是一致的。

这里可以直接使用 Swift 的 Data 类型,虽然不是 Equatable 的,但是遵循了 Hashable,可以大致判断出两个 Data 是否相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static func == (lhs: AnyShapeStyle, rhs: AnyShapeStyle) -> Bool {
let oldBuffer = withUnsafeBytes(of: oldValue) { $0 }
let newBuffer = withUnsafeBytes(of: newValue) { $0 }
if let oldPointer = oldBuffer.baseAddress,
let newPointer = newBuffer.baseAddress {
// Read bytes
let oldBytes = Data(bytes: oldPointer, count: oldBuffer.count)
let newBytes = Data(bytes: newPointer, count: newBuffer.count)

// Compare hash values of two data type
return oldBytes.hashValue == newBytes.hashValue
}
}

测试了下,完美!

这样一来,就可以保证页面根据样式配置实时刷新,同时保持按需渲染。

小技巧:让任何类型遵循 Equatable

https://liyanan2004.github.io/make-everything-equatable/

作者

LiYanan

发布于

2023-07-30

更新于

2023-07-30

许可协议

评论