十六进制字符串转UIColor

支持十六进制字符串的格式有:

  • #FFF
  • #FFFF
  • #FFFFFF
  • #FFFFFFFF
  • 0xFFF
  • 0xFFFF
  • 0xFFFFFF
  • 0xFFFFFFFF
  • 可省略#0x符号
  • 不区分大小写
1
2
3
4
5
@interface UIColor (XP)
+ (instancetype)hexColor:(NSString *)hex;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@implementation UIColor (XP)
+ (instancetype)hexColor:(NSString *)hex {
NSString *str = hex;
if ([hex hasPrefix:@"#"]) {
str = [hex substringFromIndex:1];
} else if ([hex.lowercaseString hasPrefix:@"0x"]) {
str = [hex substringFromIndex:2];
}
NSScanner *scanner = [[NSScanner alloc] initWithString:str];
unsigned int hexValue;
if ([scanner scanHexInt:&hexValue]) {
CGFloat r, g, b, a = 1.0;
switch (str.length) {
case 3:
r = ((hexValue & 0xF00) >> 8) / 15.0;
g = ((hexValue & 0x0F0) >> 4) / 15.0;
b = (hexValue & 0x00F) / 15.0;
break;
case 4:
r = ((hexValue & 0xF000) >> 12) / 15.0;
g = ((hexValue & 0x0F00) >> 8) / 15.0;
b = ((hexValue & 0x00F0) >> 4) / 15.0;
a = (hexValue & 0x000F) / 15.0;
break;
case 6:
r = ((hexValue & 0xFF0000) >> 16) / 255.0;
g = ((hexValue & 0x00FF00) >> 8) / 255.0;
b = (hexValue & 0x0000FF) / 255.0;
break;
case 8:
r = ((hexValue & 0xFF000000) >> 24) / 255.0;
g = ((hexValue & 0x00FF0000) >> 16) / 255.0;
b = ((hexValue & 0x0000FF00) >> 8) / 255.0;
a = (hexValue & 0x000000FF) / 255.0;
break;
default:
return nil;
}
if (@available(iOS 10.0, *)) {
return [UIColor colorWithDisplayP3Red:r green:g blue:b alpha:a];
}
return [UIColor colorWithRed:r green:g blue:b alpha:a];
}
return nil;
}
@end

修改DZNEmptyDataSet按钮的宽高

相信没有哪款App是没有使用到滚动视图(UIScrollView、UITableView、UICollectionView)的吧,我相信大家用的最多的就是UITableView了,当需要展示列表数据时,我想大家第一次时间想到的就是UITableView。

如果数据不为空的时候还好,可谁也不能保证一定会有数据展示吧,如果没数据的时候,就会给客户呈现一个空白页面(白板),此时我们可以给用户一点提示信息(图片、文字),这不仅可以提升用户体验,也不至于让用户一脸懵逼。

DZNEmptyDataSet 就是为此而生的,只需设置好数据源和代理即可,然后根据你的需求返回对应的图片或者提示文字。

但是 DZNEmptyDataSet 提供的按钮默认不能调整宽高,如果你实现了该数据源方法 - (UIImage *)buttonBackgroundImageForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state 并返回对应按钮背景图片后,DZNEmptyDataSet 会将按钮高度自动调整为图片的高度;即便如此,还是无法调整宽度。

由于 DZNEmptyDataSet 并没有提供相应的接口给我们设置按钮的宽高,那我们只能通过曲线救国的方式来达到目的了。

遵守 DZNEmptyDataSetDelegate 协议,实现 - (void)emptyDataSetDidAppear:(UIScrollView *)scrollView 方法,然后修改按钮的约束。

不要在 - (void)emptyDataSetWillAppear:(UIScrollView *)scrollView 这个方法里设置。宽度只能通过调整 leadingtrailing 的约束来达到效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)emptyDataSetDidAppear:(UIScrollView *)scrollView {
UIButton *button = [scrollView valueForKeyPath:@"emptyDataSetView.button"];
if ([button isKindOfClass:[UIButton class]]) {
// Change button width
for (NSLayoutConstraint *constraint in button.superview.constraints) {
if (constraint.firstItem == button && constraint.firstAttribute == NSLayoutAttributeLeading) {
constraint.constant = 130.0;
} else if (constraint.secondItem == button && constraint.secondAttribute == NSLayoutAttributeTrailing) {
constraint.constant = 130.0;
}
}
// Change button height
for (NSLayoutConstraint *constraint in button.constraints) {
if (constraint.firstItem == button && constraint.firstAttribute == NSLayoutAttributeHeight) {
constraint.constant = 40.0;
}
}
}
}

