Oniityann


  • 首页

  • 分类

  • 归档

  • 标签

WWDC2016-Optimizing App Startup Time

发表于 2017-09-23 | 分类于 Performance Optimization

Mach-O

Mach-O 文件是运行时可执行文件类型,包括:

  • 可执行文件。Executable — Main binary for application.
  • Dylib — Dynamic library.(aka DSO or DLL)
  • Bundle — Dylib that can not be linked. 只能在运行时用 dlopen() 函数打开。
  • Images — Any executable dylib or bundle.
  • Framework — Dylib with directory for resources and headers.
  • 图像格式:分成3段,每一段都是页面大小的倍数。

页面大小由硬件决定,arm64处理器页面大小是16K,其他的是4K。

  • 通用文件:Universal Files

假设你有一个Mach-O文件运行在64位(arm64)处理器的设备上,如果你想把它运行在32位(armv7s)的设备上,Xcode里会发生什么变化?

答案是:会生成另一个单独的Mach-O文件。然后这两个文件合并生成第三个文件,这个文件就是通用文件。通用文件会有一个头文件,占一页大小。

虚拟内存:Virtual Memory

虚拟内存是间接层。当所有的进程存在时,OS 会使用间接层来管理物理内存。每一个进程都是一个逻辑地址空间,它们被映射到 RAM 的某个物理页面。

  1. Page fault:如果一个虚拟内存不映射到任何物理内存,那么访问这个进程 时,就会产生页面错误,内核会停止该进程,并试图找出解决方案。
  2. 多个进程可以共享同一块物理内存,进程共享。
  3. File backed pages:基于文件的映射:不用把整个文件读入 RAM,而是调用 mmap() 函数告诉虚拟内存系统,我想把这个文件映射到进程里的这段地址。
  4. Copy-On-Write (COW):写入时复制。写入时复制所做的就是它积极地在所有进程里共享DATA页面,只要进程只读有共享内容的全局变量,但是一旦有进程想要写入其DATA页面,写入时复制开始。内核会把该页面复制,放入另一个物理RAM并重定向映射,所以该进程有了该页面的副本。
  5. Dirty vs. clean Pages:脏页面和干净页面。上面的副本被认为是脏页面。脏页面是指含有进程特定信息。干净页面是指内核可以按照需要重新建立的页面,比如重新读取磁盘的时候。脏页面比干净页面昂贵得多。

exec() —> main() 的过程

  • exec() 是一个系统调用。当你进入一个内核,说:我想把这个进程换成这个新程序。然后内核会抹去整个地址来映射这个新的可执行程序。ASLR 会给它分配一个随机地址。下一步是从该随机地址回溯到 0 地址,把整个地址标记为不可访问:

wwdc2016-406_p_1

  • Kernal loads helper program
  • Dyld (Dynamic loader)
  • Executions starts in Dyld (aka LD.SO)

当内核完成内存映射之后,就把指针指向Dyld,让Dyld来完成进程的启动。它的工作是加载所有依赖的Dylib,让它们准备好开始运行。其加载过程如下:

wwdc2016-406_p_2

  • Load dylibs

    • 读取所有依赖的 Dylib。首先从内核中读取已经加载好的主可执行文件。在这个主可执行文件的 Header 中有所有依赖库的列表。然后打开和运行这些Dylib,验证它是不是一个 Mach-O 文件,找到它的编码签名,在内核里对它进行注册,在该 Dylib 的每一段调用 mmap() 函数。
    • 在加载每个 Dylib时,每个 Dylib 可能还依赖于另一个 Dylib,所以需要递归式的把它们一个一个找出来加载到内存。
  • DATA修复

    • 重设基址(Rebase):遍历所有内部数据指针,然后为它们添加一个滑动值。这些指针在段里的位置都编码在__LINKEDIT段里。I/O比较多。
    • 绑定(Bind):针对那些指向Dylib范围外的指针而言的。其计算复杂度比Rebase要高得多。
    • ObjC:经过前两步之后,在ObjC运行时还需要一些额外的操作。在ObjC运行时,必须要维护一张表格,包含所有名称及其映射的类。每次加载的名称都将定义一个类,名称需要登记在一个全局表格里
  • Initializer

  • 跳转到main()函数

启动时间优化

经过上面的总结:
理论上 App 的启动时间是由 main() 函数之前的加载时间(t1)和 main() 函数之后的加载时间(t2)。

t1 时间优化

加入 DYLD_PRINT_STATISTICS 分析 App 启动日志

屏幕快照 2018-10-23 下午12.47.17

在环境变量中添加 DYLD_PRINT_STATISTICS 就可以在控制台看见自己 App 的启动时间输出。

优化方向

  1. 根据上文的总结,首先就是需要减少动态库的加载,构建OS时,会预计算大量的dylib数据,但是始终无法全部预算。

  2. 减少 rebase/binding time。时间都花费在修复__DATA里的指针。所以方法就是减少需要修复的指针。

  3. 减少ObjC类对象和ivars的数量。

  4. 使用Swift语言 :Swift 通常用的数据要少一些。而且 Swift 更为内联,可以更好地使用 code-gen 减少消耗。

  5. 使用 initialize 方法替换 load 方法。

    • 显示的初始化器:ObjC +load() 方法。如果你是用的话最好换成 +initialize() 方法,但是这种显示的初始化器最好换成点初始化器:dispatch_once() 或者 pthread_once() 或者 std::once()。
    • 隐式初始化器:大部分来自 C++ 的全局变量,带有非默认的初始化器,非默认的构造函数。这里可以用前面提到的点初始化器替代,或者把全局的换成非全局的结构或者指针,指向想要初始化的对象。或者 Only set simple values (PODs)。或者用 Swift 重新编写,Swift 的全局变量可以在使用前确保被初始化,其本质还是在后台调用了点初始化器。

t2 时间优化

在 main() 被调用过后,App 主要所做的是一些初始化工作和首页显示。

首先的显示主要包含四个方面:

  • 图片的解码
  • 控件的布局
  • 控件的绘制
  • 网络请求

初始化主要是:

  • 加载 UserDefault 产生的 plist 文件
  • didFinishLaunching 中的配置代码

优化方向

  1. 使用纯代码代替 nib 加载首页视图
  2. 在控件布局周期中打点观察,特别是 viewDidLoad 和 viewWillAppear 这两个操作
  3. 减少 Log
  4. 梳理启动的网络请求,评估是否可以在
  5. 延迟加载 didFinishLaunching 中的某些配置代码

WebView 长按识别二维码和点击查看大图

发表于 2017-06-21 | 分类于 小技巧

本地识别二维码

CIDetecor

  1. 首先要识别本地二维码, webView 基本知识几条 js 语句的问题.
    创建 QRCodeDetector 类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import "YNQRCodeDetector.h"

@implementation YNQRCodeDetector

+ (CIQRCodeFeature *)yn_detectQRCodeWithImage:(UIImage *)image {
// 1. 创建上下文
CIContext *context = [[CIContext alloc] init];

// 2. 创建探测器
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:context options:@{CIDetectorAccuracy: CIDetectorAccuracyLow}];

// 3. 识别图片获取图片特征
CIImage *imageCI = [[CIImage alloc] initWithImage:image];
NSArray<CIFeature *> *features = [detector featuresInImage:imageCI];
CIQRCodeFeature *codeF = (CIQRCodeFeature *)features.firstObject;

return codeF;
}

@end
  1. 在本地 imageView 中添加手势, 处理二维码识别的链接, 有二维码显示保存和识别, 没有只显示保存
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
- (void)imageLongPress {

UIAlertController *ac = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];

