iOS战斗总结笔记一期

笔记

分类中定义的属性名最好添加一个前缀

Masonry : mas_xxx

1
2
3
[label mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.view).insets(UIEdgeInsetsMake(20, 20, 20, 20));
}];

SDWebImage : sd_xxx

1
[self.headImgView sd_setImageWithURL:[NSURL URLWithString:DKImgUrl(joinRecord.uphoto)]];

比如我们通常写的view的扩展分类,会定义一些xywidthheight等属性,如果不写上前缀,有可能跟苹果原有的一些属性同名导致冲突,还可能跟其他框架定义的属性同名冲突,比如Masonry里就有widthheight等属性名;

1
2
3
4
5
[label mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
make.height.equalTo(@50);
make.width.equalTo(@50);
}];

正确姿势:

1
2
3
4
5
x >>> dk_x
y >>> dk_y
width >>> dk_width
height >>> dk_height
...

导航栏标题

设置导航栏标题应该用 self.navigationItem.title,而不是用 self.title,self.title 是许多操作的结合体操作,除了更改控制器导航栏标题,还会改了底部 tabBarItem 的标题,甚至还可能使 tabBar 里边的 item 的排列顺序发生变化。

  • 定义宏的时候不能是纯小写字母,苹果的做法是全部大写。
  • 定义的宏如果找不到(按Command+鼠标左键出现一个?),可能是定义在了 Build Setting 中,比如 DEBUG 这个宏。

Nib文字换行

  • 在xib或者storyboard中,UILabel、UITextView等控件的文字换行,需要设置Lines为0,然后按 option + 回车 即可换行。
  • 在代码中可以用\n来换行

AutoLayout

在xib或者storyboard中做约束时,如果要距离顶部或者底部约束,参照的目标不应该选layout Guide,而要选UIView,否则会出现许多奇怪的问题。

IBOutlet

同一个xib中,如果有两个或者多个view,它们的内容几乎一致(只有小部分有差异),那么对于相同的控件可以共用一个暴露在代码中的 IBOutlet,不需要每个控件都拉一条线给一个独有的 IBOutlet。

weak & strong

在写 property 的时候,一般控件的策略是weak,但需要懒加载的控件要用strong。

dispatch_async

如果在 tableView 或者 collectionView 的 reloadData 方法后需要立即获取 cell、高度,或者需要做滚动等操作,那么直接在 reloadData 后执行代码是有可能出问题的。reloadData 并不会等待 tableview 更新结束后才返回,而是立即返回,然后去计算cell高度,获取cell等。

如果表格中的数据非常大,在一个 run loop 周期还没执行完,这时需要 tableview 视图数据的操作就会出问题了。apple并没有直接提供 reloadData 的api,想要程序延迟到 reloadData 结束再操作,可以用以下两种方法:

方法一

1
2
3
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
// 刷新完成

方法二

1
2
3
4
[self.tableView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
// 刷新完成
});

reloadData 会在主队列执行,而 dispatch_get_main_queue 会等待机会,直到主队列空闲才执行。

错误姿势: 把 reloadData 放在 dispatch_async(dispatch_get_main_queue(), ^{}); 里面!

UICollectionView加载不到xib

UICollectionView与UITableView不同,需要在.m文件中重写 initWithFrame 方法,再次加载nib。

1
2
3
4
5
6
7
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
self = [[[NSBundle mainBundle] loadNibNamed:@"XxxCell" owner:nil options:nil] lastObject];
}
return self;
}

图片压缩

图片的压缩其实是两个概念   

  • “压” 是指文件体积变小,但是像素数不变,长宽尺寸不变,那么质量可能下降;
  • “缩” 是指文件的尺寸变小,也就是像素数减少,而长宽尺寸变小,文件体积同样会减小。

对图片只“压”不缩,有时候是达不到我们的需求的。比如图片已经被压得很模糊了但体积还是很大。因此,还要适当地“缩”图片。

图片的“压”处理

1
NSData *imgData = UIImageJPEGRepresentation(image, 0.5);
  • 第一个参数是图片对象。
  • 第二个参数是压的系数,其值范围为0~1。
    压的系数不宜太低,通常是0.3~0.7,过小则会很模糊,甚至可能会出现黑边等。
    压的系数不等于压缩比例,需要慢慢调试到最佳。

或者

1
NSData *imgData = UIImagePNGRepresentation(image);

一般用 UIImagePNGRepresentation 返回的图片比 UIImageJPEGRepresentation 返回的图片更清晰,也就更大。使用 JPEG 的压处理比较可控。

图片的“缩”处理

  • 给 UIImage 写一个分类,定义一个类方法来缩图片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* 压缩图片至目标尺寸