使用 unwind segues 关闭当前页面

我们经常会遇到这样一种需求,在控制器上有一个关闭按钮,点击来关闭当前页面。而我们通常的做法都是在 Storyboard 上拖一个 UIButton 出来,然后将 UIButton 拖线到控制器上,新建一个 IBAction 方法,并在该方法中通过代码来关闭当前控制器。

其实,我们可以不需要这么麻烦,可以通过 Unwind Segue 在 Storyboard 上拖拖线即可达到关闭控制器的效果。

创建一个 Unwind Segue

unwind是一个实例方法,以UIStoryboardSegue作为其唯一的方法参数,返回类型为IBAction,它对方法名称不作要求,也不需要在头文件中进行声明。

需要注意的是,我们需要将 uniwnd action 定义在 UIStoryboardSegue 的 sourceViewController 上,而非当前控制器。例如 A push B 然后从 B 返回 A 这个场景,我们需要将 unwind action 定义在 A 上。

但是我们可以通过扩展 UIViewController 以便所有控制器均可以使用,从而不用关心具体写在哪个控制器上。

1
2
3
- (IBAction)unwindSegue:(UIStoryboardSegue*)sender {
// Pull any data from the view controller which initiated the unwind segue.
}
1
2
3
4
5
6
7
extension UIViewController {
@IBAction func unwindSegue(_ sender: UIStoryboardSegue) {
// Pull any data from the view controller which initiated the unwind segue.
}
}

将 Unwind Segue 添加到故事板

  • 方法1

a. 选中退出图标,右击弹出 Segues 列表

b. 将 unwindSegue: 拖线到控制器图标上

c. 设置一个 identifier

经过这样的拖线绑定之后,我们就可以在代码中通过发送一个 performSegue(withIdentifier:sender:) 消息来关闭当前控制器。

  • 方法2

a. 选中按钮,然后按住 control 拖线到退出图标上

b. 在弹出的 Action Segues 中选择 unwindSegue:

这样,当我们点击按钮的时候,就会自动关闭当前控制器了。

Technical Note TN2298 Using Unwind Segues

在Markdown中创建表格

越来越多人采用Markdown进行写作,而表格作为一种比较常见的需求,Markdown也支持快速创建表格。

语法:

1
2
3
4
| Name | Score |
| :- | :-: |
| zhangsan | 90 |
| lisi | 100 |

效果:

Name Score
zhangsan 90
lisi 100

语法说明:

  • 第一行为表头,第二行为分割表头和表格内容,第三行开始每一行表示一个表格行
  • 列与列之间用|进行分割
  • 第二行中可以指定内容对其模式
    • - 默认对其
    • :- 左对齐
    • -: 右对齐
    • :-: 居中对齐

博客搬迁至阿里云

前因

清明假期前,收到阿里云发来的邮件,说我的域名未指向阿里云国内节点(不含香港)服务器,根据工信部相关法规规定属于空壳网站,需要我尽快处理,否则将删除网站备案接入信息,甚至网站备案号有可能会被注销。

email

我的域名之前确实是指向了国外服务器,我在 GitHub 通过 pages 服务搭建了一个 hexo 博客,并通过 CNAME 解析将域名绑定到了 GitHub pages 上

收到邮件当天,我就将 hexo 博客从 GitHub 迁移到了码云上,后来发现码云无法绑定域名,遂将博客继续搬迁到了Coding上,然后绑定了我的域名并修改了域名解析,将域名指向了 Coding Pages;至此,我心想Coding.net毕竟是国内公司,我把博客托管至它提供的服务器上,应该没事了吧。

后果

不曾想,几个工作日后,在4月9日收到工信部备案系统发来的短信,说我的网站备案号粤ICP备15020585已被收回,网站信息被注销了,域名也访问不了了。

