Swift宏让Codable的使用更加简单
我很早之前的文章就写了有关Swift中JSON与Model的转换,回顾了自己使用的一些工具与经验,有兴趣的请点击会看:
现在Swift已经支持宏了,Codable可以更简单的嵌入到代码中,同时有AI的加持,让CV工作越来越简单。
不过关于Swift宏需要注意的是,目前Swift宏原生支持只支持SPM。
但是据我了解,很多项目开发还是通过Cocoapods集成的,所以在使用Codable宏的时候,考虑这个宏框架是否支持Cocoapods,当然在一个有Cocopods的项目中使用SPM也是可行的。
我找到两个相关的库同时支持SPM与Cocoapods,大家可以参考一下:
如果不想使用Codable的宏,自己稍微手动写一写也没有问题。
这里我有"被坑"经验分享给大家。这个坑也是我在之前文章中对JSON中的值转为Swift的枚举的后续。
转enum类型的注意事项
举个简单的例子,后端会传一个性别的JSON数据,数据如下:
json
{
"sexType": 0,// 0表示男性,1表示女性
}
在App端我写了这样一段代码
swift
enum SexType: Int {
case man
case woman
}
extension SexType: Codable {}
struct SexModel: Codable {
var sexType: SexType?
var name: String?
}
我们转换逻辑大致如下:
swift
let SexTypeJSONString = """
{
"sexType": 0,
"name": "season",
}
"""
let sexData = SexTypeJSONString.data(using: .utf8)!
let sexModel = try? JSONDecoder().decode(SexModel.self, from: sexData)
这样看,转换应该没啥问题。结果某一天后台传过来的数据出错了,数据以"sexType": 2,
传过来了,会转换成功吗?
这里大家可以试试动手试试?
有的同学会想,SexModel中定义的字段都是可选类型,那么应该name转换成功,sexType转换为nil。
真的是这样吗?实际上整个sexModel为nil,并且会抛出error!
但是放在这里,可能看不出来什么问题。
我们把这个逻辑放在Moya的网络请求中:
swift
provider.rx.request(Service.fake)
.map(SexModel.self)
.compactMap { $0 }
.asObservable()
.asSingle()
.subscribe { event in
switch event {
case .success(let model):
/// 模型转成功,将数据绑定到正常的页面
/// 构建正常页面
break
case .failure:
/// 模型转失败,显示异常的页面
/// 构建异常页面
break
}
}
.disposed(by: disposeBag)
如果按照之前认为model转换成功,只是sexType为nil,那么此刻会走success的逻辑,可能只是页面上的性别相关的显示异常。
但是实际上,会走到failure逻辑中,页面中不会显示任何用户想获取的信息。
想想看,如果一个JSON很多个字段,而且字段类型传的也是对的,只是恰好转Swift时,enum类型的匹配不上,页面异常了,尬不尬?
而且这种情况,很有可能是Android端仅有性别的UI异常了,而iOS端直接展示Error页面了。于是一定会有人丢出这句话:
为什么安卓是好的?iOS却不行?
想想就鸭梨山大。
JSON一口气转为enum固然美好,因为通过0或者1这样的字段表示某个状态,理解上总要"翻译"一下,但是这里带来的风险也需要开发者承担,一旦JSON返回的字段无法与枚举值匹配上,就会导致整个Model转换失败!
对于这个问题有以下几个解决方式:
- 模型转的时候,还是使用Int类型去接受,另起一个只读计算属性去手动转Int为SexType,这个方法可以规避Codable转化的异常,但是增加了手动写代码的逻辑,而且不同模型中的不同的枚举都需要自己写一份,维护成本高;
- 通过PropertyWrapper进行一次保底,这种属性包装器会允许转失败的时候向nil转换,或者附上默认值;
- 通过引入Swift宏,通过宏运算符进行异常守护;
下面是通过propertyWrapper改造的一个例子:
swift
@propertyWrapper
struct CanNilEnumType<Enum>: Codable where Enum: Codable, Enum: RawRepresentable, Enum.RawValue: Codable {
var wrappedValue: Enum?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(Enum.RawValue.self) {
wrappedValue = Enum(rawValue: value) ?? nil
} else {
wrappedValue = nil
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if let wrappedValue {
try? container.encode(wrappedValue.rawValue)
} else {
/// do Nothing
}
}
}
代码这样改造:
swift
struct SexModel: Codable {
/// 注意使用属性包装器修饰的属性只能用var修饰
@CanNilEnumType
var sexType: SexType?
}
有兴趣的朋友可以上网找更多的例子,比如兼容后台传Int类型或者String类的数字,Swift都可以通过String类型接住等等,这里只是一个抛砖引玉。
同时考虑到性能,Swift宏应该属性包装器更好。
同时有关Swift宏做异常处理的例子,上面分享的MetaCodable链接中有详细的文档说明:
swift
@Codable
struct CodableData {
@Default("some")
let field: String
}
总结
Codable是Swift中转Model的利器,同时也需要时时刻刻记住,Swift是一门强语言,任何数据类型的错误,任何数据匹配不上,都可能导致转模型失败,在书写Swift代码的同时,加强容错是我们需要考虑的,同时后端正确、正常的传值往往也是必要的。