记录一些关于学习『百思不得姐』中实用的知识点,以供日后查看。

🙋 1 设置图片的渲染模式

我们在使用 UITabBarController 添加 tabBarItem 的时候,默认文字和图标都会被渲染成 iOS 默认的蓝色,如果我们想要让 tabBarItem 的图标(image)显示本身的颜色,就要将 image 的 UIImageRenderingMode 修改成 UIImageRenderingModeAlwaysOriginal。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@implementation ZLTabBarController
- (void)viewDidLoad {
[super viewDidLoad];
UIViewController *homeVC = [[UIViewController alloc] init];
homeVC.tabBarItem.title = @"主页";
homeVC.tabBarItem.image = [UIImage imageNamed:@"tabbar_home_normal"];
homeVC.tabBarItem.selectedImage = [[UIImage imageNamed:@"tabbar_home_selected"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
[self addChildViewController:homeVC];
UIViewController *discoverVC = [[UIViewController alloc] init];
discoverVC.tabBarItem.title = @"发现";
discoverVC.tabBarItem.image = [[UIImage imageNamed:@"tabbar_discover_normal"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
discoverVC.tabBarItem.selectedImage = [[UIImage imageNamed:@"tabbar_discover_selected"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
[self addChildViewController:discoverVC];
}
@end

还可以直接在 Assets.xcassets 下直接对图片进行修改:

在 Assets.xcassets 下直接对图片进行修改

效果如下:第 1 张是默认效果,第 2、3 张是修改后的效果

被渲染成 iOS 自带的蓝色


小 Tips:如果属性或者方法带有 UI_APPEARANCE_SELECTOR,那么就可以通过 appearance 对象来全局设置,设置后如果需要不一样的话也是可以单独设置的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 比如这个方法
- (void)setTitleTextAttributes:(nullable NSDictionary<NSString *,id> *)attributes forState:(UIControlState)state NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
- (void)viewDidLoad {
[super viewDidLoad];
// 设置全局
[[UITabBarItem appearance] setTitleTextAttributes:@{NSForegroundColorAttributeName: [UIColor grayColor]} forState:UIControlStateNormal];
[[UITabBarItem appearance] setTitleTextAttributes:@{NSForegroundColorAttributeName: [UIColor redColor]} forState:UIControlStateSelected];
// 如果想单独设置也是可以的
UIViewController *homeVC = [[UIViewController alloc] init];
homeVC.tabBarItem.title = @"主页";
homeVC.tabBarItem.image = [UIImage imageNamed:@"tabbar_home_normal"];
homeVC.tabBarItem.selectedImage = [[UIImage imageNamed:@"tabbar_home_selected"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
[self addChildViewController:homeVC];
UIViewController *discoverVC = [[UIViewController alloc] init];
discoverVC.tabBarItem.title = @"发现";
discoverVC.tabBarItem.image = [[UIImage imageNamed:@"tabbar_discover_normal"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
discoverVC.tabBarItem.selectedImage = [[UIImage imageNamed:@"tabbar_discover_selected"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
[self addChildViewController:discoverVC];
}

🙋 2 KVC

在很多时候,我们如果想给一个 readonly 的属性直接赋值是不可以的(比如现在在 ZLTabBarController 中):

self.tabBar 是只读的

这个时候我们就可以使用 KVC(Key-Value Coding),如下:

1
2
// 将 ZLTabBarController(self)的 tabBar 属性赋值为 [[ZLTabBar alloc] init]
[self setValue:[[ZLTabBar alloc] init] forKey:@"tabBar"];

🙋 3 自定义 UITabBar

想自定义成类似于『闲鱼』那样的 tabBar,可以自定义一个 UITabBar,然后重写 layoutSubViews,把『发布』按钮的位置大小写好,把 tabBar 的(子控件)重新布局一遍即可。

🙋 4 Category

在分类中声明 @property,只会生成其方法的声明,不会生成方法的实现和带有下划线的成员变量(语法问题),即:

1
2
3
4
5
6
7
8
#import <UIKit/UIKit.h>
@interface UIView (ZLExtension)
// 这句代码就会生成了『setX: 这个 set 方法』和『x 这个 get 方法』
@property (assign, nonatomic) CGFloat x;
@end

🙋 5 UIBarButtonItem

自定义 leftBarButtonItem、rightBarButtonItem 等

1
2
3
4
5
6
7
8
9
10
11
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *listButton = [[UIButton alloc] init];
[listButton setBackgroundImage:[UIImage imageNamed:@"toolbar_compose_normal"] forState:UIControlStateNormal];
[listButton setBackgroundImage:[UIImage imageNamed:@"toolbar_compose_highlighted"] forState:UIControlStateHighlighted];
// 设置它的大小:等于图片的大小
listButton.size = listButton.currentBackgroundImage.size; // 我这里是用了 UIView+Extension
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:listButton];
}

🙋 6 Log 打印相关

Log 打印一多,也会影响性能,因此在开发阶段可以把 Log 打开,发布之后要把 Log 关掉,可以使用宏来自定义一个 Log(可以写在 pch 文件里):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifdef DEBUG
// 开发调试阶段
#define EVLog(...) NSLog(__VA_ARGS__)
#else
// 已上线阶段
#define EVLog(...)
#endif
NSLog 比 printf 的效率要低几十倍,因为 NSLog 会向 ASL 写 log,同时向 Terminal 写 log,而且同时会出现在 Console.app 中。NSLog 尽量不要在 release 中打开,在 Debug 中可以写一个宏:
#ifdef DEBUG
#define NSLog(FORMAT, ...) do {fprintf(stderr,"%s:%d\t%s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, [[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String]);} while(0)
#else
#define NSSLog(...)
#endif

🙋 7 关于导航条的 title

在有 NavigationBar、TabBar 的情况下,设置 ① 相当于同时设置了 ② 和 ③:

1
2
3
self.title = @"我的"; // ①
self.navigationItem.title = @"我的"; // ②
self.tabBarItem.title = @"我的"; // ③

🙋 8 返回键的恩怨情仇

自定义『返回键』的文字:

1
2
// 在控制器 A -> B 中,A 要在 viewDidLoad 方法中做以下设置:
self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"回退" style:UIBarButtonItemStyleDone target:nil action:nil];

自定义『返回键』的文字

如果需要修改所有的『返回键』的文字,就要自定义 UINavigationController,重写方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)viewDidLoad {
[super viewDidLoad];
// 设置返回按钮的文字和返回箭头的颜色
self.navigationBar.tintColor = [UIColor orangeColor];
}
// 可以在这里拦截所有 push 进来的 viewController
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
// 下一个 viewController 的 navigationItem.backBarButtonItem 都是这样设置
// 由于 rootViewController 也是通过 pushViewController 方法来添加的,所以就是说所有通过 pushViewController 方法来添加的 viewController 设置了以下代码
// 返回的按钮不需要写 target 和 action,内部已经做了处理
viewController.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"回退" style:UIBarButtonItemStyleDone target:nil action:nil];
// 放在后面,可以让个别 ViewController 覆盖这种样式的返回按钮
[super pushViewController:viewController animated:animated];
}

如果想自定义『返回按钮』的样式,比如返回箭头、点击高亮颜色等,就不能够直接修改 backBarButtonItem 了,它因此不支持通过 customView 创建的 UIBarButtonItem,所以可以修改 leftBarButtonItem 来实现需求。

1
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
//
// ZLNavigationController.m
// SearchEverything
//
// Created by angelen on 2016/11/22.
// Copyright © 2016年 ANGELEN. All rights reserved.
//
#import "ZLNavigationController.h"
#import "UIView+ZLExtension.h"
@interface ZLNavigationController ()<UIGestureRecognizerDelegate>
@end
@implementation ZLNavigationController
// 在第一次调用这个 ZLNavigationController 才会调用
+ (void)initialize {
// 可以在这里设置默认属性
// 如果使用 appearance 这个方法就会导致,以后就算使用 UINavigationController 或者 XXNavigationController,它的 NavigationBar 都是这样子的;
// 如果使用 appearanceWhenContainedIn 这个方法就可以做到,ZLNavigationController 的 NavigationBar 是这样子的,其它的可以重新定义,不受影响
[[UINavigationBar appearanceWhenContainedIn:[self class], nil] setBackgroundImage:[UIImage imageNamed:@"navigationbar_background"] forBarMetrics:UIBarMetricsDefault];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// 控制手势什么时候触发,只有非根控制器才需要触发手势
self.interactivePopGestureRecognizer.delegate = self;
}
// 解决左滑返回失败的问题
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
return self.childViewControllers.count > 1;
}
// 可以在这里拦截所有 push 进来的 viewController
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
// rootViewController 已经在里面的时候,count > 0,此时再准备 push 的时候,会先设置返回按钮,然后就再调用系统的 push 方法
if (self.childViewControllers.count > 0) {
// 如果只想在根控制器中显示 TabBar(如果有),可以加上这句即可
// 这样一来,push rootViewController 到时候不会隐藏,push 其他 ViewController 都会隐藏
viewController.hidesBottomBarWhenPushed = YES;
UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom];
[backButton setTitle:@"往返" forState:UIControlStateNormal];
[backButton setTitleColor:[UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:1.00] forState:UIControlStateNormal];
[backButton setTitleColor:[UIColor colorWithRed:0.98 green:0.25 blue:0.22 alpha:1.00] forState:UIControlStateHighlighted];
[backButton setImage:[UIImage imageNamed:@"navigationbar_back_normal"] forState:UIControlStateNormal];
[backButton setImage:[UIImage imageNamed:@"navigationbar_back_highlighted"] forState:UIControlStateHighlighted];
[backButton addTarget:self action:@selector(backAction) forControlEvents:UIControlEventTouchUpInside];
// 设置按钮尺寸,方法 ①:直接设置尺寸
backButton.size = CGSizeMake(120, 44); // UIView+Extension
backButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; // 按钮内容居左显示
// 方法 ②:包裹内容
// [backButton sizeToFit];
// 设置背景色,分析用,到时候去掉即可
// backButton.backgroundColor = [UIColor cyanColor];
// 设置内边距:左边突出 12(不过注意设置按钮尺寸方法不一样会有差距)
backButton.contentEdgeInsets = UIEdgeInsetsMake(0, -12, 0, 0);
viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backButton];
}
// 这一句放在后面是因为:可以让个别不想用这种样式返回按钮的 ViewController,就可以在自己的 viewDidLoad 中写 leftBarButtonItem
[super pushViewController:viewController animated:animated];
}
- (void)backAction {
// 当然也可以将 popViewControllerAnimated: 作为 backButton 的 SEL,但是它需要一个参数而 backButton 没有传递,所以还是用这种比较好
[self popViewControllerAnimated:YES];
}
@end

效果,第一张图为普通状态的返回键,第二张图是高亮状态(点击的时候)状态的返回键:

自定义『返回按钮』普通状态
自定义『返回按钮』高亮状态

🙋 9 关于 Storyboard 中 UILabel 换行显示的问题

在 Storyboard 中编辑 UILabel 的时候,如果想换行,就同时按下 ⌥+⏎ 即可。

在 Storyboard 中 UILabel 的换行输入

🙋 10 UIKeyboard 与 UITextField/UITextView 结合体

1
2
3
4
5
6
7
8
9
10
11
12
13
// inputAccessoryView 可以用来作为类似于键盘上方的一些功能(菜单等),当然也可以是一个 View,注意宽度是满屏
UIBarButtonItem *cameraBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCamera target:nil action:nil];
UIBarButtonItem *flexibleSpaceBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
UIBarButtonItem *undoBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemUndo target:nil action:nil];
UIToolbar *toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, 0, 44)];
toolbar.backgroundColor = [UIColor grayColor];
toolbar.items = @[cameraBarButtonItem, flexibleSpaceBarButtonItem, undoBarButtonItem];
textField.inputAccessoryView = toolbar;
// inputView 可以自定义为 Emoji 键盘或者一些选择日期时间等的 View,注意宽度是满屏
UIView *emojiInputView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 200)];
emojiInputView.backgroundColor = [UIColor orangeColor];
textField.inputView = emojiInputView;