容我悲伤一会 :(

痛定思痛

发现将博客托管在第三方提供的服务器上已无望,没办法,只好花点软妹币购买阿里云的服务器了。在经过两天的思量后(其实还不是因为穷),花了RMB199软妹币购买了阿里云的弹性Web托管服务。

接下来就是重新备案了,这个没什么好说的,先进入弹性Web托管控制台,然后去申请一个备案服务号,然后拿着这个备案服务号去申请备案,填写网站基本信息,上传资料,提交审核。一周左右,我的备案就通过了。

重新上线我的博客

服务器买好了,备案也审核通过了,下面就开始将我的博客重新上线了。

  • 上传博客内容文件

hexo 博客生成的静态页面,全部存储在 public 目录下,我们只需将该目录下的文件上传到服务器的 htdocs 目录下即可。除了用FTP软件上传之外,还可以通过 hexo 提供的一键部署功能,将网站部署到服务器上。

  • 绑定域名

进入弹性Web托管控制台,分别绑定 www.0daybug.com0daybug.com

进入域名解析控制台,添加如下两条域名解析,其中记录值为弹性Web托管中的测试域名

记录类型 主机记录 解析线路(isp) 记录值
CNAME @ 默认 ew-*.aliapp.com
CNAME www 默认 ew-*.aliapp.com

至此,我的博客又重新上线了~~~

Swift选项集合(OptionSet)

Objective-C中的NS_OPTION宏

对于iOS开发者来说,NS_OPTION这个宏应该都不会陌生吧,这个宏给我们的开发带来了极大的便利。

1
2
3
4
5
6
7
8
9
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

Objective-C中可以通过|来组合选项,通过&来判断给定的选项中是否包含某个成员。

1
2
3
4
UIViewAutoresizing autoresizing = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if (autoresizing & UIViewAutoresizingFlexibleWidth) {
...
}

通过|=来追加选项。

1
autoresizing |= UIViewAutoresizingFlexibleLeftMargin;

OptionSet协议

Swift使用struct来遵从OptionSet协议,以引入选项集合,而非enum。为什么这样处理呢?当枚举成员互斥的时候,比如说,一次只有一个选项可以被选择的情况下,枚举是非常好的。但是和 C 不同,在 Swift 中,你无法把多个枚举成员组合成一个值,而 C 中的枚举对编译器来说就是整型,可以接受任意整数值。

1
2
3
4
5
6
7
8
9
10
11
struct Hobbies: OptionSet {
var rawValue: UInt
static let none = Hobbies(rawValue: 0) // 这人很懒,居然没任何爱好😂
static let basketball = Hobbies(rawValue: 1 << 0)
static let football = Hobbies(rawValue: 1 << 1)
static let baseball = Hobbies(rawValue: 1 << 2)
static let swim = Hobbies(rawValue: 1 << 3)
static let badminton = Hobbies(rawValue: 1 << 4)
static let all: Hobbies = [.basketball, .football, .baseball, .swim, .badminton]
}

和C一样,Swift中的选项集合结构体使用了高效的位域来表示,但是这个结构体本身表现为一个集合,它的成员则为被选择的选项。这允许你使用标准的集合运算来维护位域,比如使用contains(_:)来检验集合中是否有某个成员,或者是用union(_:)来组合两个位域,通过remove(_:)来移除某个成员。另外,由于OptionSet继承于ExpressibleByArrayLiteral,你可以使用数组字面量来生成一个选项集合。

1
2
3
4
5
6
7
var hobbies: Hobbies = [.football, .swim]
_ = hobbies.contains(.football) // true
_ = hobbies.contains(.baseball) // false
// 增加`.badminton`选项
hobbies = hobbies.union(.badminton)
// 移除`.football`选项
hobbies.remove(.football)

源码

你可以在GitHub上查看到OptionSet的源码swift/stdlib/public/core/OptionSet.swift,如果你有兴趣。

UICollectionView瀑布流布局

仅作为学习交流之用

环境

  • Xcode9.0+
  • Swift4.0+

TODO

  • 废弃CollectionViewWaterfallLayoutcontentInset属性,改用UICollectionView的contentInset属性。

一开始是采用UICollectionView.contentInset来做四周留白的,但是后来发现一旦设置了UICollectionView.contentInset.left的值且>0的时候,整个UICollectionView就会一片空白,但是top、bottom、right属性均不受此影响。测试了网上的一些Demo也发现存在此问题,具体原因不详,如有知道的还望赐教。

鉴于此,暂时给CollectionViewWaterfallLayout增加了一个contentInset属性。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
protocol CollectionViewDelegateWaterfallLayout {
/// 返回每个cell的高度
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, heightForItemAt indexPath: IndexPath, itemWidth width: CGFloat) -> CGFloat
}
class CollectionViewWaterfallLayout: UICollectionViewLayout {
/// 纵列数量
var column: Int = 2
/// 每一行cell之间的间距
var minimumLineSpacing: CGFloat = 5.0
/// 每一纵列cell之间的间距
var minimumInteritemSpacing: CGFloat = 5.0
///
var delegate: CollectionViewDelegateWaterfallLayout?
/// 内容偏移
var contentInset: UIEdgeInsets = .zero
/// 每个纵列的Y值偏移量
private var maxYForColumn = [CGFloat]()
/// 布局数据
private var layoutAttributes = [UICollectionViewLayoutAttributes]()
override var collectionViewContentSize: CGSize {
guard let max = maxYForColumn.max() else { return .zero }
return CGSize(width: 0.0, height: max + contentInset.bottom)
}
override func prepare() {
super.prepare()
maxYForColumn = Array(repeating: contentInset.top, count: column)
layoutAttributes.removeAll()
guard let collectionView = collectionView else { return }
guard let delegate = delegate else {
assertionFailure("delegate can'not be nil.")
return
}
let contentWidth = collectionView.bounds.width - contentInset.left - contentInset.right
let width = (contentWidth - CGFloat(column - 1) * minimumInteritemSpacing) / CGFloat(column)
let itemCount = collectionView.numberOfItems(inSection: 0)
for item in 0..<itemCount {
let indexPath = IndexPath(item: item, section: 0)
let height = delegate.collectionView(collectionView, layout: self, heightForItemAt: indexPath, itemWidth: width)
// 计算当前cell应该展示在哪一列上
var targetColumn: Int
if item < column {
targetColumn = item
} else {
let min = maxYForColumn.min()!
targetColumn = maxYForColumn.index(of: min)!
}
// 计算frame
let x = contentInset.left + width * CGFloat(targetColumn) + CGFloat(targetColumn) * minimumInteritemSpacing
let y = minimumLineSpacing + maxYForColumn[targetColumn]
let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attribute.frame = CGRect(x: x, y: y, width: width, height: height)
layoutAttributes.append(attribute)
// 更新当前列的Y值偏移量
maxYForColumn[targetColumn] = y + height
}
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return layoutAttributes[indexPath.item]
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return layoutAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
guard let collectionView = collectionView else { return false }
return !collectionView.bounds.size.equalTo(newBounds.size)
}
}