CIQRCodeFeature *codeF = [YNQRCodeDetector yn_detectQRCodeWithImage:self.imageView.image];

[ac addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {

}]];

[ac addAction:[UIAlertAction actionWithTitle:@"保存图片" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[self savePicture];
}]];

if (codeF.messageString) {

[ac addAction:[UIAlertAction actionWithTitle:@"识别二维码" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
// 内置浏览器打开网页
SFSafariViewController *safariVC = [[SFSafariViewController alloc] initWithURL:[NSURL URLWithString:codeF.messageString]];
[self presentViewController:safariVC animated:YES completion:nil];
}]];
}

[self presentViewController:ac animated:YES completion:nil];
}
  1. 保存图片没什么好说的, 记得 iOS 10 以后手动开启相册权限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)savePicture {
if (self.imageView.image == nil) {
// [SVProgressHUD showErrorWithStatus:@"图片还未加载"];
} else {
UIImageWriteToSavedPhotosAlbum(self.imageView.image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
}
}

- (void)image:(UIImage *)image
didFinishSavingWithError:(NSError *)error
contextInfo:(void *)contextInfo {

if (error) {

// [SVProgressHUD showErrorWithStatus:@"保存失败"];

} else {

// [SVProgressHUD showSuccessWithStatus:@"保存成功"];
}

}

WebView上的一些处理

WebView添加长按手势

因为项目需求, 需要改的地方太多了, 我直接写在自定义 WebView 里面了, 直接替换 webView 即可. 因为公司需求的 web 没有过于复杂的功能, 直接写在自定义 webView 就行.

自定义 webView 添加长按手势:

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
- (instancetype)init {
self = [super init];
if (self) {
[self basicConfigure];
}
return self;
}

- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self basicConfigure];
}
return self;
}

- (void)basicConfigure {
self.delegate = self;

UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressWebPic:)];
longPress.delegate = self;
[self addGestureRecognizer:longPress];
}

- (void)longPressWebPic:(UILongPressGestureRecognizer *)recognizer {

if (recognizer.state != UIGestureRecognizerStateBegan) {
return;
}

// 获取点击区域坐标, 通过坐标得到图片
CGPoint touchPoint = [recognizer locationInView:self];
NSString *imgURL = [NSString stringWithFormat:@"document.elementFromPoint(%f, %f).src", touchPoint.x, touchPoint.y];
NSString *urlToSave = [self stringByEvaluatingJavaScriptFromString:imgURL];

if (urlToSave.length == 0) {
return;
}

// ENLog(@"%@", urlToSave);

[self imageWithUrl:urlToSave];

}

下载图片

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
// 因为做了个 demo 不想引用三方, 可以用 sdImageDownloader 替代
- (void)imageWithUrl:(NSString *)imageUrl {

// 在子线程中下载图片, 不在子线程中下载图片会造成主线程阻塞, 导致 alertController 需要很久时间才弹出
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSURL *url = [NSURL URLWithString:imageUrl];

NSURLSessionConfiguration * configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue new]];

NSURLRequest *imgRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:30.0];

NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:imgRequest completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
return ;
}

NSData *imageData = [NSData dataWithContentsOfURL:location];

// 在主线程中配置 UI 显示相关操作
dispatch_async(dispatch_get_main_queue(), ^{

UIImage *image = [UIImage imageWithData:imageData];

UIAlertController *ac = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];

CIQRCodeFeature *codeF = [YNQRCodeDetector yn_detectQRCodeWithImage:image];

[ac addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {

}]];

[ac addAction:[UIAlertAction actionWithTitle:@"保存图片" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[self savePicture:image];
}]];

if (codeF.messageString) {

[ac addAction:[UIAlertAction actionWithTitle:@"识别二维码" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {

SFSafariViewController *safariVC = [[SFSafariViewController alloc] initWithURL:[NSURL URLWithString:codeF.messageString]];
[[self currentViewController] presentViewController:safariVC animated:YES completion:nil];
}]];

}
// 直接用 webView 模态出控制器
[[self currentViewController] presentViewController:ac animated:YES completion:nil];
});
}];

[task resume];
});
}

获取最上层控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (UIViewController *)currentViewController {
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
UIViewController *vc = keyWindow.rootViewController;
while (vc.presentedViewController) {
vc = vc.presentedViewController;

if ([vc isKindOfClass:[UINavigationController class]]) {
vc = [(UINavigationController *)vc visibleViewController];
} else if ([vc isKindOfClass:[UITabBarController class]]) {
vc = [(UITabBarController *)vc selectedViewController];
}
}
return vc;
}

- (UINavigationController *)currentNavigationController {
return [self currentViewController].navigationController;
}

处理 WebView 代理方法

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
- (void)webViewDidFinishLoad:(UIWebView *)webView {

// 屏蔽网页自带操作
[self stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.webkitUserSelect='none';"];

NSString * jsCallBack = @"window.getSelection().removeAllRanges();";
[webView stringByEvaluatingJavaScriptFromString:jsCallBack];

//js方法遍历图片添加点击事件 返回图片个数
static NSString * const jsGetImages =
@"function getImages(){\
var objs = document.getElementsByTagName(\"img\");\
for(var i=0;i<objs.length;i++){\
objs[i].onclick=function(){\
document.location=\"myweb:imageClick:\"+this.src;\
};\
};\
return objs.length;\
};";

// 获取图片链接
[webView stringByEvaluatingJavaScriptFromString:jsGetImages];//注入js方法
[webView stringByEvaluatingJavaScriptFromString:@"getImages()"];
}

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
// 将url转换为string
NSString *requestString = [[request URL] absoluteString];

// 判断创建的字符串内容是否以pic:字符开始
if ([requestString hasPrefix:@"myweb:imageClick:"]) {
NSString *imageUrl = [requestString substringFromIndex:@"myweb:imageClick:".length];
ENLog(@"------%@", imageUrl);
// 点击网页图片查看大图
ShowBigPicController *sb = [[ShowBigPicController alloc] init];
sb.imageURL = imageUrl;
[[self currentViewController] presentViewController:sb animated:NO completion:nil];

return NO;
}
return YES;
}

Demo 地址

YNQRCodeWebView

ReactiveCocoa Quick Search

发表于 2017-04-21 | 分类于 ReactiveCocoa

ReactiveCocoa 常用 API

常见类

RACSiganl 信号类。

  1. RACEmptySignal:空信号,用来实现 RACSignal 的 +empty 方法;
  2. RACReturnSignal:一元信号,用来实现 RACSignal 的 +return: 方法;
  3. RACDynamicSignal:动态信号,使用一个 block - 来实现订阅行为,我们在使用 RACSignal 的 +createSignal: 方法时创建的就是该类的实例;
  4. RACErrorSignal:错误信号,用来实现 RACSignal 的 +error: 方法;
  5. RACChannelTerminal:通道终端,代表 RACChannel 的一个终端,用来实现双向绑定。

RACSubscriber 订阅者


RACDisposable 用于取消订阅或者清理资源,当信号发送完成或者发送错误的时候,就会自动触发它。

  1. RACSerialDisposable:作为 disposable 的容器使用,可以包含一个 disposable 对象,并且允许将这个 disposable 对象通过原子操作交换出来;
  2. RACKVOTrampoline:代表一次 KVO 观察,并且可以用来停止观察;
  3. RACCompoundDisposable:它可以包含多个 disposable 对象,并且支持手动添加和移除 disposable 对象。
  4. RACScopedDisposable:当它被 dealloc 的时候调用本身的 -dispose 方法。