键盘 & UITextField

🙋 11 简单定制 UIButton

图片在上,文字在下:

1
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
34
35
36
37
#import "ZLVerticalButton.h"
@implementation ZLVerticalButton
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupUIElements];
}
return self;
}
- (void)awakeFromNib {
[super awakeFromNib];
[self setupUIElements];
}
- (void)setupUIElements {
// 设置一些基础属性(在外面定义的时候也可以设置)
self.titleLabel.textAlignment = NSTextAlignmentCenter;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.imageView.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame), CGRectGetWidth(self.frame)); // 正方形
self.titleLabel.frame = CGRectMake(0, CGRectGetMaxY(self.imageView.frame), CGRectGetWidth(self.imageView.frame), CGRectGetHeight(self.frame) - CGRectGetHeight(self.imageView.frame)); // 按钮总高度 - 图片高度
}
@end
// 调用:
ZLVerticalButton *verticalButton = [[ZLVerticalButton alloc] initWithFrame:CGRectMake(100, 100, 54, 54 + 32)];
verticalButton.backgroundColor = [UIColor colorWithRed:0.94 green:0.94 blue:0.95 alpha:1.00];
[verticalButton setImage:[UIImage imageNamed:@"tabbar_compose_lbs"] forState:UIControlStateNormal];
[verticalButton setTitle:@"签到" forState:UIControlStateNormal];
[verticalButton setTitleColor:[UIColor orangeColor] forState:UIControlStateNormal];
[self.view addSubview:verticalButton];

