本期知识小集:

  • 如何定制一个 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 格式结果,可以选择 YYCacheEGOCache 等缓存框架。而图片的缓存,则可以选择专注图片缓存的 YYWebImageSDWebImage 等框架。

对于体积较大的文件,自然需要一个专注大文件下载的模块。这个模块不关注具体的文件类型,不关注具体的业务场景,它只需要文件 url 、文件管理模块生成的本地目标路径,完成下载任务即可。

在以上通用模块的基础上,有一个业务层的封装,它负责根据提交的下载任务,协调调用各基础组件。举个例子,一个下载任务包括一个视频文件、一个网络请求结果、三张图片。本模块在收到任务后,首先解析出以上的任务具体结构。使用文件管理模块,根据视频文件 url 生成本地存储目标路径,调用大文件下载器完成下载,此为一个子任务。对于网络请求结果,调用缓存模块,进行缓存,此为一个子任务。对于三张图片,使用图片缓存器完成缓存,此为一个子任务。三个子任务均完成,使用数据库模块,对下载记录、媒体文件记录等进行存储。除此之外,本模块还负责对外提供下载中任务、已下载任务等数据。

再谈 iOS 输入框的字数统计/最大长度限制

作者: KANGZUBIN

前两周我们发了一个小集「iOS 自带九宫格拼音键盘与 Emoji 表情之间的坑」,介绍了如何解决由于输入框限制 Emoji 表情的输入导致中文拼音也无法输入的问题。

后面我们又有了新需求:对输入框已输入的文本字数进行实时统计,并在界面上显示剩余字数,且不能让所输入的文本超过最大限制长度。但这个简单的功能仍然有不少小坑。

在上一个小集中,我们讲到,对于 iOS 系统自带的键盘,有时候它在输入框中填入的是占位字符(已被高亮选中起来),等用户选中键盘上的候选词时,再替换为真正输入的字符,如下:



这会带来一个问题:比如输入框限定最多只能输入 10 位,当已经输入 9 个汉字的时候,使用系统拼音键盘则第 10 个字的拼音就打不了(因为剩余的 1 位无法输入完整的拼音)。

怎么办呢?上面提到,输入框中的拼音会被高亮选中起来,所以我们可以根据 UITextFieldmarkedTextRange 属性判断是否存在高亮字符,如果有则不进行字数统计和字符串截断操作。我们通过监听 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;但如果我们要按字节来统计和限制,同一字符在不同编码下所占的字节数也是不同的;另外有时我们要统计的是所输入文本的单词个数,而不是字符串的长度,所以我们需要根据不同的使用场景进行分析。