RACSubject 信号提供者,自己可以充当信号,又能发送信号。

  1. RACGroupedSignal:分组信号,用来实现 RACSignal 的分组功能。
  2. RACBehaviorSubject:重演最后值的信号,当被订阅时,会向订阅者发送它最后接收到的值。
  3. RACReplaySubject:重演信号,保存发送过的值,当被订阅时,会向订阅者重新发送这些值。

RACTuple 元组类

类似 NSArray,用来包装值。


RACSequence

RAC 中的集合类


RACCommand

RAC中用于处理事件的类, 可以把事件如何处理,事件中的数据如何传递,包装到这个类中,他可以很方便的监控事件的执行过程。


RACMulticastConnection

用于当一个信号,被多次订阅时,为了保证创建信号时,避免多次调用创建信号中的block,造成副作用,可以使用这个类处理。


RACScheduler RAC中的队列,用GCD封装的。

  1. RACImmediateScheduler:立即执行调度的任务,这是唯一一个支持同步执行的调度器;
  2. RACQueueScheduler:一个抽象的队列调度器,在一个 GCD 串行列队中异步调度所有任务;
  3. RACTargetQueueScheduler:继承自 RACQueueScheduler ,在一个以一个任意的 GCD 队列为 target 的串行队列中异步调度所有任务;
  4. RACSubscriptionScheduler:一个只用来调度订阅的调度器。

常见用法

  1. rac_signalForSelector:fromProtocol:代替代理。
  2. rac_valuesAndChangesForKeyPath:KVO。
  3. rac_signalForControlEvents:监听事件。
  4. rac_addObserverForName:代替通知。
  5. rac_textSignal:监听文本框文字改变。
  6. rac_liftSelector:withSignalsFromArray:Signals:当传入的 Signals(信号数组),每一个 signal 都至少 sendNext 过一次,就会去触发第一个selector参数的方法。

常见宏

  1. RAC(TARGET, [KEYPATH, [NIL_VALUE]]):用于给某个对象的某个属性绑定
  2. RACObserve(self, name):监听某个对象的某个属性,返回的是信号。
  3. @weakify(Obj) 和 @strongify(Obj)
  4. RACTuplePack:把数据包装成RACTuple(元组类)
  5. RACTupleUnpack:把RACTuple(元组类)解包成对应的数据
  6. RACChannelTo 用于双向绑定的一个终端

常用操作方法

  1. flattenMap map 用于把源信号内容映射成新的内容。
  2. concat 组合 按一定顺序拼接信号,当多个信号发出的时候,有顺序的接收信号。
  3. then 用于连接两个信号,当第一个信号完成,才会连接then返回的信号。
  4. merge 把多个信号合并为一个信号,任何一个信号有新值的时候就会调用。
  5. zipWith 把两个信号压缩成一个信号,只有当两个信号同时发出信号内容时,并且把两个信号的内容合并成一个元组,才会触发压缩流的next事件。
  6. combineLatest:将多个信号合并起来,并且拿到各个信号的最新的值,必须每个合并的signal至少都有过一次sendNext,才会触发合并的信号。
  7. reduce:聚合,用于信号发出的内容是元组,把信号发出元组的值聚合成一个值。
  8. filter:过滤信号,使用它可以获取满足条件的信号。
  9. ignore:忽略完某些值的信号。
  10. distinctUntilChanged:当上一次的值和当前的值有明显的变化就会发出信号,否则会被忽略掉。
  11. take:从开始一共取N次的信号。
  12. takeLast:取最后N次的信号,前提条件,订阅者必须调用完成,因为只有完成,就知道总共有多少信号。
  13. takeUntil:获取信号直到某个信号执行完成。
  14. skip:跳过几个信号,不接受。
  15. switchToLatest:用于 signalOfSignals(信号的信号),有时候信号也会发出信号,会在 signalOfSignals 中,获取 signalOfSignals 发送的最新信号。
  16. doNext: 执行 Next 之前,会先执行这个 Block。
  17. doCompleted:执行 sendCompleted 之前,会先执行这个 Block。
  18. timeout:超时,可以让一个信号在一定的时间后,自动报错。
  19. interval:定时,每隔一段时间发出信号。
  20. delay:延迟发送 next。
  21. retry:重试,只要失败,就会重新执行创建信号中的block,直到成功。
  22. replay:重放,当一个信号被多次订阅,反复播放内容。
  23. throttle:节流,当某个信号发送比较频繁时,可以使用节流,在某一段时间不发送信号内容,过了一段时间获取信号的最新内容发出。

UI

rac_prepareForReuseSignal - 需要复用时用、

相关UI:MKAnnotationView、UICollectionReusableView、UITableViewCell、UITableViewHeaderFooterView


rac_buttonClickedSignal - 点击事件触发信号

相关UI:UIActionSheet、UIAlertView


rac_command - button类、刷新类相关命令替换

相关UI:UIBarButtonItem、UIButton、UIRefreshControl


rac_signalForControlEvents - control event 触发

相关UI:UIControl


rac_gestureSignal - UIGestureRecognizer 事件处理信号

相关UI:UIGestureRecognizer


rac_imageSelectedSignal - 选择图片的信号

相关UI:UIImagePickerController


rac_textSignal

相关UI:UITextField、UITextView


可实现双向绑定的相关API

rac_channelForControlEvents: key: nilValue:

相关UI:UIControl类

rac_newDateChannelWithNilValue:

相关UI:UIDatePicker

rac_newSelectedSegmentIndexChannelWithNilValue:

相关UI:UISegmentedControl

rac_newValueChannelWithNilValue:

相关UI:UISlider、UIStepper

rac_newOnChannel

相关UI:UISwitch

rac_newTextChannel

相关UI:UITextField


Foundation - Category (常用汇总)

NSArray:

rac_sequence:信号集合


NSData:

rac_readContentsOfURL: options: scheduler:比 OC 多出线程设置


NSDictionary:

  1. rac_sequence
  2. rac_keySequence:key 集合
  3. rac_valueSequence:value 集合


    NSEnumerator:

rac_sequence


NSFileHandle:

rac_readInBackground


NSIndexSet:

rac_sequence


NSInvocation:

  1. rac_setArgument:atIndex:设置参数
  2. rac_argumentAtIndex:取某个参数
  3. rac_returnValue:所关联方法的返回值

NSNotificationCenter:

rac_addObserverForName:object:注册通知


NSObject:

  1. rac_willDeallocSignal:对象销毁时发动的信号
  2. rac_description:debug用
  3. rac_observeKeyPath:options:observer:block:监听某个事件
  4. rac_liftSelector:withSignals:全部信号都 next 在执行
  5. rac_signalForSelector:代替某个方法
  6. rac_signalForSelector:fromProtocol:代替代理

NSOrderedSet:

rac_sequence


NSSet:

rac_sequence


NSString:

  1. rac_keyPathComponents:获取一个路径所有的部分。
  2. rac_keyPathByDeletingLastKeyPathComponent:删除路径最后一部分。
  3. rac_keyPathByDeletingFirstKeyPathComponent:删除路径第一部分。
  4. rac_sequence:character。
  5. rac_readContentsOfURL:usedEncoding:scheduler:多线程调用。

NSURLConnection:

rac_sendAsynchronousRequest:发起异步请求。


NSUserDefaults:

rac_channelTerminalForKey:用于双向绑定。

浅谈 ReactiveCocoa 在各种情况下的使用

发表于 2017-04-14 | 分类于 ReactiveCocoa

情景一 - RACCommand + Signal 控制 button 的点击