*
* @param sourceImage 源图片
* @param targetWidth 图片最终尺寸的宽
*
* @return 返回按照源图片的宽、高比例压缩至目标宽、高的图片
*/
+ (UIImage *)compressImage:(UIImage *)sourceImage toTargetWidth:(CGFloat)targetWidth
{
CGSize imageSize = sourceImage.size;
CGFloat originWidth = imageSize.width;
CGFloat originHeight = imageSize.height;
CGFloat targetHeight = (targetWidth / originWidth) * originHeight;
UIGraphicsBeginImageContextWithOptions(CGSizeMake(targetWidth, targetHeight), NO, 0.0);
[sourceImage drawInRect:CGRectMake(0, 0, targetWidth, targetHeight)];
UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return scaledImage;
}
  • 把“压”和“缩”都处理后,返回的NSData对象就可以上传到服务器了。
1
2
// 源图片为upImage,指定压缩后的图片宽度为150,压缩系数为0.8
NSData *imageData = UIImageJPEGRepresentation([UIImage compressImage:upImage toTargetWidth:150], 0.8);

动态计算cell高度

动态计算cell的高度,一直贯穿了我的整个iOS开发,从入门的ISA,到正式项目的验房宝,一直到金锄头和一元领宝,下面进入详细报导。


动态计算cell高度

假设现在有一个DKPersonCell,里面有一个模型DKPerson,要计算DKPerson模型的username(NSString)属性的文本高度。

思路1.0 通过字数和字体大小计算文本的高度

给 NSString 写个分类,返回字符串的大小

1
2
3
4
5
6
7
8
9
10
11
/**
* text:需要计算的文本 // @"bingo"
* maxSize: 限制大小 // CGSizeMake(MAXFLOAT, MAXFLOAT) 不限制
* fontSize: 字体大小 // 17
*/
+ (CGSize)sizeWithText:(NSString *)text maxSize:(CGSize)maxSize fontSize:(CGFloat)fontSize
{
CGSize size = [text boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:fontSize]} context:nil].size;
return size;
}

在cell的模型的setter方法中计算字符串的高度

  • 在tableView的数据源方法cellForRowAtIndexPath:中给cell赋值一个模型对象
1
2
3
4
5
6
7
8
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
DKPersonCell * cell = [DKPersonCell personCellWithTableView:tableView];
// 给cell的模型对象赋值
cell.person = self.persons[indexPath.row];
return cell;
}
  • 在 DKPersonCell.h 中定义一个 cellHeight 属性,用来保存cell的高度
1
2
/** cell的高度 */
@property (nonatomic,assign) CGFloat cellHeight;
  • 在 DKPersonCell.m 中重写person的setter方法,计算文本高度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)setPerson:(DKPerson *)person
{
_person = person;
// 参数1:要计算的文本
NSString *text = person.username;
// 参数2:设置最大限制大小
CGFloat limitWidth = [UIScreen mainScreen].bounds.size.width * 0.7;
CGFloat limitHeight = MAXFLOAT;
CGSize maxSize = CGSizeMake(limitWidth, limitHeight);
// 参数3:设置字体大小
CGFloat fontSize = 14.0;
// 计算文本大小
CGSize stringSize = [NSString sizeWithText:text maxSize:maxSize fontSize:fontSize];
// 文本的高度即为 stringSize.height
// 计算cell的高度
// cell的高度要结合实际情况,这里举例的是纯文本情况
// 如果是纯文本的cell,cell的高度就是文本的高度,或者再加上顶部和底部的间距;
// 如果cell有其它控件,就要先计算高度固定部分的高度,然后再加上文本的高度;
self.cellHeight = 20 + stringSize.height;
}

在控制器中定义个可变数组,用来保存模型的高度

1
2
/** cell高度数组 */
@property (nonatomic,strong) NSMutableArray *cellHeightArr;

在 cellForRowAtIndexPath: 中把模型的高度保存到高度数组中

1
2
3
4
5
6
7
8
9
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
DKPersonCell *cell = [DKPersonCell personCellWithTableView:tableView];
cell.person = self.persons[indexPath.row];
// 保存模型的高度到高度数组中
[self.cellHeightArr addObject:@(cell.cellHeight)];
return cell;
}

在 heightForRowAtIndexPath: 中返回对应模型的高度

1
2
3
4
5
6
7
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (self.cellHeightArr.count) {
return [self.cellHeightArr[indexPath.row] doubleValue];
}
return 44; // 默认高度
}

这样就完成了动态计算cell高度的需求,但是这样是有风险的。

因为现在表格的数据源和代理各自对应了一个数组,这两个数组必须实时同步。

如果其中一个数组的元素增加或者减少了,而另外一个数组不变,一旦拽动表格程序就崩溃;如果两个数组在一个run loop周期内没有都更新完毕,程序立马崩溃,或者出现视图刷新时cell的高度不正确的情况。

另外,因为苹果显示文字的时候会有一个换行的算法,根据输入的字符串的类型去智能换行。

比如一行能显示10个汉字,当我输入了8个汉字再开始输入足够多的英文字母的时候,会发现英文的字符串被自动换行到下一行了,而第一行后边还剩下2个汉字的空白区域。那么可以想象如果这种情况出现得比较多,同样的字数和字体大小显示出来的行数要比理论计算得出来的更多。结合这种情况,计算字符串的高度也十分不稳定。