iOS从通讯录中复制粘贴电话号码携带特殊字符

在做收货地址的功能,其中有个场景就是用户先从手机通讯录中拷贝了某个电话号码,然后将其粘贴到输入框中。一切看似正常,但是这里有个问题,从通讯录中拷贝的电话号码,自带了特殊的不可见unicode字符,如果你不打断点查看变量值,压根不会看到这些特殊字符。

特殊字符

这些特殊字符在OC和Swift中有不同的表现形式:

Objective-C:

1
phone __NSCFString * @"\U0000202d(888) 555-5512\U0000202c" 0x000060000065b7b0

Swift:

1
phone String "\u{e2}(888) 555-5512\u{e2}"

解决方案

Swift:

1
2
3
4
5
6
7
8
9
func removeSpecialCharactersForPhoneNumber(_ phone: String) -> String {
var result = phone
["+86","+","-","(",")"," "].forEach {
result = result.replacingOccurrences(of: $0, with: "")
}
// 过滤`通讯录`拷贝的特殊字符
result = result.replacingOccurrences(of: "\\p{Cf}", with: "", options: .regularExpression)
return result
}

Objective-C

1
[phone stringByReplacingOccurrencesOfString:@"\\p{Cf}" withString:@"" options:NSRegularExpressionSearch range:NSMakeRange(0, phone.length)]

参考: https://stackoverflow.com/questions/47623828/ios-copy-paste-phone-from-contacts-to-uitextfield-adds-strange-unicode-character

Swift截取子字符串

在Swift中截取子串,一直是个头疼的问题,毕竟牵涉到String.Index这个鬼东西,而且API还不好用。现在连substring(to:)substring(from:)substring(width:)这几个方法也被废弃了。

One-sided Slicing

好在Swift4之后,苹果为String增加了一个语法糖...,可以对字符串进行单侧边界取子串。注意返回的类型是Substring而不是String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
extension String {
/// 截取子字符串
///
/// - Parameters:
/// - start: 要抽取的子串的起始下标。如果是负数,那么该参数声明从字符串的尾部开始算起的位置。
/// 也就是说,-1 指字符串中最后一个字符,-2 指倒数第二个字符,以此类推。
/// - length: 截取长度。如果省略了该参数,那么返回从`start`开始到结尾的子串。
/// - Returns: 子字符串
func substr(_ start: Int, length: UInt? = nil) -> String? {
if abs(start) >= count { return nil }
let offset = (start >= 0) ? start : count+start
let s = self.index(startIndex, offsetBy: offset)
guard let length = length else {
// length==nil,直接截取到字符串结尾
return String(self[s...])
}
let e = self.index(startIndex, offsetBy: min(offset+Int(length), count))
return String(self[s..<e])
}
}