情景解释:现在我们有一个场景,在 TextField 中需要填入符合某些要求的字符串,一个按钮才可以点击。

  • 首先创建一个 Textfield 和一个 UIButton:
1
2
3
4
UITextField *textField = [[UITextField alloc] init];
textField.borderStyle = UITextBorderStyleBezel;

UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
  • 创建一个符合 button 的 enable 要求的 signal,例如我们要求 TextField 输入的个数大于 6,button 才能点击:
1
2
3
4
RACSignal *enableSignal = [textField.rac_textSignal map:^id _Nullable(NSString * _Nullable value) {
// 将 Bool 映射成了一个 NSNumber 对象
return @(value.length > 6);
}];

配置 button 的 rac_command:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// button 是否 enable 是根据 enable signal 来判断的
button.rac_command = [[RACCommand alloc] initWithEnabled:enableSignal signalBlock:^RACSignal * _Nonnull(id _Nullable input) {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
// 模拟耗时操作
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[subscriber sendNext:[[NSDate date] description]];
[subscriber sendCompleted];
});

return [RACDisposable disposableWithBlock:^{
NSLog(@"信号被销毁----->%@", [self class]);
}];
}];
}];

此时,只要 button 点击,就会执行 command 中的操作。

获取 Command 中的操作:

  • button.rac_command.executionSignals 是一个执行的信号组,订阅之后参数在是一个信号,参数的参数才是我们需要用的值:
1
2
3
4
5
6
7
#if 0
[button.rac_command.executionSignals subscribeNext:^(RACSignal<id> * _Nullable x) {
[x subscribeNext:^(id _Nullable x) {
NSLog(@"%@", x);
}];
}];
#endif
  • 我们也可以通过 button.rac_command.executionSignals.switchToLatest 更简单的方法去获取 Subcriber 传出的值:
1
2
3
4
5
6
7
8
9
[[button.rac_command.executionSignals.switchToLatest deliverOnMainThread] subscribeNext:^(id  _Nullable x) {
// 此处的 x 就是上面 subscriber 发出的时间字符串
// 让它显示在一个 alertView 上
UIAlertController *ac = [UIAlertController alertControllerWithTitle:@"Time" message:[NSString stringWithFormat:@"%@", x] preferredStyle:UIAlertControllerStyleAlert];
[ac addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"123");
}]];
[self presentViewController:ac animated:YES completion:nil];
}];

情景二 - 双向绑定实现 ColorPicker

情景解释:三个 Slider 分别管理色值的 RGB,拖动 slider 以配置颜色或者在 RGB 值的 textField 中输入数字来配置颜色。

RACChannelTerminal

通道终端,用于实现 Objects 的双向绑定。

创建双向绑定 Signal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (RACSignal *)bindSlider:(UISlider *)slider withTextField:(UITextField *)textField {

// 使用通道终端进行双向绑定
RACChannelTerminal *sliderChannelTerminal = [slider rac_newValueChannelWithNilValue:nil];
RACChannelTerminal *textChannelTerminal = [textField rac_newTextChannel];

// slider 返回的浮点数非常长, 需先进行格式化
// slider 的通道终端订阅 textField 通道终端
[[sliderChannelTerminal map:^id _Nullable(id _Nullable value) {
return [NSString stringWithFormat:@"%.3f", [value floatValue]];
}] subscribe:textChannelTerminal];

// textField 通道终端订阅 slider 通道终端
[textChannelTerminal subscribe:sliderChannelTerminal];

RACSignal *textSignal = textField.rac_textSignal;

// merge 不管先后顺序, 因为 combinelatest 需要所有信号都有新值, 所以在赋值的时候通过 merge textFild 的输入内容, 让其触发一次新值, 这样合并之后, 初始都触发了一次
return [[sliderChannelTerminal merge:textChannelTerminal] merge:textSignal];
}

初始化信息并调用

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
- (void)viewDidLoad {
[super viewDidLoad];

self.redField.text = @"0.500";
self.greenField.text = @"0.500";
self.blueField.text = @"0.500";
RACSignal *rSignal = [self bindSlider:self.redSlider withTextField:self.redField];
RACSignal *gSignal = [self bindSlider:self.greenSlider withTextField:self.greenField];
RACSignal *bSignal = [self bindSlider:self.blueSlider withTextField:self.blueField];

// combineLatest 需要所有信号都有新值
RACSignal *colorSignal = [[RACSignal combineLatest:@[rSignal, gSignal, bSignal]] map:^id _Nullable(RACTuple * _Nullable value) {
return [UIColor colorWithRed:[value[0] floatValue] green:[value[1] floatValue] blue:[value[2] floatValue] alpha:1];
}];

#if 0
// 传统的订阅
@weakify(self);
[colorSignal subscribeNext:^(UIColor * _Nullable color) {
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self);
self.colorView.backgroundColor = color;
});
}];
#endif

// RAC 宏用来绑定
RAC(self.colorView, backgroundColor) = colorSignal;
}

iOS App 状态恢复

发表于 2017-01-05 | 分类于 小技巧

前言

最近项目需求加上状态恢复, 记得之前在书上看过, 这次单独抽出这个功能实现详细梳理一下, 方便自己温习一下, 也方便不知道的 developer 学习.


状态恢复?

举个栗子:

在使用名字为 A 的 app 时, 从列表页面进入详情页面, 这时你不想看了, 点击 Home 键, 回到后台, 打开 B 开始玩. 过了一段时间之后, 由于 A 没有写后台运行的功能, 这时, 系统会关闭 A, 再打开时, 你看到的是之前进入的详情页面.

系统一点的话说就是, 系统在进入后台时会保存 app 的层次结构, 在下一次进入的时候会恢复这个结构中所有的 controller. 系统在终止之前会遍历结构中每一个节点, 恢复标识, 类, 保存的数据. 在终止应用之后, 系统会把这些信息存储在系统文件中.


恢复标识

一般和对象的类名相同, 其类被称为恢复类.


##实现

下面通过一个 demo 演示状态恢复的实现, 这个 demo 是一个保存联系人信息的 demo. 以下代码以 demo 中控制器为例. 建议 demo 和本文一起看, 更好理解.

demo地址

###1. 开启

默认情况下, app 的状态恢复是关闭的, 需要我们手动开启.
在 AppDelegate.m 中手动打开:

1
2
3
4
5
6
7
8
9
10
#pragma mark - open state restoration

// 和NSCoding协议方法有点像, encode, decode
- (BOOL)application:(UIApplication *)application shouldSaveApplicationState:(NSCoder *)coder {
return YES;
}

- (BOOL)application:(UIApplication *)application shouldRestoreApplicationState:(NSCoder *)coder {
return YES;
}

系统在保存 app 状态时, 会先从 root VC 去查询是否有restorationIdentifier属性, 如果有, 则保存状态, 继续查询其子控制器, 有则保存. 直到找不到带有restorationIdentifier的子控制器, 系统会停止保存其与其子控制器的状态.

画个图解释一下:
状态恢复示意图

上图三级 VC 即使有restorationIdentifier也不会恢复.

application:willFinishLaunchingWithOptions:方法会在启用状态恢复之前调用, 我们需要将触发启用方法之前的代码写在这个方法中.

1
2
3
4
5
6
7
8
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

self.window = [[UIWindow alloc] init];
self.window.frame = [UIScreen mainScreen].bounds;
self.window.backgroundColor = [UIColor whiteColor];

return YES;
}

然后为根视图控制器添加恢复标识:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

// 如果没有触发恢复, 则重新设置根控制器
if (!self.window.rootViewController) {

YNMainTableController *table = [[YNMainTableController alloc] init];

UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:table];

