这里记录了一部分 iOS 开发中的小技巧与常见注意事项
# 让方法只调用一次
在 Objective-C 中我们可以使用 dispatch_once
来让方法只调用一次,而在新的 Swift 版本中这个方法不可用,但我们可以通过 lazy
来做到
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行一次的任务
...
});
2
3
4
5
lazy var runOnce: Void = {
// 这里写需要调用的方法
}()
/// 在需要调用的地方这样使用
_ = runOnce
2
3
4
5
6
# 创建单例
创建单例需要注意无论使用何种初始化方法都能正确的指向单例对象 (当然也可以简单粗暴的使用 NS_UNAVAILABLE
标记来禁止其他初始化方法)。
下面是一种比较安全的单例创建方法
static SingleInstance *single;
// 正常的单例初始化入口
+ (instancetype)sharedInstance{
return [[self alloc] init];
}
- (instancetype)init{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
single = [super init];
});
return single;
}
// 复写该方法确保每次创建对象都只返回同一个内存空间
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
single = [super allocWithZone:zone];
});
return single;
}
// 如果遵循了 NSCopying 协议
- (id)copyWithZone:(NSZone *)zone{
return single;
}
// 如果遵循了 NSMutableCopying 协议
- (id)mutableCopyWithZone:(NSZone *)zone{
return single;
}
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
这样可以确保在调用 init、new、copy、mutableCopy 时依然可以返回正确的单例。
一种比较简单的创建 Swift 单例的方式
class Single: NSObject {
/// 单例
static let shareInstance = Single()
/// 私有化初始化方法确保通过单例来调用
override private init() {
super.init()
}
}
2
3
4
5
6
7
8
9
# 自定义输入视图
我们经常需要在应用中自定义输入视图的样式,系统的 UITextField
往往不能满足我们的需求,尤其是密码输入方面,很多时候我们都需要自己利用 UIView
来绘制输入框。那如何让我们自己的输入视图也能像系统的输入框一样调起键盘输入呢?其实只要让我们的自定义视图实现 UIKeyInput
协议就可以了。同时需要实现下面的方法
// 记录输入的字符串
@property (nonatomic, strong) NSMutableString *password;
#pragma mark - 协议方法实现
// 返回需要开启的键盘样式
- (UIKeyboardType)keyboardType {
return UIKeyboardTypeNumberPad;
}
// 返回当前是否已经输入过文本
- (BOOL)hasText {
return self.password.length > 0;
}
// 用户输入文本的回调
- (void)insertText:(NSString *)text {
[self.password appendString:text];
}
// 用户输入删除的回调
- (void)deleteBackward {
if (self.password.length > 0) {
[self.password deleteCharactersInRange:NSMakeRange(self.password.length - 1, 1)];
[self updateDisplay];
}
}
// 触摸视图时成为第一响应者来调起键盘
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self becomeFirstResponder];
}
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
# 关联对象
关联对象常用来给类扩展属性,使用前必须添加头文件 <objc/runtime.h>
runtime 提供了三个方法来管理关联对象,分别对应了存储、获取、移除
//设置关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)
2
3
4
5
6
其中的参数
- id object:被关联的对象
- const void *key:关联的 key,要求唯一
- id value:关联的对象
- objc_AssociationPolicy policy:内存管理的策略
内存管理策略如下
objc_AssociationPolicy | modifier |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic, strong |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic, copy |
OBJC_ASSOCIATION_RETAIN | atomic, strong |
OBJC_ASSOCIATION_COPY | atomic, copy |
常见用法
// 使用@selector()方法来做key
// @selector()的返回值SEL会被映射为C字符串
- (void)setAssociatedObject:(id)associatedObject{
objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//_cmd在Objective-C的方法中表示当前方法的selector,也就是 @selector(associatedObject)。
- (id)associatedObject{
return objc_getAssociatedObject(self, _cmd);
}
2
3
4
5
6
7
8
9
10
// 使用自定义的C字符串来做key
static char *AssociatedObjectKey = "AssociatedObjectKey";
- (void)setAssociatedObject:(id)associatedObject{
objc_setAssociatedObject(self, AssociatedObjectKey, associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject{
return objc_getAssociatedObject(self, AssociatedObjectKey);
}
2
3
4
5
6
7
8
9
10
# 捕获崩溃信息
要捕获程序运行中的崩溃信息只需要在 didFinishLaunchingWithOptions
方法中使用 NSSetUncaughtExceptionHandler
就可以了。不过有时候我们可能使用了多个第三方的异常处理系统,他们可能使用了同样的方法来捕获异常,这样后注册的将会覆盖掉前面注册的,导致前面注册的异常处理函数不能正常工作。这时候就需要先使用 NSGetUncaughtExceptionHandler
方法来保存其他的异常处理方法,然后我们处理完异常后再调用保存的方法就可以避免覆盖了。
// 记录之前的崩溃回调函数
static NSUncaughtExceptionHandler *otherExceptionHandler = NULL;
// 崩溃时的处理函数
static void ExceptionHandler(NSException * exception) {
// 异常的堆栈信息
NSArray * stackArray = [exception callStackSymbols];
// 出现异常的原因
NSString * reason = [exception reason];
// 异常名称
NSString * name = [exception name];
...
// 这里可以保存或上传日志
...
// 调用之前保存崩溃的回调函数
if (otherExceptionHandler) {
otherExceptionHandler(exception);
}
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 保存其他的异常处理函数
otherExceptionHandler = NSGetUncaughtExceptionHandler();
// 注册我们的异常处理函数
NSSetUncaughtExceptionHandler(&ExceptionHandler);
return YES;
}
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
# 在 Xcode 中使用标记
Xcode 提供了三种实用的简易标记,即 MARK、TODO、FIXME, Xcode 将会在代码中寻找这样的注释,然后以粗体标签的形式将名称显示在导航栏。
//TODO: 标记将来要完成的内容
//MARK: 标记一件事情
//FIXME: 标记以后要修正或完善的内容
2
3
Xcode 还支持???与!!!标记
// ???: 疑问的地方
/// !!!: 需要注意的地方
2
那怎样防止忘记了代码中的这些标记呢,我们可以通过一段 shell 脚本来解决。 在 target-->build phases 中新建 shell 脚本:
TAGS="TODO:|FIXME:|WARNING:"
ERRORTAG="ERROR:"
find "${SRCROOT}" \( -name "*.h" -or -name "*.m" -or -name "*.swift" \) -print0 | xargs -0 egrep --with-filename --line-number --only-matching "($TAGS).*\$|($ERRORTAG).*\$" | perl -p -e "s/($TAGS)/ warning: \$1/"| perl -p -e "s/($ERRORTAG)/ error: \$1/"
2
3
添加脚本后,编译代码,那些标记就会被当做 warning 显示出来。
# Swift 实现协议的可选方法
原生的 Swift protocol 里没有可选项,所有定义的方法都是必须实现的。如果想让某些协议方法变成可选的则有下面几种方法实现
# 使用 @objc
@objc protocol MyProtocol {
// 可选
@objc optional func optionalMethod()
}
2
3
4
当调用可选方法时使用下面的方式
self.delegate?.optionalMethod?()
如果 delegate
或 optionalMethod
不存在时,该方法会返回 nil,这样可以判断方法是否调用成功
if self.delegate?.optionalMethod?() == nil {
// 方法没有调用
}
2
3
# 使用 extension
实现可选方法
给自定义的协议添加 extension, 在 extension 中对可选方法进行默认实现,这样遵守协议的对象就可以不用实现可选方法.
protocol MyProtocol {
func optionalMethod() // 可选
}
extension MyProtocol {
func optionalMethod() {
// 实现可选的方法
}
}
2
3
4
5
6
7
8
9
# 自定义视图在 Storyboard 中预览
我们经常会使用自定义视图来实现一些特殊的 UI 布局,如果同时还使用了 Storyboard 那如何让我们自己的视图可以在 Storyboard 中预览并可以在侧边栏修改自定义的属性呢。
只需要在我们的自定时视图前加上 IB_DESIGNABLE
,在自定义的属性前加上 IBInspectable
就可以了
IB_DESIGNABLE
@interface CustomView : UIView
@property (nonatomic, assign) IBInspectable CGFloat cornerRadius;
@property (nonatomic, strong) IBInspectable UIColor *shadowColor;
@end
2
3
4
5
@IBDesignable class CustomView: UIView {
@IBInspectable var cornerRadius:CGFloat
@IBInspectable var shadowColor:UIColor
}
2
3
4
# Masonry 等间隔排列控件
利用下面的两个 API 可以方便的排列多个视图
/**
* distribute with fixed spacing
*
* @param axisType 横排还是竖排
* @param fixedSpacing 两个控件间隔(横排左右间隔 竖排上下间隔)
* @param leadSpacing 第一个控件与边缘的间隔
* @param tailSpacing 最后一个控件与边缘的间隔
*/
- (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedSpacing:(CGFloat)fixedSpacing leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing;
/**
* distribute with fixed item size
*
* @param axisType 横排还是竖排
* @param fixedItemLength 控件的宽或高(横排设置宽度 竖排设置高度)
* @param leadSpacing 第一个控件与边缘的间隔
* @param tailSpacing 最后一个控件与边缘的间隔
*/
- (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedItemLength:(CGFloat)fixedItemLength leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
如等间隔横向排列 Label 控件
NSMutableArray *gridArr = [[NSMutableArray alloc] init];
for (NSInteger index = 0; index < 4; index++) {
UILabel *label = [[UILabel alloc] init];
label.textAlignment = NSTextAlignmentCenter;
[self addSubview:label];
[gridArr addObject:label];
}
// 横向排列 控件宽度50 等间隔分布
[gridArr mas_distributeViewsAlongAxis:MASAxisTypeHorizontal withFixedItemLength:50 leadSpacing:0 tailSpacing:0];
2
3
4
5
6
7
8
9
10
11
# 优雅的限制输入框字符数量
有时候我们需要对文本框输入的字符数量进行限制,通常是在 textFieldDidChange
或 textViewDidChange
方法中利用字符串的 substringToIndex
方法截取指定长度。这种方法会在输入汉字时将候选时的拼音一起计入长度而出现实际输入字数过少的问题。这时我们可以通过 markedTextRange
来判断。emoji 表情在字符串中是以 2 个长度来处理的当遇到字符串截取时,如果截断位置刚好在 emoji 表情的中间,此时 emoji 表情就会出现无法解码。这时我们可以通过 rangeOfComposedCharacterSequenceAtIndex
方法来获取完整字符串的长度,该方法会将 emoji 表情视作一个连续的字符串,如果 index 处于连续的字符串之间,就会返回这个字符串的 range。
下面的例子限制输入框最多输入 10 个字符
- (void)textFieldDidChange:(UITextField *)textField{
if (textField.text.length > 10) {
if ([textField markedTextRange]) {
// 候选字符不计入长度判断
return;
}
// 防止 emoji 被截断
NSRange range = [textField.text rangeOfComposedCharacterSequenceAtIndex:10];
textField.text = [textField.text substringToIndex:range.location];
}
}
2
3
4
5
6
7
8
9
10
11
# 去掉视图的隐式动画
在刷新 tableView 的 section 时默认会有动画效果,如果我们想要去掉这个隐式动画可以使用
performWithoutAnimation
[UIView performWithoutAnimation:^{
// 需要去掉动画的操作
........
}];
2
3
4
UIView.performWithoutAnimation {
// 需要去掉动画的操作
........
}
2
3
4
上面这种方式无法取消 CALayer
的动画,要想取消 CALayer
的动画需要通过下面的方式
[CATransaction begin];
[CATransaction setDisableActions:YES];
// 需要去掉动画的操作
........
[CATransaction commit];
2
3
4
5
CATransaction.setDisableActions(true)
// 需要去掉动画的操作
........
CATransaction.commit()
2
3
4
未完待续...