最近我接触了一个新项目,这个项目是纯swift 开发。这激起了我对swift 的兴趣,
前言
之前都是拿swift 做一些demo ,没有用在项目中。这回可以大手大脚的开始写swift 。早就对swift 的函数式编程感兴趣。这回终于如愿所偿了。先说下。项目中用到的第三方库。
Toaster
弹出一些提示信息
SnapKit
swift 中的约束库, 和objective-c中的masonry 语法非常相似。只要会用masonry的,这个库可以无脑上手。
Alamofire
这个不用说,swift 版的网络请求库
ReactiveCocoa
和ReactiveSwift
,因为项目是MVVM 开发模式。所以这两个库是必须要有的。吐槽一句,这两个库用起来太方便了。
Result
配合
ReactiveCocoa
和
ReactiveSwift
。它俩天生就是一对
Argo
+
Curry
+
Runes
这三个库就是完成网络数据转换成模型的。也是今天要介绍的重点。
例子
好了,废话不多说。先来看一个现成的例子吧:
guard let dataArr = tool["data"] as? [JSONData], dataArr.count > 0 else {
return
}
let tools = dataArr.flatMap { (dict: JSONData) -> Tool? in
return decode(dict)
}
public struct Tool {
let id: String
let type: String
let name: String
let code: String
let url: String?
let iconUrl: String?
}
extension Tool: Decodable {
public static func decode(_ json: JSON) -> Decoded {
return curry(Tool.init)
<^> json <| "id"
<*> json <| "type"
<*> json <| "name"
<*> json <| "code"
<*> json <|? "url"
<*> json <|? "iconUrl"
}
}
看完这个例子有什么感想(ps: 我第一次看见的时候感觉这什么东西,就这几句话就完成模型转换了。swift也太神奇了吧!)。看不懂没关系。下面我们一层一层的来解释这几行代码。
正文
1.首先解释下这个decode
方法。这个是Decodable
协议中的方法。自定义的数据结构只要遵循Decodable
协议,实现decode
方法就可以完成模型转换。
- JSON 参数。这个是argo中定义的数据结构。
public enum JSON {
case object([String: JSON])
case array([JSON])
case string(String)
case number(NSNumber)
case bool(Bool)
case null
}
public extension JSON {
/**
Transform an `Any` instance into `JSON`.
This is used to move from a loosely typed object (like those returned from
`NSJSONSerialization`) to the strongly typed `JSON` tree structure.
- parameter json: A loosely typed object
*/
init(_ json: Any) {
switch json {
case let v as [Any]:
self = .array(v.map(JSON.init))
case let v as [String: Any]:
self = .object(v.map(JSON.init))
case let v as String:
self = .string(v)
case let v as NSNumber:
if v.isBool {
self = .bool(v.boolValue)
} else {
self = .number(v)
}
default:
self = .null
}
}
}
这是它的定义。可以看到,这个JSON 就是对网络数据的枚举化。JSON 可以适配所有的网络数据。
3.拿到网络数据后调用了下面
let tools = dataArr.flatMap { (dict: JSONData) -> Tool? in
return decode(dict)
}
这句会调用Tool的decode方法,将网络数据dict 传入decode方法中,decode 会将网络数据转化成Tool对象(也有可能转换不成功返回nil),最后用flatMap对无效数据过滤。最后就得到了我们想要的模型了。所以所有的关键就是decode方法了。
4.先抛开curry 函数不说。我们先来看下
<^> json <| "id"
<*> json <| "type"
这两行是什么意思。
<^>,** <|,<>* 都是自定义运算符。在Runes中定义了
写到这里有要说说swift 中的自定义运算符了, 链接。运算符要指定结合性和优先级。拿 RunesApplicativePrecedence来说这组运算符是左结合,优先级比 NilCoalescingPrecedence低,比 RunesAlternativePrecedence高,而 NilCoalescingPrecedence是swift 中定义的标准运算符组 ??所属的优先级。[swift中常用的运算符对应的优先级]( http://www.jianshu.com/p/4f025476701a)。而 RunesAlternativePrecedence又是自定义的运算符优先级组
precedencegroup RunesAlternativePrecedence {
associativity: left
higherThan: LogicalConjunctionPrecedence
lowerThan: ComparisonPrecedence
}
这个优先级组比 比较运算符 低,比&&高。
所以 <^>比&&高,比 ??低。
在来看<| 。
可见这个运算符比
RunesApplicativeSequencePrecedence
高。而
RunesApplicativeSequencePrecedence
比
RunesApplicativePrecedence
高。所以
<|比
<^>高。
所以
<^> json <| "id"
中先执行的是json <| "id"
然后在执行的<^>
5.<|
表达式执行过程:
这个函数会对每一个key 调用
decodedJSON
方法,这个方法会对数据模型转换。如果成功会转换成
.success()。如果失败会变成
.typeMismatch或者
.missingKey。
public func decodedJSON(_ json: JSON, forKey key: String) -> Decoded {
switch json {
case let .object(o): return guardNull(key, o[key] ?? .null)
default: return .typeMismatch(expected: "Object", actual: json)
}
}
private func guardNull(_ key: String, _ json: JSON) -> Decoded {
switch json {
case .null: return .missingKey(key)
default: return pure(json)
}
}
public func pure(_ x: T) -> Decoded {
return .success(x)
}
最后得到一个decoded对象。调用A.decode方法。如果是success的会对数据解包。如果失败会返回一个错误。
public extension Decoded {
/**
Conditionally map a function over `self`, flattening the result.
- If `self` is `.Failure`, the function will not be evaluated and this will
return `.Failure`.
- If `self` is `.Success`, the function will be applied to the unwrapped
value.
- parameter f: A transformation function from type `T` to type `Decoded`
- returns: A value of type `Decoded`
*/
func flatMap(_ f: (T) -> Decoded) -> Decoded {
switch self {
case let .success(value): return f(value)
case let .failure(error): return .failure(error)
}
}
}
6.在argo库中有一个文件StandardTypes.swift
这里面定义的对基本数据的decode方法。这就是完整的argo 从网络数据转换成模型的过程。有兴趣的可以看下argo 这个库的源码。Argo ,Rune。
extension String: Decodable {
/**
Decode `JSON` into `Decoded`.
Succeeds if the value is a string, otherwise it returns a type mismatch.
- parameter json: The `JSON` value to decode
- returns: A decoded `String` value
*/
public static func decode(_ json: JSON) -> Decoded {
switch json {
case let .string(s): return pure(s)
default: return .typeMismatch(expected: "String", actual: json)
}
}
}
extension Int: Decodable {
/**
Decode `JSON` into `Decoded`.
Succeeds if the value is a number that can be converted to an `Int`,
otherwise it returns a type mismatch.
- parameter json: The `JSON` value to decode
- returns: A decoded `Int` value
*/
public static func decode(_ json: JSON) -> Decoded {
switch json {
case let .number(n): return pure(n.intValue)
default: return .typeMismatch(expected: "Int", actual: json)
}
}
}
......
7.说完了argo 在来看之前的代码。是不是明白了很多。但是还有一点。那就是Curry函数。那下面就来说道说道Curry函数吧。
Curry化技术是一种通过把多个参数填充到函数体中,实现将函数转换为一个新的经过简化的(使之接受的参数更少)函数的技术
柯里化函数就是接受一个参数然后返回另一个带有参数的函数。可以看到Curry函数最终会调用
function(a, b)
,
function(a, b,c)
等方法。所以在最初的代码中。
return curry(Tool.init)
<^> json <| "id"
<*> json <| "type"
<*> json <| "name"
<*> json <| "code"
<*> json <|? "url"
<*> json <|? "iconUrl"
转化后就是Tool.init(id,type,name,code,url,iconUrl) 方法。就是结构体自带的默认初始化方法。
所以到这里就全部解开了。argo+Curry+Runes 将网络数据转换成model的过程了。