nav.restorationIdentifier = NSStringFromClass([nav class]);

self.window.rootViewController = nav;
}

[self.window makeKeyAndVisible];

return YES;
}

2. 为子控制器实现

a. 设置恢复标识和恢复类

在一级控制器初始化方法中为其设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma mark - initial

- (instancetype)init {

self = [super init];

if (self) {

// 设置恢复标识和恢复类
self.restorationIdentifier = NSStringFromClass([self class]);
self.restorationClass = [self class];
}

return self;
}

在子控制器中设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (instancetype)initWithNewItem:(BOOL)isNew {

self = [super initWithNibName:nil bundle:nil];

if (self) {

// 设置恢复类和恢复标识
self.restorationIdentifier = NSStringFromClass([self class]);
self.restorationClass = [self class];

if (isNew) {

UIBarButtonItem *doneItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(save:)];
self.navigationItem.rightBarButtonItem = doneItem;

UIBarButtonItem *cancelItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancel:)];
self.navigationItem.leftBarButtonItem = cancelItem;
}
}

return self;
}

如果是模态推出带有navigationController的控制器, 需要为这个 nav 设置恢复标识:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)addNewItem:(id)sender {

YNCustomItem *item = [[YNItemHandler sharedStore] createItem];

YNSonViewController *sonVC = [YNSonViewController newItem:YES];
sonVC.item = item;

UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:sonVC];

// 为 UINavigationController 设置恢复类
nav.restorationIdentifier = NSStringFromClass([nav class]);

[self presentViewController:nav animated:YES completion:nil];
}

b. 遵循恢复协议

需要状态恢复的控制器需要遵循<UIViewControllerRestoration>协议:

一级视图控制器中:

1
2
3
4
5
#pragma mark - view controller restoration

+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder {
return [[self alloc] init];
}

同样, 二级视图控制器中, demo 中添加新联系人信息和查看联系人信息调用的是同一个控制器, 初始化方法为自己封装的方法newItem:(BOOL)isNew, isNew 为 NO 时, 查看联系人, 为 YES 时, 新建联系人. 此时有两种情况:

  1. 新建联系人:

    在恢复状态时newItem:(BOOL)isNew参数传入 YES

  2. 查看联系人:

    参数传入 NO

那么如何判断传入什么参数呢? 通过

1
+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder;

方法中的identifierComponents来判断, identifierComponents存储了当前视图控制器及其所有上级视图控制器的恢复标识. 那么现在我们来看一下:

  1. 新建联系人程序中的恢复标识有:
    1. root VC 的 nav 恢复标识
    2. 二级 VC 的 nav 恢复标识(没有一级 VC 的标识是因为 二级 VC 是由一级 VC 的 nav 模态出来的)
    3. 二级 VC 自身的恢复标识
  2. 查看联系人的恢复标识有:
    1. 根 VC 的 nav 恢复标识
    2. 二级 VC 自身的恢复标识(没有一级的和上面同理)

所以新建联系人的 VC 的identifierComponents的个数为3, 查看联系人的为2个. 那么则可以判断参数如何传递:

1
2
3
4
5
6
7
8
9
10
11
12
#pragma mark - view controller restoration

+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder {

BOOL isNew = NO;

if (identifierComponents.count == 3) {
isNew = YES;
}

return [[self alloc] initWithNewItem:isNew];
}

c. 为 nav 设置恢复类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 如果某个对象没有设置恢复类, 那么系统会通过 AppDelegate 来创建
- (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder {

UINavigationController *nav = [[UINavigationController alloc] init];

// 恢复标识路径中最后一个对象就是 nav 的恢复标识
nav.restorationIdentifier = [identifierComponents lastObject];

if (identifierComponents.count == 1) {
self.window.rootViewController = nav;
}

return nav;
}

至此, 控制器的状态恢复已完成, 但是现实的数据还需要做持久化处理, 否则只是恢复了一个没有数据的控制器.

d. 数据持久化

使二级页面详情页需要的数据保存:

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
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
[coder encodeObject:self.item.itemKey forKey:kRestorationKey];

// 保存 textField 中的文本, 以便恢复更改后的文本
self.item.name = self.nameField.text;
self.item.phoneNumber = [self.phoneField.text integerValue];
self.item.sex = self.sexField.text;

// 存入本地
[[YNItemHandler sharedStore] saveItems];

[super encodeRestorableStateWithCoder:coder];
}

- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {

NSString *itemKey = [coder decodeObjectForKey:kRestorationKey];

for (YNCustomItem *item in [[YNItemHandler sharedStore] allItems]) {
if ([item.itemKey isEqualToString:itemKey]) {
self.item = item;
NSLog(@"name:%@, phone:%ld, sex:%@", self.item.name, self.item.phoneNumber, self.item.sex);
break;
}
}

[super decodeRestorableStateWithCoder:coder];
}

二级页面状态恢复完成, 这时候测试(测试方法: 运行后, cmd + shift + h回到桌面, Xcode停止运行, 然后再运行), 重新打开项目, 发现视图控制器状态是恢复了, 但是数据还是空白. 然后打上断点看了下周期, 把数据获取方法写在viewWillAppear:里就好了.

e. 记录 tableview 状态

为一级 VC 设置其 tableView 的恢复标识:

1
2
3
4
5
6
7
8
9
10
11
- (void)viewDidLoad {
[super viewDidLoad];

self.navigationItem.title = @"State Restoration";
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addNewItem:)];

[self.tableView registerNib:[UINib nibWithNibName:NSStringFromClass([YNTableViewCell class]) bundle:nil] forCellReuseIdentifier:kCellIdentifier];

// 给 tableView 设置恢复标识, tableView 自动保存的 contentOffset 会恢复其滚动位置
self.tableView.restorationIdentifier = kTableViewIdentifier;
}

记录 tableView 是否处于 editing 状态:

1
2
3
4
5
6
7
8
9
10
// 记录 tableView 是否处于编辑状态
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
[coder encodeBool:self.isEditing forKey:kTableViewEditingKey];
[super encodeRestorableStateWithCoder:coder];
}

- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
self.editing = [coder decodeBoolForKey:kTableViewEditingKey];
[super decodeRestorableStateWithCoder:coder];
}

通过<UIDataSourceModelAssociation>协议使视图对象在恢复时关联正确的 model 对象. 当保存状态时, 其会根据 indexPath 保存一个唯一标识.

