本期知识小集:
- 如何定制一个 UIView 类型控件的出入动画
- UIView 的事件透传
- iOS 如何调试 WebView (二)
- 一个结构较为合理的下载模块该怎么设计
- 再谈 iOS 输入框的字数统计/最大长度限制
如何定制一个 UIView 类型控件的出入动画
作者: halohily
在 iOS 开发中,自定义的弹层组件非常常见,比如分享框、自定义的 actionSheet 组件等。有的场景下,会选择使用 UIViewController 类型来实现,这时定制这个视图的出现、隐藏动画非常方便。然而,有时候需要选择轻量级的 UIView 类型来实现。这时该怎么定制它的出现、隐藏动画呢?这里提供一个思路:
使用 UIView 的
willMoveToSuperview:
和didMoveToSuperview
这组方法,它们会在UIView
作为subView 被添加到其他 UIView 中时调用。这里需要注意,自身调用removeFromSuperview
方法时,同样会触发这组方法,只不过这时的参数会是一个 nil。
提供一个例子来说明:一个选择 UIView 类型实现的自定义 actionSheet 的出入动画,交互基本和微信一致。
#pragma mark - show & dismiss
- (void)didMoveToSuperview {
if (self.superview) {
[UIView animateWithDuration:0.35 delay:0 usingSpringWithDamping:0.9 initialSpringVelocity:10 options:UIViewAnimationOptionCurveEaseIn animations:^{
_backgroundControl.alpha = 1;
self.actionSheetTable.frame = CGRectMake(0, SCREEN_HEIGHT - _sheetHeight, SCREEN_WIDTH, _sheetHeight);
} completion:^(BOOL finished) {
[super didMoveToSuperview];
}];
}
}
- (void)hideSelf {
[UIView animateWithDuration:0.35 delay:0 usingSpringWithDamping:0.9 initialSpringVelocity:10 options:UIViewAnimationOptionCurveEaseIn animations:^{
_backgroundControl.alpha = 0;
self.actionSheetTable.frame = CGRectMake(0, SCREEN_HEIGHT, SCREEN_WIDTH, _sheetHeight);
} completion:^(BOOL finished) {
[self removeFromSuperview];
}];
}
复制代码
UIView 的事件透传
作者: Vong_HUST
通常我们会遇到这种需求,一个视图除了需要响应子视图的点击事件,其它空白地方希望能将点击事件透传到,比如自定义了一个“导航栏”,除了左右两边按钮,希望其它部分点击能够透传到底下的视图。这个时候我们可以通过复写 hitTest
方法,具体实现如下。
@implementation PassthroughView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.hidden || self.alpha < FLT_EPSILON || self.userInteractionEnabled) {
return [super hitTest:point withEvent:event];
}
UIView *targetView = nil;
for (UIView *subview in [[self subviews] reverseObjectEnumerator]) {
if ((targetView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event])) {
break;
}
}
return targetView;
}
@end
复制代码
以上代码即可实现,只响应子视图的事件,而非子视图区域部分的交互事件则透传到响应链中的下一个响应者。
如果你有其它更好方式,也可以分享出来,一起交流下。
iOS 如何调试 WebView (二)
作者: Lefe_x
上次的小集中,我主要讨论了如何调试 WebView ,小集发出后 @折腾范儿_味精
提供了另一种方法来调试 WebView。我觉得有必要再扩展一下,原话是这样的:
真说方便还是植入一个 webview console 在 debug 环境,可以在黑盒下不连电脑不连 safari 调 dom,调 js,另外在开发期间 Xcode 断点 run 的时候,js hook console.log console.alert,接管 window.onerror 全都改 bridge NSLog 输出,也会方便点。
短短几句话,信息量很大,私下向味精学习了下,这里总结一下。写完这个小集特意让味精看了下,觉得有必要再补充下第二种调试技巧,但中途踩了几个坑,一直到23:30左右才搞定。
第一,把 WebView 用来调试的 log、alert、error 显示到 NA ,在调试时会方便不少。做 WebView 与端交互的时候,主要用 window.webkit.messageHandlers.xxx.postMessage(params);
来给端发消息,也就是说 WebView 想给端发消息的时候直接调用这个方法即可,端会通过 WKScriptMessageHandler
的代理方法来接收消息,而此时端根据和 WebView 约定的规则进行通信即可。
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
复制代码
而添加调试信息,无非就是给 WebView 添加了 log、alert、error 这些消息的 bridge,这样当 WebView 给端发送消息后,端根据和 WebView 约定的规则解析 log、alert、error 为端对应的事件,比如 log 直接调用端的 NSLog
,alert 调用端的 UIAlertController
。
第二,黑盒下调试 WebView,无需连接电脑和 safari 即可调试 DOM,这个可以参考小程序的 vConsole 或者 eruda 。可以直接在 WebView 中接入,或者在端中接入。这里以在端中接入 eruda 为例,这里踩到几个坑:
1.有些页面显示不出来,估计是故意屏蔽掉的,味精特意使用 JSBox 试了下其它页面,发现百度等都不可以显示调试按钮,而掘金是可以的;
2.使用本地的页面也显示不出来,这是 webview 跨域安全方面的考虑,file 协议下会禁止 js css html 以部分 file,部分网络的方式加载。
下面这段代码直接在 webview 加载完成后执行即可。
NSString *js = @"(function() {var script = document.createElement('script');script.type = 'text/javascript';script.src = 'https://xteko.blob.core.windows.net/neo/eruda-loader.js';document.body.appendChild(script);})();";
[self.webView evaluateJavaScript:js completionHandler: nil];
复制代码
一个结构较为合理的下载模块该怎么设计
作者: halohily
最近负责下载组件的开发,对于如何设计一个下载模块有一些粗浅体会,今天分享一下我采用的方案,希望能够抛砖引玉。
“下载”作为一个需要本地结构化、持久化存储的场景,使用数据库是比较自然的选择。所以,我们首先拆分出一个数据库模块,用来存储下载记录。主要字段为下载任务的信息,如 url、文件大小、时间戳等,以及最重要的文件本地存储路径。这一层可以在接口设计上认真思虑,比如仅涉及当前业务逻辑,而不涉及具体的数据库操作,相当于是较 FMDB 等数据库组件来说更高层的抽象。后期需要更换底层数据库引擎时,本层封装无需改动,是比较理想的实现。
数据库是用来存储下载记录的,那么所下载的具体文件呢?自然就需要一个文件管理模块,在这个模块里,负责根据文件 url 生成本地的存储路径,以及进行文件校验、存储、移除等操作。
所要下载的文件,我们可以按体积、类型等进行区分。对于网络请求的结果这类简短内容,我抽象出了一个缓存管理器,用来完成网络请求、图片等内容的缓存。网络请求的 JSON 格式结果,可以选择 YYCache
、EGOCache
等缓存框架。而图片的缓存,则可以选择专注图片缓存的 YYWebImage
、SDWebImage
等框架。
对于体积较大的文件,自然需要一个专注大文件下载的模块。这个模块不关注具体的文件类型,不关注具体的业务场景,它只需要文件 url 、文件管理模块生成的本地目标路径,完成下载任务即可。
在以上通用模块的基础上,有一个业务层的封装,它负责根据提交的下载任务,协调调用各基础组件。举个例子,一个下载任务包括一个视频文件、一个网络请求结果、三张图片。本模块在收到任务后,首先解析出以上的任务具体结构。使用文件管理模块,根据视频文件 url 生成本地存储目标路径,调用大文件下载器完成下载,此为一个子任务。对于网络请求结果,调用缓存模块,进行缓存,此为一个子任务。对于三张图片,使用图片缓存器完成缓存,此为一个子任务。三个子任务均完成,使用数据库模块,对下载记录、媒体文件记录等进行存储。除此之外,本模块还负责对外提供下载中任务、已下载任务等数据。
再谈 iOS 输入框的字数统计/最大长度限制
作者: KANGZUBIN
前两周我们发了一个小集「iOS 自带九宫格拼音键盘与 Emoji 表情之间的坑」,介绍了如何解决由于输入框限制 Emoji 表情的输入导致中文拼音也无法输入的问题。
后面我们又有了新需求:对输入框已输入的文本字数进行实时统计,并在界面上显示剩余字数,且不能让所输入的文本超过最大限制长度。但这个简单的功能仍然有不少小坑。
在上一个小集中,我们讲到,对于 iOS 系统自带的键盘,有时候它在输入框中填入的是占位字符(已被高亮选中起来),等用户选中键盘上的候选词时,再替换为真正输入的字符,如下:
这会带来一个问题:比如输入框限定最多只能输入 10 位,当已经输入 9 个汉字的时候,使用系统拼音键盘则第 10 个字的拼音就打不了(因为剩余的 1 位无法输入完整的拼音)。
怎么办呢?上面提到,输入框中的拼音会被高亮选中起来,所以我们可以根据 UITextField
的 markedTextRange
属性判断是否存在高亮字符,如果有则不进行字数统计和字符串截断操作。我们通过监听 UIControlEventEditingChanged
事件来对输入框内容的变化进行相应处理,如下:
[self.textField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
复制代码
- (void)textFieldDidChange:(UITextField *)textField {
// 判断是否存在高亮字符,如果有,则不进行字数统计和字符串截断
UITextRange *selectedRange = textField.markedTextRange;
UITextPosition *position = [textField positionFromPosition:selectedRange.start offset:0];
if (position) {
return;
}
// maxWowdLimit 为 0,不限制字数
if (self.maxWowdLimit == 0) {
return;
}
// 判断是否超过最大字数限制,如果超过就截断
if (textField.text.length > self.maxWowdLimit) {
textField.text = [textField.text substringToIndex:self.maxWowdLimit];
}
// 剩余字数显示 UI 更新
}
复制代码
对于 UITextView
的处理也是类似的。
另外,对于“字数”的定义是很多种理解:在 Objective-C 中字符串 NSString
的长度 length
,对于一个中文汉字和一个英文字母都是 1;但如果我们要按字节来统计和限制,同一字符在不同编码下所占的字节数也是不同的;另外有时我们要统计的是所输入文本的单词个数,而不是字符串的长度,所以我们需要根据不同的使用场景进行分析。