Skip to content

Latest commit

 

History

History
304 lines (197 loc) · 14.6 KB

06.md

File metadata and controls

304 lines (197 loc) · 14.6 KB

2018.06

再谈 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;但如果我们要按字节来统计和限制,同一字符在不同编码下所占的字节数也是不同的;另外有时我们要统计的是所输入文本的单词个数,而不是字符串的长度,所以我们需要根据不同的使用场景进行分析。

iOS 处理 navigationBar.titleTextAttributes 属性时机

作者: 无彦主

笔者一般在 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)
    }

    ...
}

这里简单展示处理思路,具体代码还是要根据项目需求定制。

Demo

参考

推荐几个高频使用的 Xcode 小技巧

作者: halohily

今天是节后上班第一天,也是 WWDC 期间知识小集微博话题暂停之后回归的第一天。欢迎关注本次 WWDC 期间 知识小集 和 老司机、SwiftGG 联合完成的掘金免费专题。

今天小集给大家推荐几个实用的 Xcode 小技巧。

  1. 快速打开: Command + Shift + O。这个命令可以开启一个小窗格用来快速搜索浏览文件、类、算法以及函数等,且支持模糊搜索。这个命令可以说是我日常开发中最高频使用的一个了。
  2. 显示项目导航器: Command + Shift + J。使用快速打开命令跳转到对应文件后,如果需要在左侧显示出该文件在项目中的目录结构,只需要键入这个命令,非常方便。
  3. 显示编辑历史。如果发现一行需要膜拜或者需要吐槽的代码,不需要跑到专门的 diff 工具查看代码历史。在该代码行处右键,选择 Show Blame for Line,即可弹出一个小标签,显示该处代码的修改作者、commit 记录等信息。

延时动画的两种方式对比

作者: Vong_HUST

一般情况下,做延时动画有下面常见的两种方式,方式1采用 UIView 提供的带延迟参数的类方法,方式2则是使用 NSObject 的实例方法来延迟执行某个方法。如下图所示,二者都能在3秒之后做一个时长为0.3秒的渐隐动画,那区别在哪呢?

区别就在于:方式1在执行 [self dismissWithDelay:3]; 后,selfalpha 会马上变成0(但还是可见的),导致点击事件不响应,方式2则可以正常响应。

原因在于 UIView 的动画类方法,只是对 CoreAnimation 的封装,在调用了该方法后,相当于给 self.layer 加了一个 opacityCABasicAnimationselfmodelLayer 的透明度(opacity)已经被设置成了动画结束时的值(0)(modelLayer 的属性和 view 的对应属性是一致的,比如这里的 modelLayeropacityviewalpha),进而导致无法响应点击事件。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

Xcode 设置代码只在 Debug 下起效的几种方式

作者: 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 PathsFramework 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

如果想在终端查看当前目录的层级结构,不妨了解下 tree,它可以以树状的形式显示当前的目录结构。

安装: 在终端输入:brew install tree

使用: 在当前目录下,显示树状目录结构:tree -L 2 -d 。其中 -L 表示遍历的深度,这里为 2;-d 表示只显示目录。更多参数可以使用 man tree 查看。

Go2Shell

有时候在 Finder 中的目录,想在终端中直接切换到 Finder 当前显示的目录。使用 Go2Shell 即可,一步到位,非常方便。在官网上 下载,安装,打开 Finder,按住 command 键,拖动 Go2Shell 的图标到 Finder 菜单,在 Finder 的菜单栏中会显示 Go2Shell 图标。下次想在终端显示当前 Finder 的目录,直接点击图标即可。