实现<UIDataSourceModelAssociation>协议方法:

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
- (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view {
NSString *identifier = nil;

if (idx && view) {
YNCustomItem *item = [[YNItemHandler sharedStore] allItems][idx.row];
identifier = item.itemKey;
}

return identifier;
}

- (NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view {

NSIndexPath *indexPath = nil;

if (identifier && view) {

for (YNCustomItem *item in [[YNItemHandler sharedStore] allItems]) {

if ([identifier isEqualToString:item.itemKey]) {
NSInteger row = [[[YNItemHandler sharedStore] allItems] indexOfObjectIdenticalTo:item];
indexPath = [NSIndexPath indexPathForRow:row inSection:0];
break;
}
}
}

return indexPath;
}

最后记得在进入后台前持久化当前的 item(实际开发中记得用 cache(项目里使用 YYCache) 或者 db(项目里使用 FMDB) 去即时持久化视图数据, 是一个比较稳妥的方案):

1
2
3
4
5
6
7
8
9
- (void)applicationDidEnterBackground:(UIApplication *)application {
BOOL success = [[YNItemHandler sharedStore] saveItems];

if (success) {
NSLog(@"成功保存所有项目");
} else {
NSLog(@"保存项目失败");
}
}

至此, 状态恢复基本使用已经实现.


测试

  1. 添加 n 个新的联系人, 滑动列表到测试位置, 让 tableView 进入到编辑状态. 按下cmd + shift + h进入 home, 用 Xcode 结束程序cmd+., 再次运行看看是否在最后滑动位置, 或者是否处于编辑状态.
  2. 恢复编辑状态, 随便进入一个联系人详情, 重复上面的操作, 看看进入程序之后是否处于上次退出前的详情页面.

理解面向对象六大原则

发表于 2016-07-02 | 分类于 小技巧

单一职责原则

Single Responsibility Principle - SRP,就一个类而言,应该仅有一个引起它变化的原因。

单一职责原则的优点是,类的责任划分的很清楚,可以提高代码的可读性,减少维护的成本。

以京东为例,例如:

1
2
3
4
5
6
7
8
9
10
11
@interface jder : NSObject 

@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *workNumber;

// 写京东 App
- (void)writeJDApp;
// 送快递
- (void)deliverPackage;

@end

这样看来这个类就有三个职责,第一个是保存员工信息,第二个是写 App,第三个是送快递。这样,日后如果需要增加员工职能,则只好在这个类中增加,从而增加了维护成本和减少了代码可读性。

遵从单一职责原则,可以修改为:

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
@interface JDer : NSObject 

@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *workNumber;

// 写京东 App
- (void)writeJDApp;
// 送快递
- (void)deliverPackage;

@end

#import "JDer.h"
@interface Development : NSObject

// 写京东 App
- (void)writeJDApp:(JDer *)jder;
// 升级聊天模块
- (void)updateIM:(JDer *)jder;

@end

#import "JDer.h"
@interface Express : NSObject

// 送快递
- (void)deliverPackage:(NSString *)packageName jder:(JDer *)jder;
// 分拣快递
- (void)sortPackage:(NSDate *)date jder:(JDer *)jder;

@end

由此可以看出,开发和快递抽出放在两个类中,分工明确。代码可读性高,如果都挤在员工类,那么后期修改起来就很麻烦。如果后期开发想升级 IM 模块,那直接在开发类中指定某一个员工去升级 IM 模块,或者在快递类中增加一个某员工去分拣某天的快递。

开闭原则

Open Close Principle - OCP,程序中的对象应该对应扩展是开放的,对于修改是封闭的。

以一个论坛模型模型为例,假设一开始只有 item 的名称,描述和图片,在后期的迭代中,需要加入小视频,允许用户上传音频,需要加入音频。如果在一个 model 类中实现,可能会造成如下问题, 如果后期还需要增加别的东西:

  1. 反复修改最早创建的 Model 类
  2. 有些开发者上传音频或视频,有些不上传,这就造成了冗余

例如:

1
2
3
4
5
6
7
8
9
@interface App : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *desc;
@property (copy, nonatomic) NSString *picURL;

// --- 后期迭代新增 ---
@property (copy, nonatomic) NSString *videoURL;
@property (copy, nonatomic) NSString *musicURL;
@end

这样的做法,不遵从对扩展开放,修改关闭,而是直接修改了这个类。可以通过继承的方式将其拆分。

定义一个 App model 基类(假定所有 App 都需要展示名称、描述和图片):

1
2
3
4
5
@interface Item : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *desc;
@property (copy, nonatomic) NSString *picURL;
@end

定义一个视频 App:

1
2
3
@interface VideoItem : Item
@property (copy, nonatomic) NSString *videoURL;
@end

定义一个音频 App:

1
2
3
@interface MusicItem : Item
@property (copy, nonatomic) NSString *musicURL;
@end

这样在以后的迭代中,如果要增加 HD 视频和音频,那么直接在对应的 Item 子类中添加即可。

里氏替换原则

Liskov Substitution Principle - LSP

依赖倒置原则

Dependency Inversion Principle - DIP,模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。即依赖抽象,而不依赖具体的实现。

对于定义,通俗的,可以理解为,针对 api 编程,而不是针对实现编程,不要从具体的类派生,而是通过继承抽象类或者实现接口去实现功能。

以一个宴会为例,一个宴会有中餐厨师和西餐厨师,并且已经订好了晚宴菜谱,有中餐和西餐,分别建立两个类:

1
2
3
4
5
6
7
8
9
@interface ChineseCook: NSObject
// 辣椒炒肉
- (void)capsicumFriedMeat;
@end

@interface WesternCook: NSObject
// 煎牛排
- (void)friedBeefSteak;
@end

接着有一个做菜类,等宾客到来,就开始做菜了:

1
2
3
4
5
6
7
8
@interface Cooking: NSObject

- (instanceType)initWithCooks:(NSArray *)cooks;
- (void)beginCooking;

@end

@implemetation Cooking

接口隔离原则

Interface Segregation Principle - ISP

迪米特法则

Law of Demeter(Least Knowledge Principle)- LoD

iOS 动态字体的实现

发表于 2016-05-25 | 分类于 小技巧

效果图

最近项目要求字体大小不能固定, 要追随系统设置, 先上效果图. 第一张, 系统最小字体, 第二张, 苹果最大字体.(注: 真机测试)
IMG_1169
IMG_1170


普通界面实现

preferredFontForTextStyle: 方法

preferredFontForTextStyle:方法会根据用户首选字体和传入的文本样式返回对应的UIFont对象

添加方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)setUpFonts {

// preferredFontForTextStyle 方法会根据用户首选字体和传入的文本样式返回对应的 UIFont 对象
UIFont *font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];

self.nameLabel.font = font;
self.phoneLabel.font = font;
self.sexLabel.font = font;

self.nameField.font = font;
self.phoneField.font = font;
self.sexField.font = font;
}
  • 如果是用 xib 拖的控件, 不要限定死控件的宽高, 然它们在 pin 菜单等宽即可. 如果限定死宽高, 文本会显示不全, 如图:

屏幕快照 2016-05-25 下午2.08.39

然后在视图加载方法中调用此方法:

1
2
3
4
5
6
7
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];

[self setUpViewData];

[self setUpFonts];
}

此时按下HOME键, 在通用->辅助功能->更大字体中改变系统显示字体大小. 切回程序, 发现这个视图里需要改变字体大小的控件的字体并没有改变, 这是因为程序并不知道系统更改字体, 我们需要注册一个监听事件告诉程序字体改变了.
注册监听:

1
2
3
4
5
// 注册观察者, 监听字体改变
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(setUpFonts)
name:UIContentSizeCategoryDidChangeNotification
object:nil];

其中UIContentSizeCategoryDidChangeNotification可以监听内容尺寸的变化
在dealloc方法中销毁观察者

1
2
3
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

至此, 重新运行程序, 重复上面的设置字体大小步骤, 字体发生效果图上的改变.


tableView 上改变字体

tableView上设置动态字体需要注意 rowHeight 也要动态随字体改变.

效果图

系统最小字体和最大字体IMG_1173

IMG_1172

实现