简单自定义 UIButton

🙋 12 Runtime 初识

Runtime(运行时)是 Apple 官方的一套 C 语言库,能做很多的底层操作(比如访问隐藏的一些成员变量/成员方法)。以修改 UITextField 的 placeHolder 的颜色为例(只是作为例子,事实上可以直接用设置 attributedPlaceholder 属性):

首先获取 UITextField 所有成员变量:

1
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#import <objc/runtime.h>
- (void)runtimeDemo {
unsigned int ivarCount = 0;
// Describes the instance variables declared by a class.
// 获取成员变量列表(一般指下划线开头的)
// 如果这个指针是指向数组到首元素的话,那么这个指针就可以当数组来用
Ivar *ivars = class_copyIvarList([UITextField class], &ivarCount);
for (int i = 0; i < ivarCount; i++) {
// 取出成员变量
Ivar ivar = *(ivars + i); // 等价于 Ivar ivar = ivars[i];
// 打印成员变量名字
printf("%s\n", ivar_getName(ivar));
}
// 释放 ivars,因此它是通过 copy 创建出来的
free(ivars);
}
// 获取到 UITextField 所有成员变量
_textStorage
_borderStyle
_minimumFontSize
_delegate
_background
_disabledBackground
_clearButtonMode
_leftView
_leftViewMode
_rightView
_rightViewMode
_traits
_nonAtomTraits
_fullFontSize
_padding
_selectionRangeWhenNotEditing
_scrollXOffset
_scrollYOffset
_progress
_clearButton
_clearButtonOffset
_leftViewOffset
_rightViewOffset
_backgroundView
_disabledBackgroundView
_systemBackgroundView
_floatingContentView
_contentBackdropView
_fieldEditorBackgroundView
_fieldEditorEffectView
_displayLabel
_placeholderLabel
_suffixLabel
_prefixLabel
_iconView
_label
_labelOffset
_overriddenPlaceholder
_overriddenPlaceholderAlignment
_interactionAssistant
_selectGestureRecognizer
_inputView
_inputAccessoryView
_systemInputViewController
_atomBackgroundView
_textFieldFlags
_deferringBecomeFirstResponder
_avoidBecomeFirstResponder
_setSelectionRangeAfterFieldEditorIsAttached
_animateNextHighlightChange
_cuiCatalog
_cuiStyleEffectConfiguration
_roundedRectBackgroundCornerRadius
_adjustsFontForContentSizeCategory
_tvUseVibrancy
_disableTextColorUpdateOnTraitCollectionChange
_baselineLayoutConstraint
_baselineLayoutLabel
_tvCustomTextColor
_tvCustomFocusedTextColor