如果懂JavaScript的话,一看就懂,这函数和JS的substr是一样的用法。

Swift4之Codable协议

最近公司新项目采用了Swift开发,在处理JSON数据时,一开始是准备采用阿里开源的HandyJSON,后来了解了下,随着Swift4的到来,苹果为我们带来了Codable协议,用于数据的编解码,虽然现在的Codable并不完善,使用上也并没有那么的友好,但是毕竟是官方出品,后期肯定会越来越完善的,所以决定采用Codable而弃用HandyJSON。

基础

首先,Codable是Decodable协议和Encodable协议的组合类型,它们分别定义了init(from decoder: Decoder) throwsfunc encode(to encoder: Encoder) throws方法。如果我们只需单向转换,选择其一即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/// A type that can encode itself to an external representation.
public protocol Encodable {
/// Encodes this value into the given encoder.
///
/// If the value fails to encode anything, `encoder` will encode an empty
/// keyed container in its place.
///
/// This function throws an error if any values are invalid for the given
/// encoder's format.
///
/// - Parameter encoder: The encoder to write data to.
public func encode(to encoder: Encoder) throws
}
/// A type that can decode itself from an external representation.
public protocol Decodable {
/// Creates a new instance by decoding from the given decoder.
///
/// This initializer throws an error if reading from the decoder fails, or
/// if the data read is corrupted or otherwise invalid.
///
/// - Parameter decoder: The decoder to read data from.
public init(from decoder: Decoder) throws
}
/// A type that can convert itself into and out of an external representation.
public typealias Codable = Decodable & Encodable

源码

可以在Swift源码目录/stdlib/public/SDK/Foundation/JSONEncoder.swift看到苹果这该功能的实现。

JSON转Model

最理想的情况下,当然是服务器返回的JSON数据中key和我们Model中定义的key保持一致,那么我们只需要将Model声明为遵守Codable协议即可。

1
2
3
4
struct Person: Codable {
var name: String
var age: Int
}

接下来就可以直接对JSON数据进行解码操作了:

1
2
let data = "{\"name\":\"xiaopin\", \"age\": 18}".data(using: .utf8)!
let model = try? JSONDecoder().decode(Person.self, from: data)

看,是不是so easy!代码简直是6到没朋友啊。

自定义键值

  • CodingKey协议

当然了,大多数情况下我们都会遇到服务器返回的Key与Model属性名称不一致的情况,此时我们就需要自己定义映射关系了。Codable也为我们提供了解决方案,我们只需要定义一个名称为CodingKeys的嵌套枚举,关联值类型为String,并遵守CodingKey协议即可。

假设我们需要给Person增加一个学校信息,服务器返回的Key为school_name,而我们Model中则为schoolName,我们只需简单修改Person的定义即可:

1
2
3
4
5
6
7
8
9
10
struct Person: Codable {
enum CodingKeys: String, CodingKey {
case name, age
case schoolName = "school_name"
}
var name: String
var age: Int
var schoolName: String
}

通过CodingKeys这个枚举,我们便可轻易完成JSON Key和Model之间的映射关系。

默认情况下,编译器会为我们自动生成CodingKeys,并提供init(from decoder: Decoder) throwsfunc encode(to encoder: Encoder) throws的默认实现。

  • keyDecodingStrategy属性

随着Swift4.1的更新,Apple给JSONDecoder/JSONEncoder扩展了一个keyDecodingStrategy属性,为我们带来更加方便的自定义键值映射功能。

keyDecodingStrategy是一个枚举值,有个case是convertFromSnakeCase,可以在对象/结构体的Camel Case格式的属性名和JSON的Snake Case格式的key之间转换(即在Decoder时会自动将JSON中的school_name映射成Person中的schoolName),这个是核心功能内置的,就不需要我们额外写代码处理了。上面加上的枚举CodingKeys也可以去掉了,只需要在JSONDecoder这个实例设置这个属性就行。

1
2
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

枚举

Codable提供对枚举的支持,只需定义好枚举的关联值类型,并遵守Codable协议即可。

