作者: 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;但如果我们要按字节来统计和限制,同一字符在不同编码下所占的字节数也是不同的;另外有时我们要统计的是所输入文本的单词个数,而不是字符串的长度,所以我们需要根据不同的使用场景进行分析。
作者: 无彦主
笔者一般在 ViewController 的 viewWillAppear 中处理导航条的 UI 变化。比如是否隐藏导航栏、改变状态栏颜色等。但是最近发现在 viewWillAppear 中改变 navigationBar 的 titleTextAttributes
属性却在 pop 时出现了问题:「 从 SecondViewController 点击导航栏返回到 FirstViewController 时并没有生效,而使用滑动手势返回却可以生效。」
import UIKit
class FirstViewController: UIViewController {
...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavgationBarTitleTextAttributes(
color: .nav_purple,
font: .nav_regular
)
}
...
}
在 iOS 系统 10 以后,UIKit 已经更新,统一后台管理 UINavigationBar,UITabBar 和 UIToolbar。特别是,对这些视图的背景属性(例如背景或阴影图像或设置条形样式)的更改可能会启动条形码的布局传递以解析新的背景外观。特别地,这意味着,试图改变的内部这些条的背景外观
layoutSubviews,
-[UIView updateConstraints] ,
viewWillLayoutSubviews,
viewDidLayoutSubviews,
updateViewConstraints
或响应布局而调用的任何其他方法都可能导致布局循环。布局更改调用的 viewWillAppear 似乎触发了所提到的布局循环。
比较简单的处理方法是在 SecondViewController 中重写 willMove(:)
方法,在这里将 titleAttribute 赋值回去,但这样的方式不够彻底,它显然不能处理两种或两种以上的状态变化。更为稳妥的的方法是重写自定义 UINavigationController 中的 popViewController(:)
方法。
// NavBarTitleChangeable.swift
import UIKit
public protocol NavBarTitleChangeable: class {
var preferrdTextAttributes: [NSAttributedStringKey: AnyObject] { get }
}
...
// Custom UINavigationController
import UIKit
class FunNavigationViewController: UINavigationController {
private var topViewControllerNavBarTitleAttributes: [NSAttributedStringKey: AnyObject]? {
return (topViewController as? NavBarTitleChangeable)?.preferrdTextAttributes
}
private func setNavBarTitleAttributes(_ attributes: [NSAttributedStringKey: AnyObject]) {
navigationBar.titleTextAttributes = attributes
}
override func viewDidLoad() {
super.viewDidLoad()
if let attributes = topViewControllerNavBarTitleAttributes {
setNavBarTitleAttributes(attributes)
}
}
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
super.pushViewController(viewController, animated: animated)
if let attributes = topViewControllerNavBarTitleAttributes {
setNavBarTitleAttributes(attributes)
}
}
override func popViewController(animated: Bool) -> UIViewController? {
let popViewController = super.popViewController(animated: animated)
if let attributes = topViewControllerNavBarTitleAttributes {
setNavBarTitleAttributes(attributes)
}
transitionCoordinator?.animate(alongsideTransition: nil) { [weak self] _ in
if let attributes = self?.topViewControllerNavBarTitleAttributes {
self?.setNavBarTitleAttributes(attributes)
}
}
return popViewController
}
}
// MyViewController.swift
import UIKit
class FirstViewController: UIViewController, NavBarTitleChangeable {
var preferrdTextAttributes: [NSAttributedStringKey : AnyObject] {
let item = FunNavTitleTextAttributesItem(color: .nav_purple, font: .nav_regular)
return getNavgationBarTitleTextAttributes(with: item)
}
...
}
这里简单展示处理思路,具体代码还是要根据项目需求定制。
作者: halohily
今天是节后上班第一天,也是 WWDC 期间知识小集微博话题暂停之后回归的第一天。欢迎关注本次 WWDC 期间 知识小集 和 老司机、SwiftGG 联合完成的掘金免费专题。
今天小集给大家推荐几个实用的 Xcode 小技巧。
- 快速打开:
Command + Shift + O
。这个命令可以开启一个小窗格用来快速搜索浏览文件、类、算法以及函数等,且支持模糊搜索。这个命令可以说是我日常开发中最高频使用的一个了。 - 显示项目导航器:
Command + Shift + J
。使用快速打开命令跳转到对应文件后,如果需要在左侧显示出该文件在项目中的目录结构,只需要键入这个命令,非常方便。 - 显示编辑历史。如果发现一行需要膜拜或者需要吐槽的代码,不需要跑到专门的 diff 工具查看代码历史。在该代码行处右键,选择
Show Blame for Line
,即可弹出一个小标签,显示该处代码的修改作者、commit 记录等信息。
作者: Vong_HUST
一般情况下,做延时动画有下面常见的两种方式,方式1采用 UIView
提供的带延迟参数的类方法,方式2则是使用 NSObject
的实例方法来延迟执行某个方法。如下图所示,二者都能在3秒之后做一个时长为0.3秒的渐隐动画,那区别在哪呢?
区别就在于:方式1在执行 [self dismissWithDelay:3];
后,self
的 alpha
会马上变成0(但还是可见的),导致点击事件不响应,方式2则可以正常响应。
原因在于 UIView
的动画类方法,只是对 CoreAnimation
的封装,在调用了该方法后,相当于给 self.layer
加了一个 opacity
的 CABasicAnimation
。self
的 modelLayer
的透明度(opacity
)已经被设置成了动画结束时的值(0)(modelLayer
的属性和 view
的对应属性是一致的,比如这里的 modelLayer
的 opacity
和 view
的 alpha
),进而导致无法响应点击事件。presentationLayer
则是动画过程中近似我们实时看到的内容。
所以一般情况下,如果延时动画操作的是 alpha
或者 hidden
属性,建议采用 performSelector:withObject:afterDelay:
的方式,这样可以在延迟时间未到之前还是能够响应对应的交互。
PS:如果想在延时还未到的时候取消,方式1可以采用 [self.layer removeAllAnimations]
,方式2可以采用 [NSObject cancelPreviousPerformRequestsWithTarget:self]
的方式。
更多关于 CoreAnimation
的内容可以查看 动画解释 以及 iOS-Core-Animation-Advanced-Techniques。
// 方式1
- (void)dismissWithDelay:(NSTimeInterval)delay {
[UIView animateWithDuration:0.3
delay:delay
options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowUserInteraction
animations:^{
self.alpha = 0.f;
}
completion:^(BOOL finished) {
}];
}
[self dismissWithDelay:3];
NSLog(@"%f, %f, %f", self.alpha, self.layer.presentationLayer.opacity, self.layer.modelLayer.opacity); // ---> 0.000000, 1.000000, 0.000000
// 方式2
- (void)dismiss {
[UIView animateWithDuration:0.3
delay:0
options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowUserInteraction
animations:^{
self.alpha = 0.f;
}
completion:^(BOOL finished) {
}];
}
[self performSelector:@selector(dismiss) withObject:nil afterDelay:3 inModes:@[NSRunLoopCommonModes]];
NSLog(@"%f, %f, %f", self.alpha, self.layer.presentationLayer.opacity, self.layer.modelLayer.opacity); // ---> 1.000000, 1.000000, 1.000000
作者: KANGZUBIN
在日常开发中,我们通常会在 Debug 开发模式下写很多测试代码,或者引入一些测试专用的 .a
静态库或 .framework
动态库,也会通过 CocoaPods 引入一些第三方测试调试工具等;但我们往往不希望这些测试代码和测试用的库(Library/Framework)在 Release 正式包中被引用或导入,如何做到呢?
.h/.m
文件中的测试代码
Xcode 在 Debug 模式下已经自动帮我们定义了宏 DEBUG=1
,所以我们可以在代码文件中把相关测试代码写在编译预处理命令 #ifdef DEBUG ... #endif
中间即可,如下图所示,这也是我们最常见的一种用法。
- 测试用的
.a
静态库或.framework
动态库
对于通过拖拽的方式直接在工程中添加一些用于测试 .a
或者 .framework
,我们可以在 Targets - Build Settings - Search Paths 中分别设置 Library Search Paths
和 Framework Search Paths
这两个选项,如下图所示(其中 libWeChatSDK.a 放在 WeChatSDK 目录中,而 TencentOpenAPI.framework 放在 QQSDK 目录中,假设它们只在测试时会用到),我们可以移除 Release 模式下测试用的 .a
或 .framework
所在的目录,只在 Debug 下保留,这样在打 Release 包时就不会包含这些库了。(当然在代码中使用到这些测试库的地方也要像上述一样写在 DEBUG 中间)
- CocoaPods 引入的测试库
对于通过 CocoaPods 方式引入的第三方测试库,就很方便了,我们可以配置 configurations
选项让它们只在 Debug 下生效,如下图:
作者: Lefe_x
在以往的小集中已介绍过 iTerm2 和 oh-my-zsh 的使用,如果你还不了解这两个工具,不妨到以往的小集中看看他们的作用,包您满意。而今天介绍另外两个提升终端体验的工具。
如果想在终端查看当前目录的层级结构,不妨了解下 tree,它可以以树状的形式显示当前的目录结构。
安装:
在终端输入:brew install tree
。
使用:
在当前目录下,显示树状目录结构:tree -L 2 -d
。其中 -L 表示遍历的深度,这里为 2;-d 表示只显示目录。更多参数可以使用 man tree
查看。
有时候在 Finder 中的目录,想在终端中直接切换到 Finder 当前显示的目录。使用 Go2Shell 即可,一步到位,非常方便。在官网上 下载,安装,打开 Finder,按住 command 键,拖动 Go2Shell 的图标到 Finder 菜单,在 Finder 的菜单栏中会显示 Go2Shell 图标。下次想在终端显示当前 Finder 的目录,直接点击图标即可。