通过猜测,觉得 _placeholderLabel 就是显示 placeHolder 的 UILabel,此时我们可以直接设置:

1
2
3
4
5
6
7
8
9
10
11
12
UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 200, 44)];
textField.center = self.view.center;
textField.placeholder = @"请输入手机号";
// 直接使用取出这个 label 进行设置
UILabel *placeholderLabel = [textField valueForKeyPath:@"_placeholderLabel"];
placeholderLabel.textColor = [UIColor redColor];
// 注意,如果没有设置 textField.placeholder,这一行没有卵用,如果设置了,才会生效,估计是 Apple 有做判断吧
// placeholderLabel.text = @"我就是 PlaceHolder";
// 或者直接一点
[textField setValue:[UIColor orangeColor] forKeyPath:@"_placeholderLabel.textColor"];

所以说,使用 KVC 来修改 UITextField 的 PlaceHolder 颜色、字体大小等等(注意要先设置了 PlaceHolderText 后才可以修改其颜色、字体大小等,因为内部有可能是懒加载,如果一开始就设置,会拿不到这个 PlaceHolderLabel)。

同理,如果想修改 UITextField 的 clearButton(在编辑时候那个 x 按钮),也可以用 Runtime -> KVC 轻松搞定。

还有,对于这个 UITextField,有个小 Tips:如果想要在 textField 的 placeHolder 成为第一响应者和失去第一响应者的时候颜色不一样,就可以自定义一个 UITextField,然后重写以下两个方法:

1
2
3
4
5
6
7
8
9
- (BOOL)becomeFirstResponder {
// 修改颜色
return [super becomeFirstResponder];
}
- (BOOL)resignFirstResponder {
// 修改颜色
return [super resignFirstResponder];
}

对 Runtime 的一些补充:

通过 Runtime 除了可以获取成员变量,还可以获取属性(@property)、方法以及协议。

class_copy...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 获取属性列表
- (void)getPropertyList {
unsigned int propertyCount = 0;
objc_property_t *properties = class_copyPropertyList([UILabel class], &propertyCount);
for (int i = 0; i < propertyCount; i++) {
objc_property_t property = properties[i];
// 获取属性名称以及返回值(或者这些对写 JSON -> Model 有帮助)
printf("%s -> %s\n", property_getName(property), property_getAttributes(property));
}
free(properties); // 释放(because of 'copy')
}
// 获取方法列表
- (void)getMethodList {
unsigned int methodCount = 0;
Method *methods = class_copyMethodList([UILabel class], &methodCount);
for (int i = 0; i < methodCount; i++) {
Method method = methods[i];
// 获取方法名称(可以通过其他方法获取返回值等)
SEL selector = method_getName(method);
NSLog(@"%@", NSStringFromSelector(selector));
}
free(methods); // 释放(because of 'copy')
}

小 Tips:

1
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
34
35
36
37
38
39
40
41
42
43
44
// .h 文件
#import <Foundation/Foundation.h>
@interface ZLRuntimeTest : NSObject
@property (strong, nonatomic) NSString *name;
@property (assign, nonatomic) NSInteger age;
- (void)fuckMe;
@end
// .m 文件
#import "ZLRuntimeTest.h"
@interface ZLRuntimeTest() {
NSInteger _number;
NSString *_region;
}
@property (strong, nonatomic) NSString *gender;
@end
@implementation ZLRuntimeTest
- (void)fuckMe {
}
@end
// class_copyIvarList 拿到的是:
_number
_region
_name
_age
_gender
// class_copyPropertyList 拿到的是:
gender
name
age

再来一个实用的,获取数组所有元素的某个属性值:

1
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
User *user1 = [[User alloc] init];
user1.id = 1;
user1.name = @"Yiyi";
User *user2 = [[User alloc] init];
user2.id = 2;
user2.name = @"Cici";
User *user3 = [[User alloc] init];
user3.id = 3;
user3.name = @"Fifi";
NSArray *users = @[user1, user2, user3];
// name 属性数组
NSArray *names = [users valueForKeyPath:@"name"];
NSLog(@"names -> %@", names);
/** Log
names -> (
Yiyi,
Cici,
Fifi
)
*/