思路2.0 获取UILabel显示的高度

思路1.0不足之处主要有两点:

  • 数据源和代理用了两个数组,会导致许多风险,不可控,应该只用一个数组。
  • 计算字符串的高度是理论值,跟实际值有偏差。

在 xib 或者 storyboard 中把 UILabel 连线到 DKPerson.h

  • 连的是需要显示文本的控件,可以是 UILabel、UITextView 等。
  • 纯代码跳过连线,但要确保.h文件中有暴露控件。

在 DKPerson.h 中定义一个返回模型高度的对象方法,并在 DKPerson.m 中实现

1
2
3
4
5
6
7
8
9
10
11
12
/** 计算cell高度 */
- (CGFloat)cellHeight
{
// 重新布局子控件
[self layoutIfNeeded];
// 计算cell的高度
// cell的高度仍然要结合实际情况,
// 这里简单举例cell高度 = label的底部的y值 + label底部与cell底部的间距
CGFloat cellHeight = CGRectGetMaxY(self.label.frame) + 10;
return cellHeight;
}

在控制器中定义一个DKPersonCell的工具对象,用来保存cell的高度

1
2
/** 计算cell高度的工具对象 */
@property (nonatomic, strong) DKPersonCell *personCellTool;

懒加载personCellTool

1
2
3
4
5
6
7
- (DKPersonCell *)personCellTool
{
if (!_personCellTool) {
_personCellTool = [DKPersonCell personCellWithTableView:self.tableView];
}
return _chatCellTool;
}

在 heightForRowAtIndexPath 方法中计算cell高度

1
2
3
4
5
6
7
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 给计算cell高度的工具对象赋值模型
self.personCellTool.person = self.persons[indexPath.row];
// 计算cell高度
return [self.personCellTool cellHeight];
}

这样就可以完美的实现动态计算cell的高度了~

如果您没有成功,请仔细检查代码,并确定label有设置多行显示,还要约束控件的宽度或者设置explicit来限制单行最长长度。

其它情景

上面演示的是简单地计算与NSString相关的控件高度,实际开发中可能会遇到别的情况。

比如验房宝中的cell要根据房间数来决定,要计算一行能放多少间房,然后计算一共要放多少行,然后再计算总的房间高度,以此来得出cell的高度。

但2.0思路都是通用的:

  • cell暴露一个返回cell高度的方法;
  • 在控制器中懒加载一个计算cell高度的工具对象(cell);
  • 在表格的 heightForRowAtIndexPath 给工具对象set一下对应row的模型,然后调用cell返回高度的方法来得到cell的高度。

唯一不同的就是cell中返回高度的方法实现,具体情况具体分析。


开发中用到的第三方框架

  • 网络请求 : AFNetworking
  • 上拉/下拉刷新 : MJRefresh
  • 字典转模型 : MJExtension
  • 图片浏览器 : MJPhotoBrowser
  • 图片加载 : SDWebImage
  • HUD : SVProgressHUD、MBProgressHUD
  • 数据缓存归档 : YYCache
  • 第三方支付 : Ping++、BeeCloud、Iapppay
  • 第三方登录 : Mob
  • 社交分享 : Mob
  • 键盘处理 : IQKeyboardManager
  • 即时聊天 : EaseMob
  • 录音 : EMCDDeviceManager

第三方框架的坑

AFNetworking

很常见的一个坑:解析 text/html 格式失败

需要在 AFURLResponseSerialization.m 文件的 init 方法的 acceptableContentTypes 中添加@”text/html”

1
2
3
4
5
6
7
8
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
self.acceptableContentTypes = [NSSet setWithObjects: @"application/json", @"text/json", @"text/javascript", @"text/html", nil];
return self;
}

IQKeyboardManager

有导航栏控制器的时候,点击输入框进入编辑状态弹出键盘,导航栏会被顶上去挤出界面。需要在 IQUIView+Hierarchy.m 文件中的 topMostController 方法中,把以下代码段注释掉。

1
2
3
4
5
6
7
8
// while (matchController != nil && [controllersHierarchy containsObject:matchController] == NO)
// {
// do
// {
// matchController = [matchController nextResponder];
//
// } while (matchController != nil && [matchController isKindOfClass:[UIViewController class]] == NO);
// }

如果做了以上修改,可能会有一个 bug,就是 push 后马上给输入框获取焦点,收起键盘后会发现 self.view 向上偏移了64个点,也就是导航栏的高度。

原理是如果不注释这段代码,那么该方法返回的是导航栏控制器,IQKeyBoardManager 会将导航栏控制器的view一起偏移,注释掉后返回的就是 viewController 而不是 NavigationViewController,就不会把导航栏移上去了。

如果真的有刚需进入控制器就进入编辑状态,并且出现了上述的 view 上移的 bug,那 becomeFirstResponder 就不要写在 viewDidLoad 里,应该写在 viewDidAppear 里。

  • 本文作者: Bingo
  • 本文链接: https://blog.bingo.ren/3.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!