现在我们给Person增加一个性别属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Person: Codable {
enum CodingKeys: String, CodingKey {
case name, age, gender
case schoolName = "school_name"
}
enum Gender: String, Codable {
case male, female
}
var name: String
var age: Int
var gender: Gender
var schoolName: String
}

只需简单修改,Codable就会自动为我们将JSON中的gender字符串转换为对应的Gender枚举值。

日期处理

安利一个网站www.nsdateformatter.com,你可以查看各种日期格式的字符串表示。

JSON 没有数据类型表示日期格式,因此需要客户端和服务端对序列化进行约定。

JSONDecoder提供了一个枚举类型来处理日期格式,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// The strategy to use for decoding `Date` values.
public enum DateDecodingStrategy {
/// Defer to `Date` for decoding. This is the default strategy.
case deferredToDate
/// Decode the `Date` as a UNIX timestamp from a JSON number.
case secondsSince1970
/// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
case millisecondsSince1970
/// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
case iso8601
/// Decode the `Date` as a string parsed by the given formatter.
case formatted(DateFormatter)
/// Decode the `Date` as a custom value decoded by the given closure.
case custom((Decoder) throws -> Date)
}

可以看到JSONDecoder内置了几种日期处理方式,前四种没什么可说的,直接赋值拿来用就行了,着重说一下.formatted(DateFormatter).custom((Decoder) throws -> Date)

  • .formatted(DateFormatter)

当服务器返回的是一个非标准格式的日期字符串时,我们可以提供一个DateFormatter来指定日期格式

1
2
3
4
5
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.timeZone = TimeZone(abbreviation: "UTC")
decoder.dateDecodingStrategy = .formatted(formatter)
  • .custom((Decoder) throws -> Date)

当DateFormatter也不能满足我们的需求时,我们可以自己指定如何解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
if let str = try? container.decode(String.self) {
// 如有必要,这里还可以判断字符串是否为时间戳,最终转换成Date
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.timeZone = TimeZone(abbreviation: "UTC")
if let date = formatter.date(from: str) {
return date
}
}
if let double = try? container.decode(Double.self) {
// 可根据服务器返回的时间戳是相对于1970.1.1 00:00:00还是2001.1.1 00:00:00进行相应的转换
return Date(timeIntervalSinceReferenceDate: double)
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date.")
})
...

我们可以进一步优化代码,通过给JSONDecoder.DateDecodingStrategy扩展一个customDateConvert()方法,之后在多个地方进行日期的转换也将变得很简单。

1
decoder.dateDecodingStrategy = .customDateConvert()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
extension JSONDecoder.DateDecodingStrategy {
/// 自定义解析日期
/// 使用方式: JSONDecoder().dateDecodingStrategy = .customDateConvert()
///
/// - Returns: .custom((Decoder) -> Date)
static func customDateConvert() -> JSONDecoder.DateDecodingStrategy {
return .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
if let str = try? container.decode(String.self) {
let formatter = DateFormatter()
formatter.timeZone = TimeZone(abbreviation: "UTC")
for dateFormat in ["yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd"] {
formatter.dateFormat = dateFormat
guard let date = formatter.date(from: str) else { continue }
return date
}
}
if let double = try? container.decode(Double.self) {
// 根据服务器返回的时间戳是相对于1970.1.1 00:00:00还是2001.1.1 00:00:00进行相应的转换
return Date(timeIntervalSince1970: double)
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date.")
})
}
}

自定义解析

理想情况下我们当然不需要自己实现init(from decoder: Decoder) throws方法,但是总有一些特殊情况,或者说是给代码增加容错机制,我们不得不实现该方法,自己实现解码操作。

编译器的默认实现中,都是使用public func decode<T>(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T where T : Decodable方法进行解码的。

默认实现有一定的局限性

  • 我们在上面中定义的gender属性,如果JSON数据中并不存在gender这个Key时则会抛出下面的异常信息:

    1
    keyNotFound(ETNavBarTransparentDemo.Person.(CodingKeys in _34A23078E52B5E180CB78F393D171183).gender, Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key gender (\"gender\").", underlyingError: nil))
  • 如果Model中属性的类型是Int型,但是JSON中却是String类型的数字,那也会抛出异常:

    1
    typeMismatch(Swift.Int, Swift.DecodingError.Context(codingPath: [ETNavBarTransparentDemo.Person.(CodingKeys in _34A23078E52B5E180CB78F393D171183).age], debugDescription: "Expected to decode Int but found a string/data instead.", underlyingError: nil))
  • Bool类型,只能处理true/false,不能处理字符串的”true”/“false”以及数字0/1

  • Int和Double的区别,当JSON中的value为88.0时,Int和Double均可以解码,但是如果value为88.01时,Int解码失败,抛出typeMismatch异常信息

对于以上情况,如果你们App觉得无所谓,那就没什么了;但是如果你想自己处理这些情况,保证能够正常的解码JSON数据,那你就只能自行实现init(from decoder: Decoder) throws了。

自定义解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct DataModel: Codable {
enum CodingKeys: String, CodingKey {
case id, flag
}
var id: Int
var flag: Bool
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = (try container.decodeIntIfPresent(.id)) ?? 0
flag = (try container.decodeBoolIfPresent(.flag)) ?? true
}
}