首先, 我们需要更改cell内字体的显示
和普通界面方法一样, 直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)dealloc {

[[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)setUpCellFont {

UIFont *font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
self.nameLabel.font = font;
self.phoneLabel.font = font;
self.sexLabel.font = font;
}

- (void)awakeFromNib {
[super awakeFromNib];
// Initialization code

[self setUpCellFont];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(setUpCellFont)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
}

接下来, 我们需要根据字体来设置 rowHeight 属性

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
- (void)setUpTableViewFont {

static NSDictionary *cellHeight;

if (!cellHeight) {

cellHeight = @{UIContentSizeCategoryExtraSmall: @44,// 没有 Accessibility 是普通字体的大小
UIContentSizeCategorySmall: @44,
UIContentSizeCategoryMedium: @44,
UIContentSizeCategoryLarge: @44,
UIContentSizeCategoryExtraLarge: @55,
UIContentSizeCategoryExtraExtraLarge: @65,
UIContentSizeCategoryExtraExtraExtraLarge: @75,
UIContentSizeCategoryAccessibilityMedium: @75, // 注意这个是更大字体的方法
UIContentSizeCategoryAccessibilityLarge: @85,
UIContentSizeCategoryAccessibilityExtraLarge: @95,
UIContentSizeCategoryAccessibilityExtraExtraLarge: @105,
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: @115};
}

// Application 单例自带方法判断内容尺寸
NSString *tableViewSize = [[UIApplication sharedApplication] preferredContentSizeCategory];

// 根据尺寸在 cellHeight 字典中选择相应的行高
NSNumber *tableViewRowHeight = cellHeight[tableViewSize];

self.tableView.rowHeight = tableViewRowHeight.floatValue;

[self.tableView reloadData];
}

方法调用, 注册观察者, 销毁观察者:

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
- (void)dealloc {

[[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (instancetype)initWithStyle:(UITableViewStyle)style {

self = [super initWithStyle:UITableViewStylePlain];

if (self) {

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(setUpTableViewFont)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
}

return self;
}

- (void)viewWillAppear:(BOOL)animated {

[super viewWillAppear:animated];

[self setUpTableViewFont];
}

OK, 真机上运行程序, 调整字体大小
别的情况以此类推即可.

Core Graphics 基本绘制操作

发表于 2016-05-21 | 分类于 Core Graphics

Core Graphics vs. UIKit

Core Graphics UIKit
基于 C 的 API UIBezierPath 类
面向过程 面向对象
UIKit 绘制的基石 构建在 Core Graphics 之上
可以通过参数接收 Context 绘制 只能基于当前 Context 绘制

坐标系

UIKit 坐标系和数学坐标系相反
Core Graphics 默认坐标系和数学坐标系一样
一般基于 UIKit 坐标系进行绘制

坐标系转换:

Swift:

1
2
context?.translateBy(x: 0, y: bounds.height)
context?.scaleBy(x: 1.0, y: -1.0)

OC:

1
2
CGContextTranslateCTM(context, 0, bounds.size.height)
CGContextScaleCTM(context, 1.0, -1.0)

绘制过程

  1. 获取上下文
  2. 设置区域
  3. 绘制所需图形
  4. 设置填充或线条颜色
  5. 填充或绘制线条

绘制代码

椭圆形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func makeEllipse(_ context: CGContext?, _ rect: CGRect) {
// 获取图形上下文

let rectangle = CGRect(x: 20, y: 30, width: 280, height: 260)

// 添加椭圆
context?.addEllipse(in: rectangle)

// 设置填充颜色
context?.setFillColor(UIColor.orange.cgColor)

// 填充路径
context?.fillPath()
}

圆形和空心圆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func drawCircle(_ context: CGContext?, at point: CGPoint) {
let context = UIGraphicsGetCurrentContext()

let radius: CGFloat = 20.0

// 绘制圆
context?.addArc(center: point, radius: radius, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)

// 设置线条宽度颜色
context?.setStrokeColor(UIColor.blue.cgColor)
context?.setLineWidth(3.0)

// 绘制空心圆
context?.strokePath()

// 绘制小圆形
context?.addArc(center: point, radius: radius/2, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: false)

context?.setFillColor(UIColor.blue.cgColor)

context?.fillPath()
}

三角形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func drawTriangle(_ context: CGContext?) {

// 创建一个新的路径
context?.beginPath()

// 添加起始点
context?.move(to: CGPoint(x: 160, y: 140))

// 添加线条
context?.addLine(to: CGPoint(x: 190, y: 190))
context?.addLine(to: CGPoint(x: 130, y: 190))

// 关闭并终止当前路径的子路径
context?.closePath()

context?.setFillColor(UIColor.brown.cgColor)

context?.fillPath()
}

矩形

1
2
3
4
5
6
7
8
9
10
11
func drawRectangle(_ context: CGContext?) {

let rectangle = CGRect(x: 100, y: 225, width: 120, height: 15)

// 添加矩形路径
context?.addRect(rectangle)

context?.setFillColor(UIColor.red.cgColor)

context?.fillPath()
}

贝塞尔曲线参考

用 NSCalendar 处理类似微博发帖时间并封装

发表于 2016-03-14 | 分类于 小技巧

服务器返回数据

服务器返回数据往往是一整串时间, 放在发帖时间label上面太长不好看, 我们需要手动处理一下使其变得美观易读.

代码

Date+Category 封装

需要处理服务器返回一大段时间, 我们需要比较现在时间和发帖时间
首先利用日期格式化类NSDateFormatter去格式化服务器返回的一长串时间字符串.

1
2
3
4
5
6
//日期格式化类
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
//设置日期格式
dateFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss";

(...此处省略其他代码)

如果直接将以上代码放在cell里面读取会使cell里setModel方法显得冗长, 不美观, 我选择将它们封装起来. 为了便于其他人维护, 不再新建一类, 直接用date+category增加方法.
在date+category方法中自己写入方法:- (NSDateComponents *)zyn_deltaFromDateString:(NSString *)dateString withDateFormat:(NSString *)dateFormat;
然后实现

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
/**
比较传入时间和现在时间的差值

@param dateString 获取的时间字符串
@param dateFormat 字符串时间格式

@return 差值
*/
- (NSDateComponents *)zyn_deltaFromDateString:(NSString *)dateString
withDateFormat:(NSString *)dateFormat {
//日期格式化类
NSDateFormatter *zyn_dateFormat = [[NSDateFormatter alloc] init];
//设置日期格式
zyn_dateFormat.dateFormat = dateFormat;

//创建时间
NSDate *create = [zyn_dateFormat dateFromString:dateString];

//日历
NSCalendar *zyn_calendar = [NSCalendar currentCalendar];

//比较时间
NSCalendarUnit zyn_unit = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
return [zyn_calendar components:zyn_unit fromDate:create toDate:self options:0];
}

时间判断

在求出时间差值以后, 需要自己对时间进行判断, 做条件的话, 封装BOOL方法比较适合, 需要做时间判断的个人认为只需要做今年以内的时间判断就好了, 之前的时间, 去年, 前年, 大前年, 这种完全没必要.

同样是在date类目中

今年?

1
2
3
4
5
6
7
8
9
10
11
12
13
- (BOOL)isCurrentYear {

//现在时间
NSDate *current = [NSDate date];

//日历
NSCalendar *calender = [NSCalendar currentCalendar];

NSInteger currentYear = [calender component:NSCalendarUnitYear fromDate:current];
NSInteger selfYear = [calender component:NSCalendarUnitYear fromDate:self];

return currentYear == selfYear;
}

今天?

1
2
3
4
5
6
7
8
9
10
11
12
- (BOOL)isToday {

NSDate *current = [NSDate date];

NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"yyyy-MM-dd";

NSString *nowStr = [fmt stringFromDate:current];
NSString *selfStr = [fmt stringFromDate:self];

return [nowStr isEqualToString:selfStr];
}

昨天?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (BOOL)isYesterday {

NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"yyyy-MM-dd";

NSDate *currentDate = [fmt dateFromString:[fmt stringFromDate:[NSDate date]]];
NSDate *selfDate = [fmt dateFromString:[fmt stringFromDate:self]];

NSCalendar *calendar = [NSCalendar currentCalendar];

NSCalendarUnit unit = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay;
NSDateComponents *cmps = [calendar components:unit fromDate:selfDate toDate:currentDate options:0];

return cmps.year == 0 && cmps.month == 0 && cmps.day == 1;
}

OK, 时间类目封装完毕, 需要在cell的label上显示时间了.
同样判断时间需要很长的代码, 写在cell上过于冗长, 影响美观, 既然label.text是字符串, 那么我们就在string+category中完成这些判断即可.


string+category封装

直接上代码了, 逻辑就不细说了, 反正先从大时间开始判断就行了

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
#import "NSString+ZYNExtension.h"

@implementation NSString (ZYNExtension)

- (NSString *)zyn_timeWithString:(NSString *)timeStr
andOriginalDateFormat:(NSString *)dateFormat
andYesterdayFormat:(NSString *)yFormat
andYearFormat:(NSString *)yearFormat {

// 日期格式化类
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
// 设置日期格式(y:年,M:月,d:日,H:时,m:分,s:秒)
fmt.dateFormat = dateFormat;
// 帖子的创建时间
NSDate *create = [fmt dateFromString:timeStr];

if (create.isCurrentYear) {
if (create.isToday) {

//取出时间组成
NSDateComponents *cmps = [[NSDate date] zyn_deltaFromDateString:timeStr withDateFormat:dateFormat];

if (cmps.hour >= 1) {

return [NSString stringWithFormat:@"%zd小时前", cmps.hour];

//大于一分钟还是大于半分钟还是大于几秒钟, 这个根据自己喜好再加判断就行
} else if (cmps.minute >= 1) {

return [NSString stringWithFormat:@"%zd分钟前", cmps.minute];

} else {

return @"刚刚";
}

} else if (create.isYesterday) {
//传入昨天需要的时间格式
fmt.dateFormat = yFormat;
return [fmt stringFromDate:create];

} else {

//传入今年其他时间需要的格式
fmt.dateFormat = yearFormat;
return [fmt stringFromDate:create];
}

} else {
return timeStr;
}
}
@end

String类目封装完事, 接下来就是在cell上显示了, model和cell中代码会非常简单


model和cell中的代码

在pch文件中引入上面的头文件

model中代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import "ZYNHomeModel.h"

static NSString *const dateFormat = @"yyyy-MM-dd HH:mm:ss";

@implementation ZYNHomeModel

//重写网络获取的时间字符串get方法
- (NSString *)createTime {

return [[NSString string] zyn_timeWithString:_createTime
andOriginalDateFormat:dateFormat
andYesterdayFormat:@"昨天 HH:mm"
andYearFormat:@"MM-dd HH:mm"];
}

@end

cell中代码

在setModel方法中:
self.timeLabel.text = self.homeModel.createTime;
即可, 不用出现任何逻辑判断


效果

屏幕快照 2016-03-14 下午3.31.40

屏幕快照 2016-03-14 下午3.32.03

利用 Runtime 修改 UITextfield 私有属性

发表于 2016-03-05 | 分类于 Runtime

前言

系统自带UITextField的光标是默认深蓝色的, placeholder是灰色的, 在开发中, 一定程度上可能影像UI的美观程度, 所以我们需要自定义一个textField来完善界面.
而在textField自带方法和属性中是没有关于placeholder属性或是方法的, 仅有Attributes可以修改, 个人认为是没有runtime来的方便的.


Runtime使用简介

运行时(Runtime):
苹果官方的一套C语言库
利用Runtime可以查看很多底层隐藏内容
比如一些成员变量 / 方法


利用Runtime查看UITextField隐藏成员变量

首先, 需要在自定义UITextField头文件处导入:

1
#import <objc/runtime.h>

在初始化方法中敲入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+ (void)initialize {

unsigned int count = 0;

//拷贝出所有成员变量列表
Ivar *ivars = class_copyIvarList([UITextField class], &count);

for (int i = 0; i < count; i++) {

//取出成员变量
Ivar ivar = *(ivars + i);

//打印成员变量名字
NSLog(@"%s", ivar_getName(ivar));
}

//释放
free(ivars);
}

此时要注意, 尽管在ARC模式下, 取出变量后要依然手动释放内存, 利用free()方法即可:
free(...)
运行程序后, 控制台会输出如下隐藏内容:
屏幕快照 2016-03-05 下午12.14.35


修改placeholder的颜色

创建全局变量

根据控制台输出内容找到自己需要的成员变量后, 可以使用KVC修改
首先创建一个全局变量, 例如我们需要修改placeholder的颜色:

1
static NSString *const placeholderColorKeyPath = @"_placeholderLabel.textColor";

通过KVC改写

然后通过KVC在第一响应方法中修改(注意是setValue: forKeyPath: 方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

- (BOOL)becomeFirstResponder {

[self setValue:self.textColor forKeyPath:placeholderColorKeyPath];

return [super becomeFirstResponder];
}

- (BOOL)resignFirstResponder {

//修改占位文字颜色
[self setValue:[UIColor grayColor] forKeyPath:placeholderColorKeyPath];

return [super resignFirstResponder];
}

此时再运行程序可以发现选中和未选中时的placeholder颜色变得不一样了.


修改输入框光标颜色

在textField中, 是没有indicator这个属性的, 光标颜色其实就是tintColor, 例如:

1
2
3
4
5
6
7
8
- (void)awakeFromNib {

//设置光标颜色和文字颜色一致
self.tintColor = self.textColor;

//在进入页面时是没有选中的要调用resign方法
[self resignFirstResponder];
}

查看其它属性

同ivar一样, 可以使用runtime查看properties

1
2
3
4
5
6
7
8
9
10
11
12
13
+ (void)initialize {
unsigned int count = 0;

objc_property_t *properties = class_copyPropertyList([UITextField class], &count);

for (int i = 0; i < count; i++) {
objc_property_t property = properties[i];

NSLog(@"%s", property_getName(property));
}

free(properties);
}

记住不要忘记free()方法


完整代码

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
#import "CustomField.h"
#import <objc/runtime.h>

static NSString *const placeholderColorKeyPath = @"_placeholderLabel.textColor";

@implementation CustomField

#if 0
+ (void)initialize {

unsigned int count = 0;

//拷贝出所有成员变量列表
Ivar *ivars = class_copyIvarList([UITextField class], &count);

for (int i = 0; i < count; i++) {

//取出成员变量
Ivar ivar = *(ivars + i);

//打印成员变量名字
NSLog(@"%s", ivar_getName(ivar));
}

//释放
free(ivars);
}
#endif

#if 0
/**
找出系统隐藏属性
*/
+ (void)initialize {
unsigned int count = 0;

objc_property_t *properties = class_copyPropertyList([UITextField class], &count);

for (int i = 0; i < count; i++) {
objc_property_t property = properties[i];

NSLog(@"%s", property_getName(property));
}

free(properties);
}
#endif

- (void)awakeFromNib {

//设置光标颜色和文字颜色一致
self.tintColor = self.textColor;

[self resignFirstResponder];
}

- (BOOL)becomeFirstResponder {

[self setValue:self.textColor forKeyPath:placeholderColorKeyPath];

return [super becomeFirstResponder];
}

- (BOOL)resignFirstResponder {

//修改占位文字颜色
[self setValue:[UIColor grayColor] forKeyPath:placeholderColorKeyPath];

return [super resignFirstResponder];
}

@end
123

Yinan Zheng

21 日志
11 分类
15 标签
GitHub E-Mail
© 2018 Yinan Zheng
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4