目录:
- Autolayout
- anchorPoint与masonry
- UI相关的小姿势
- 布局的两三事
- presentVC iOS13适配
-
deselectItemAtIndexPath
与setSelected
的区别 - Container View Controller官方指南
- table reload
1. Autolayout
我们新的lead大哥哥说只要掌握masonry和snapkit(swift的)就好啦,并推荐了一篇文章就拿来学习了:https://www.raywenderlich.com/811496-auto-layout-tutorial-in-ios-getting-started
At first, Apple made one screen size for the iPhone. Developers didn’t have to create flexible interfaces as they only had to fit that one size. Today, differently sized devices and more emphasis on landscape mode demand user interfaces of different sizes. Auto Layout is Apple’s solution to this problem, enabling UI elements to grow, shrink and move depending on screen size.
虽然xib的解析是耗费时间的,所以我们推荐用代码布局,但是这里因为是参考原文,所以都是xib啦~
从iOS7以后就就有了safe area的参照了,所以你看当我们加约束的时候,顶部不会覆盖状态栏。
如果你规定了一个view到四周的边距,以及它的宽高,然后当你切换横屏的时候可能就会crash,提示下面酱紫:
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this: (1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(...)
Will attempt to recover by breaking constraint....
这就是constraints太多的样例,因为不是每个屏幕宽高都一样的,所以不能这么干,你只要去除两个方向的constraint就可以啦,例如trailing和bottom,只保留leading top width height。
Content Priority Ambiguity
当上面那样布局可能会遇到Content Priority Ambiguity的error,怎么理解这个error呢?比如现在我们固定container的高是200,图片和label的高都自适应;然后如果container的高变为300了呢,要怎么分配这多出来的100?都给图片,还是都给label,还是他俩五五分、四六分?
如果你不指定,那么Auto Layout会去猜,那么结果就不一定了。
解决方案就是Content Hugging Priority,
You can imagine hugging here to mean size-to-fit. In other words, the bounds of the view will hug the intrinsic content size. A higher value here means the view will be less likely to grow and more likely to stay the same.
也就是这个值越大,它约包裹内容即可,高度不容易变大。
如果设置label的Content Hugging Priority高于image,那么当container高度变大的时候,label会保持高度,image会变高。
他还有个兄弟我们看下~
Content Hugging Priority(控件抗拉伸优先级)
:优先级越高越不容易被拉伸,默认是250。
Content Compression Resistance Priority(控件抗压缩优先级)
:和拉伸一样,优先级越高的越不容易和压缩,默认是750。
实例可以参考:https://www.cnblogs.com/losedMemory/p/5844230.html?utm_source=itdadao&utm_medium=referral
2. anchorPoint与masonry
我们都知道,anchorPoint + position + bounds可以影响frame。那么如果用masonry布局的情况下更改anchorPoint会怎么样呢??
UIView *bgView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
bgView.backgroundColor = [UIColor grayColor];
[self.view addSubview:bgView];
// bgView.layer.anchorPoint = CGPointMake(0, 0.5);
[bgView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top);
make.leading.equalTo(self.view);
make.width.height.equalTo(@200);
}];
显示是酱紫的:
现在放开注释的anchorPoint的设定会变成酱紫:
如果在改了anchorPoint以后再updateConstraint呢?
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[bgView mas_updateConstraints:^(MASConstraintMaker *make) {
make.width.height.equalTo(@400);
make.top.equalTo(self.view.mas_top);
make.leading.equalTo(self.view);
}];
});
会从上面改anchorPoint的样子变成酱紫:
也就是说,如果我们改了anchorPoint,我们预期的让leading和self.view对齐,但是它并不会这样显示了。
我感觉它的计算逻辑是,假设anchorPoint在(0.5, 0.5)中间的地方的时候,想让这个bgView
满足我们的约束,那么它的position,也就是中心点anchorPoint的坐标也就是(100, 100), 并且宽高都是200,然后现在让anchorPoint的绝对坐标不动,但是改为(0, 0.5),那么此时,bgView
的frame就变为了(100, 0, 200, 200)。
所以大家如果用masonry并且还改了anchorPoint的时候要注意吖~
如果我们换成frame会怎样呢?
UIView *bgView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
bgView.backgroundColor = [UIColor grayColor];
[self.view addSubview:bgView];
bgView.layer.anchorPoint = CGPointMake(0, 0.5);
bgView.frame = CGRectMake(0, 0, 200, 200);
现在换一下顺序,先设置frame再改anchorPoint:
UIView *bgView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
bgView.backgroundColor = [UIColor grayColor];
[self.view addSubview:bgView];
bgView.frame = CGRectMake(0, 0, 200, 200);
bgView.layer.anchorPoint = CGPointMake(0, 0.5);
所以如果直接写死frame的话,anchorPoint是多少并不影响我们的frame,我们规定要在什么位置就会显示在什么位置;但是如果你规定了frame以后再改anchorPoint,那么view就会动了,因为其实frame是由position和bounds算出来的,改anchorPoint相当于position的绝对坐标没有变,但相对坐标变了,它不再是view的中心点了,所以会重新算出来一个frame。
3. UI相关的小姿势
- 动画 CoreAnimation 与 UIView.animate 这两个方式的主要差别在于,CoreAnimation如果不主动设置,那么在动画做完以后,会恢复原状。UIView.animate则不会,动画做完后是什么样,控件就是什么样。
参考:https://blog.csdn.net/github_32300233/article/details/79136250
layoutSubviews啥时候被触发:
1.初始化不会触发layoutSubviews,但是如果设置了不为CGRectZero的frame的时候就会触发。
2.addSubview会触发layoutSubviews
3.设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化
4.滚动一个UIScrollView会触发layoutSubviews
5.旋转Screen会触发父UIView上的layoutSubviews事件
6.改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件layoutIfNeeded
会强制马上更新布局和显示,由于该方法是同步的,视图框架的移动会被动画模块捕获,而setNeedsLayout
只是标记应该重绘,所以动画需要用layoutIfNeeded
:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var buttonHeight: NSLayoutConstraint!
@IBAction func heightPressed(sender: AnyObject) {
view.layoutIfNeeded()
if(self.buttonHeight.constant == 25.0) {
self.buttonHeight.constant = self.view.bounds.height - 200.0
} else {
self.buttonHeight.constant = 25.0
}
UIView.animateWithDuration(2.0) {
// 有效动画
self.view.layoutIfNeeded()
// 无效动画
//self.view.setNeedsLayout()
}
}
}
4. 布局的两三事
首先是一个关于布局要怎么写的探讨,前几天小哥哥和我说我们要不把所有masonry换成xib吧,于是我产生了这个问题:用啥写布局?
用xib写布局:冲突解的时候a little痛苦,虽然小哥哥说如果把控件拆的足够小其实冲突的几率比较小,毕竟一个小view很少俩人改。那么就会带来新的问题,文件过多读写导致慢。
masonry以及constraint (autolayout):我们缩写的约束几乎都是二元方程的感觉,就是类似
x.leading = y.leading + 10
,一两个还好其实,如果整个布局有超级多的这样的式子,系统在计算的时候也是非常痛苦并且耗时的,所以复杂布局的解方程需要慎重哦,用autolayout也不是一个最好的性能选择frame:于是性能最好的竟然是写死frame,直接乘一个屏幕放大系数适配即可。但是哦,小哥哥说无论怎样autolayout之类的我们还是要用的,不能一直用最原始的,毕竟苹果总是在改善它的性能的。所以用啥就大家看心情好啦~
如果没有make过的constraint是可以直接remake的~ 如果多次make也是可以的~ 不可以的是update之前没make过。
上周CR的时候小哥哥说如果子view撑起父view的写法会有masonry警告,因为父view的宽高是ambiguous的,我就试了一下:
UIView *bgView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
bgView.backgroundColor = [UIColor grayColor];
[self.view addSubview:bgView];
[bgView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top);
make.leading.equalTo(self.view);
}];
UIView *sub = [[UIView alloc] init];
[bgView addSubview:sub];
sub.backgroundColor = UIColor.blueColor;
[sub mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(bgView);
make.width.height.equalTo(@200);
}];
但不知道为啥我这边木有警告,但是还是建议大家把宽高都写好吧防止出现各种布局问题
- 可以用
systemLayoutSizeFittingSize
得到子view应该的size,然后设置给父view
CGSize size = [sub systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
[bgView mas_updateConstraints:^(MASConstraintMaker *make) {
make.size.equalTo(@(size));
}];
UILayoutFittingCompressedSize: 返回合适的最小大小。
UILayoutFittingExpandedSize: 返回合适的最大大小。
- cell里有infinite的动画会不会dealloc呢?答案是会的~
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0];
rotationAnimation.duration = 0.6;
rotationAnimation.repeatCount = INFINITY;
rotationAnimation.removedOnCompletion = YES;
[self.contentLabel.layer addAnimation:rotationAnimation forKey:@"rotationAnimation"];
- (void)dealloc {
NSLog(@"Table1ViewCell dealloc");
}
然后这个页面退出的时候,打印了一堆dealloc~ 所以虽然给cell里面的label加了无限循环动画,但是cell还是会正常dealloc的啦,连label也会被释放,本来还以为layer不会释放结果layer也释放了非常的神奇,只是习惯性还是会remove一下啦。一个附加姿势点:占内存最大的就是layer,view本身很小哒
5. presentVC iOS13适配
参考:https://www.cnblogs.com/guoshaobin/p/11167191.html
其实就是presentViewController现在默认是一个叠在现有页面的感觉,iOS13改了好多0.0,如果想让他恢复全屏就酱紫:
ViewController *vc = [[ViewController alloc] init];
vc.title = @"presentVC";
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
nav.modalPresentationStyle = UIModalPresentationFullScreen;
[self.window.rootViewController presentViewController:nav animated:YES completion:nil];
Refer: https://blog.csdn.net/heqiang2015/article/details/84575047
6. deselectItemAtIndexPath
与setSelected
的区别
先从选中说起吧,让UITableView有选中效果大家都很熟悉了,如果不用特殊的效果,直接设置cell
的selectionStyle
即可,系统提供这几个选项:
typedef NS_ENUM(NSInteger, UITableViewCellSelectionStyle) {
UITableViewCellSelectionStyleNone,
UITableViewCellSelectionStyleBlue,
UITableViewCellSelectionStyleGray,
UITableViewCellSelectionStyleDefault API_AVAILABLE(ios(7.0))
};
我们来尝试一下UITableViewCellSelectionStyleGray
:
self.backgroundColor = UIColor.yellowColor;
self.selectionStyle = UITableViewCellSelectionStyleGray;
这里我们把cell的背景色设为了黄色为了可以看的清楚一点,默认是黑色,所以平时一般会设为clearColor,可以看出来其实系统默认的selection就是在cell的最底层,但是还是改在cell自己的背景色上面,加了一层背景图层,位于contentView下面哈,所以如果你给contentView加了深色背景,上面的selection就看不出来了哦。
然后如果你点任意其他一行,会发现当前选中的行被deselect了,然后被选的新行变灰色了;如果你仍旧点击选中行,该行会再次触发select而不会deselect哦,这是因为我们tableview是单行选中的,最少需要有一行被选中:
如果你改为multiple selection
会发现你选中几行都可以,并且点击选中行会反选也就是消除选中效果,你还可以反选掉所有不保留任何一行被选中。
如果你改为no selection
那么点击就木有任何效果也不会触发select的回调了哈~~~
悲伤的是,日常我们从来没用过系统提供的选中效果,所以当我们需要自定义选中的时候我们经常这么干:
================= cell =================
self.selectionStyle = UITableViewCellSelectionStyleNone;
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
if (selected) {
self.contentLabel.textColor = [UIColor redColor];
} else {
self.contentLabel.textColor = [UIColor yellowColor];
}
NSLog(@"TableView Cell:%@ setSelected:%@ animated:%@", self, @(selected), @(animated));
}
- (void)setSelected:(BOOL)selected {
[super setSelected:selected];
NSLog(@"TableView Cell:%@ setSelected:%@", self, @(selected));
}
================= VC =================
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
Table1ViewCell *cell = (Table1ViewCell *)[tableView cellForRowAtIndexPath:indexPath];
NSLog(@"TableView didSelectRowAtIndexPath:%@ cell:%@", indexPath, cell);
}
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
Table1ViewCell *cell = (Table1ViewCell *)[tableView cellForRowAtIndexPath:indexPath];
NSLog(@"TableView didDeselectRowAtIndexPath:%@ cell:%@", indexPath, cell);
}
选中log:
// 第一次点击的log
2020-06-13 22:23:31.039702+0800 Example1[29407:5870373] TableView Cell:> setSelected:1 animated:0
2020-06-13 22:23:31.040744+0800 Example1[29407:5870373] TableView didSelectRowAtIndexPath: {length = 2, path = 0 - 5} cell:>
// 第二次点击的log
2020-06-13 22:26:36.514611+0800 Example1[29407:5870373] TableView Cell:> setSelected:0 animated:0
2020-06-13 22:26:36.515596+0800 Example1[29407:5870373] TableView didDeselectRowAtIndexPath: {length = 2, path = 0 - 5} cell:>
2020-06-13 22:26:36.516606+0800 Example1[29407:5870373] TableView Cell:> setSelected:1 animated:0
2020-06-13 22:26:36.517665+0800 Example1[29407:5870373] TableView didSelectRowAtIndexPath: {length = 2, path = 0 - 1} cell:>
但你有木有发现如果我们手动点击tableview触发的只会是
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
,除非代码写cell.selected = YES
才会触发- (void)setSelected:(BOOL)selected
并且单行选中的话会先反选上次选中的,再选中新的。同时会先触发
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
,再触发tableview的delegate回调
其实上面的part已经够日常自定义选中样式了,但是有的时候比较特殊我们需要手动控制selected呢?那么你可能就会写下面的代码:
// cell
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
if (selected) {
self.contentLabel.textColor = [UIColor redColor];
} else {
self.contentLabel.textColor = [UIColor yellowColor];
}
NSLog(@"TableView Cell:%@ setSelected:%@ animated:%@", self, @(selected), @(animated));
}
// VC
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellID = @"Table1ViewCell";
Table1ViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if (cell == nil) {
cell = [[Table1ViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
}
cell.contentLabel.text = self.objects[indexPath.row];
[cell setSelected:YES animated:NO];
return cell;
}
其实和上面的代码没啥区别只是加了一行[cell setSelected:YES animated:NO];
,并且将table改成了多行选中,那么我们期望的就是一打开就是一个全是红色字体的table,然鹅,事实是还都是黄色的。
如果你把[cell setSelected:YES animated:NO];
换成[tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
就可以让所有cell一开始就是选中的了。
但这个是为啥呢,我看了下log大概是酱紫:
2020-06-14 16:09:55.009479+0800 Example1[30830:6132866] TableView Cell:> setSelected:1 animated:0
2020-06-14 16:09:55.009546+0800 Example1[30830:6132866] TableView Cell:> setSelected:0 animated:0
2020-06-14 16:09:55.009889+0800 Example1[30830:6132866] TableView Cell:> setSelected:0
2020-06-14 16:09:56.089068+0800 Example1[30830:6132866] TableView Cell:> setSelected:0 animated:0
也就是虽然第一次的确是让它selected了,但是系统自动给我们反选了好几次之后,也就是系统并没有认识到,你的cell是选中状态,为了让它知道,你最好调用selectRowAtIndexPath
当我们成功让table每行都是选中状态以后再任意点击一行,则会触发setSelected:0 animated:0
以及didDeselectRowAtIndexPath
。
也就是说我们要尽量使用
selectRowAtIndexPath
以及deselectRowAtIndexPath
,不要自己调用cell.selected = YES
或者[cell setSelected:YES animated:NO]
,可能会导致选中状态无法清除 or 不能选中等问题。
可参考:https://www.cnblogs.com/AntonyGu/p/5386465.html
7. Container View Controller官方指南
大佬发给我让我看一下的:https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html
这篇主要是讲如何实现UINavigationController, UITabBarController, and UISplitViewController
这种的VC的container的~~ 和普通VC类似的是,container也是管理它的root view以及它的content,区别只是The difference is that a container view controller gets part of its content from other view controllers. container VC的内容是由它的子VC提供的(但是你也可以加一些container自己的例如导航栏),container会负责把子VC的位置和大小设置好,但是子VC里面的内容仍旧是由他们自己管理的。
当设计VC container的时候可以考虑下面的问题
* What is the role of the container and what role do its children play?
* How many children are displayed simultaneously?
* What is the relationship (if any) between sibling view controllers?
* How are child view controllers added to or removed from the container?
* Can the size or position of the children change? Under what conditions do those changes occur?
* Does the container provide any decorative or navigation-related views of its own?
* What kind of communication is required between the container and its children? Does the container need to report specific events to its children other than the standard ones defined by the `[UIViewController](https://developer.apple.com/documentation/uikit/uiviewcontroller)` class?
* Can the appearance of the container be configured in different ways? If so, how?
只要你建立了
parent-child
关系,子VC们就可以正常的获取relevant system messages
啦,例如viewDidAppear啥的~
=> 如何建立父子VC的关系
通过下面的代码可以建立parent-child relationship:
Call the
[addChildViewController:]
of your container view controller.
This method tells UIKit that your container view controller is now managing the view of the child view controller.Add the child’s root view to your container’s view hierarchy.
Always remember to set the size and position of the child’s frame as part of this process.Add any constraints for managing the size and position of the child’s root view.
Call the
[didMoveToParentViewController:](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621405-didmove)
method of the child view controller.
例如:
- (void) displayContentController: (UIViewController*) content {
[self addChildViewController:content];
content.view.frame = [self frameForContentController];
[self.view addSubview:self.currentClientView];
[content didMoveToParentViewController:self];
}
你需要主动调用didMoveToParentViewController:
因为addChildViewController:
会自动调用子VC的willMoveToParentViewController:
. 但didMoveToParentViewController:
必须在你把child VC的view放进container的view以后再调用,所以不能自动给你调必须手动。
如果你用Auto Layout加constraint,在把child的root view加入到父VC的view以后设置child的rootview的constraints,你的约束只应该影响the size and position of only the child’s root view
,不要改变父VC的rootview里面content里面其他的view。
=> remove子VC
如果这个VC仍旧在栈里面,例如navgication VC push一个新的VC,旧的不应该被remove,只有要pop这个旧VC的时候才应该彻底remove哈~
Call the child’s
[willMoveToParentViewController:](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621381-willmovetoparentviewcontroller)
method with the valuenil
.Remove any constraints that you configured with the child’s root view.
Remove the child’s root view from your container’s view hierarchy.
Call the child’s
[removeFromParentViewController]
method to finalize the end of the parent-child relationship.
removeFromParentViewController
会自动帮你调用didMoveToParentViewController
并传入nil的~
例如:
- (void) hideContentController: (UIViewController*) content {
[content willMoveToParentViewController:nil];
[content.view removeFromSuperview];
[content removeFromParentViewController];
}
=> 过渡动画
如果你想做动画,需要让旧的知道它即将被替换,[transitionFromViewController:toViewController:duration:options:animations:completion:]
会自动更新view层级,你就不用手动add或者remove啦~
例如:
- (void)cycleFromViewController: (UIViewController*) oldVC
toViewController: (UIViewController*) newVC {
// Prepare the two view controllers for the change.
[oldVC willMoveToParentViewController:nil];
[self addChildViewController:newVC];
// Get the start frame of the new view controller and the end frame
// for the old view controller. Both rectangles are offscreen.
newVC.view.frame = [self newViewStartFrame];
CGRect endFrame = [self oldViewEndFrame];
// Queue up the transition animation.
[self transitionFromViewController: oldVC toViewController: newVC
duration: 0.25 options:0
animations:^{
// Animate the views to their final positions.
newVC.view.frame = oldVC.view.frame;
oldVC.view.frame = endFrame;
}
completion:^(BOOL finished) {
// Remove the old view controller and send the final
// notification to the new view controller.
[oldVC removeFromParentViewController];
[newVC didMoveToParentViewController:self];
}];
}
=> 手动管理Appearance callbacks
//该方法返回NO则childViewController不会自动viewWillAppear和viewWillDisappear对应的方法
- (BOOL)shouldAutomaticallyForwardAppearanceMethods
{
return NO;
}
//viewWillAppear调用设置为YES,viewWillDisappear调用设置为NO
[self.customChildViewController beginAppearanceTransition:YES animated:animated];
//对应的DidAppear调用需要成对出现
[self.customChildViewController endAppearanceTransition];
=> Suggestions
Access only the root view of a child view controller. container只应该可以碰到child的root view而不应该直接操作子VC里面的其他view。
Child view controllers should have minimal knowledge of their container. 子VC应该尽可能少的依赖/了解父VC,如果真的需要可以用delegate来让父子VC之间通信。
Design your container using regular views first. 可以先把view放进container调试好,然后再抽成child VC。
8. table reload
这一点是我们之前有一个table的刷新用的是insertAtIndexPath以及replaceIndexPath的局部更新,不会reload整个table,但是这样真的很容易不好管理的,想定时merge之前的刷新统一refresh一次就更不行了。
于是最好table view刷新reload整个table,防止由于只动一部分indexpath造成crash,另外其实reload并没有reload整个table,他只是把你视线之内的刷新啦,所以不要担心会很耗性能。