针对上面的情况,我给KeyedDecodingContainer扩展了几个方法,已可满足目前项目的需求,后期可视情况再新增:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
// MARK: - 强制解码,如果key不存在则抛出异常`DecodingError.keyNotFound`
extension KeyedDecodingContainer {
/// 解码CGFloat
func decodeCGFloat(_ key: K) throws -> CGFloat {
do {
let d = try decodeDouble(key)
return CGFloat(d)
} catch {
throw error
}
}
/// 解码Double
func decodeDouble(_ key: K) throws -> Double {
do {
return try decode(Double.self, forKey: key)
} catch {
if let i = try? decode(Int.self, forKey: key) {
return Double(i)
}
if let s = try? decode(String.self, forKey: key), let d = Double(s) {
return d
}
if let b = try? decode(Bool.self, forKey: key) {
return b ? 1.0 : 0.0
}
throw error
}
}
/// 解码Int
func decodeInt(_ key: K) throws -> Int {
do {
return try decode(Int.self, forKey: key)
} catch {
if let d = try? decode(Double.self, forKey: key) {
return Int(d)
}
if let s = try? decode(String.self, forKey: key), let i = Int(s) {
return i
}
if let b = try? decode(Bool.self, forKey: key) {
return b ? 1 : 0
}
throw error
}
}
/// 解码String
func decodeString(_ key: K) throws -> String {
do {
return try decode(String.self, forKey: key)
} catch {
if let i = try? decode(Int.self, forKey: key) {
return String(i)
}
if let d = try? decode(Double.self, forKey: key) {
return String(d)
}
if let b = try? decode(Bool.self, forKey: key) {
return b ? "true" : "false"
}
throw error
}
}
/// 解码Bool
func decodeBool(_ key: K) throws -> Bool {
do {
return try decode(Bool.self, forKey: key)
} catch {
if let s = try? decode(String.self, forKey: key) {
if s.isEmpty || s == "0" || s.lowercased() == "false" {
return false
}
return true
}
if let i = try? decode(Int.self, forKey: key) {
return (i == 0) ? false : true
}
if let d = try? decode(Double.self, forKey: key) {
return (d == 0.0) ? false : true
}
throw error
}
}
/// 解码Date
func decodeDate(_ key: K) throws -> Date {
guard let date = try decodeDateIfPresent(key) else {
let context = DecodingError.Context(codingPath: [key], debugDescription: "No value associated with key `\(key)`")
throw DecodingError.keyNotFound(key, context)
}
return date
}
}
// MARK: - 忽略`DecodingError.keyNotFound`异常信息,当key不存在时返回`nil`
extension KeyedDecodingContainer {
/// 解码CGFloat数据
func decodeCGFloatIfPresent(_ key: K) throws -> CGFloat? {
if let d = try decodeDoubleIfPresent(key) {
return CGFloat(d)
}
return nil
}
/// 解码Double数据
func decodeDoubleIfPresent(_ key: K) throws -> Double? {
do {
return try decodeIfPresent(Double.self, forKey: key)
} catch {
// Int -> Double
do {
if let i = try decodeIfPresent(Int.self, forKey: key) {
return Double(i)
}
} catch {}
// String -> Double
do {
if let s = try decodeIfPresent(String.self, forKey: key) {
return Double(s)
}
} catch {}
// Bool -> Double
do {
if let b = try decodeIfPresent(Bool.self, forKey: key) {
return b ? 1.0 : 0.0
}
} catch {}
// failure
throw error
}
}
/// 解码Int数据
func decodeIntIfPresent(_ key: K) throws -> Int? {
do {
return try decodeIfPresent(Int.self, forKey: key)
} catch {
// Double -> Int
do {
if let d = try decodeIfPresent(Double.self, forKey: key) {
return Int(d)
}
} catch {}
// String -> Int
do {
if let s = try decodeIfPresent(String.self, forKey: key) {
return Int(s)
}
} catch {}
// Bool -> Int
do {
if let b = try decodeIfPresent(Bool.self, forKey: key) {
return b ? 1 : 0
}
} catch {}
// decode failure.
throw error
}
}
/// 解码String数据
func decodeStringIfPresent(_ key: K) throws -> String? {
do {
return try decodeIfPresent(String.self, forKey: key)
} catch {
// Int -> String
do {
if let i = try decodeIfPresent(Int.self, forKey: key) {
return String(i)
}
} catch {}
// Double -> String
do {
if let d = try decodeIfPresent(Double.self, forKey: key) {
return String(d)
}
} catch {}
// Bool -> String
do {
if let b = try decodeIfPresent(Bool.self, forKey: key) {
return b ? "true" : "false"
}
} catch {}
// decode failure.
throw error
}
}
/// 解码Bool数据
func decodeBoolIfPresent(_ key: K) throws -> Bool? {
do {
return try decodeIfPresent(Bool.self, forKey: key)
} catch {
// String -> Bool
do {
if let s = try decodeIfPresent(String.self, forKey: key) {
if s.isEmpty || s == "0" || s.lowercased() == "false" {
return false
}
return true
}
} catch {}
// Int -> Bool
do {
if let i = try decodeIfPresent(Int.self, forKey: key) {
return (i == 0) ? false : true
}
} catch {}
// Double -> Bool
do {
if let d = try decodeIfPresent(Double.self, forKey: key) {
return (d == 0.0) ? false : true
}
} catch {}
// decode failure.
throw error
}
}
/// 解码CGSize数据
func decodeCGSizeIfPresent(_ key: K) throws -> CGSize? {
do {
return try decodeIfPresent(CGSize.self, forKey: key)
} catch {
do {
if let s = try decodeIfPresent(String.self, forKey: key) {
// 例: 宽x高 宽,高 宽X高
for seprator in ["x", ",", "X"] {
let array = s.components(separatedBy: seprator)
if array.count == 2 {
if let w = Double(array.first!), let h = Double(array.last!) {
return CGSize(width: w, height: h)
}
}
}
}
} catch {}
throw error
}
}
/// 解码Date
func decodeDateIfPresent(_ key: K) throws -> Date? {
do {
return try decodeIfPresent(Date.self, forKey: key)
} catch {
if let s = try? decode(String.self, forKey: key) {
let formatter = DateFormatter()
formatter.timeZone = TimeZone(abbreviation: "UTC")
for dateFormat in ["yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd"] {
formatter.dateFormat = dateFormat
guard let date = formatter.date(from: s) else { continue }
return date
}
}
throw error
}
}
}

有了这些扩展方法,我们就事半功倍了。

有趣的发现

前提条件:

  • 定义了JSONDecoder.DateDecodingStrategycustomDateConvert()扩展方法
  • 定义了KeyedDecodingContainer的扩展方法decodeDate(_:)decodeDateIfPresent(_:)

伪代码(详细代码请看上面):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extension JSONDecoder.DateDecodingStrategy {
static func customDateConvert() -> JSONDecoder.DateDecodingStrategy {
...
}
}
extension KeyedDecodingContainer {
func decodeDate(_ key: K) throws -> Date {...}
func decodeDateIfPresent(_ key: K) throws -> Date? {
do {
return try decodeIfPresent(Date.self, forKey: key)
} catch {...}
}
}

下面是测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct DataModel: Codable {
var time: Date
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
time = try container.decodeDate(.time)
}
}
do {
let data = "{\"time\":\"2018-04-11 17:34:23\"}".data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .customDateConvert()
let model = try decoder.decode(DataModel.self, from: data)
} catch {
print(error)
}

从测试代码中我们可以看到,不仅仅实现了init(from:)方法,并且还指定了dateDecodingStrategy属性。

在初始化方法中我们调用了上面的扩展方法decodeDate(_:)来获取日期数据,而其内部是通过调用decodeDateIfPresent(_:)来获取日期,这看似正常,并没什么问题。关键在于decodeDateIfPresent(_:)中首先尝试通过系统方法去获取Date,即try decodeIfPresent(Date.self, forKey: key),该行代码会导致JSONDecoder使用我们的customDateConvert()来解码日期。

也就是形成了这么一条调用链: decodeDate(_:) -> decodeDateIfPresent(_:) -> customDateConvert()

更多信息请参考: