【UI】基础知识

目录
一、常用控件及其注意点
 1、UIView的frame、bounds、center属性
 2、UILabel的AutoLayout
 3、UIImageView
 4、UIButton的imageView和titleLabel的布局样式
 5、UITextField
 6、UIScrollView
 7、UITableView
 8、UICollectionView
二、UIView和UIViewController的一些方法
 1、UIView的一些方法
 2、UIViewController的一些方法


一、常用控件的注意点


1、UIView的frame、bounds、center属性

  • UIView的frame属性包含了该view的位置和大小信息,其中位置是相对于父视图的左上角来说的
  • UIView的bounds属性只包含了该view的大小信息,其中位置是相对于自己的左上角来说的、所以总是(0, 0)、没什么意义(如果说得更深一点,frame其实是一个view的可视范围,而bounds其实是一个view的内容区域,就像UIScrollView有一个自己本身的大小、还有一个可滚动区域的大小那样,而UIScrollView的底层实现其实就是通过一个平移手势来不断改变bounds达到滚动效果的)
  • UIView的center属性也是相对于父视图的左上角来说的,而不是某个视图的绝对中心点,所以我们不能设置子视图.center = 父视图.center,这在很多情况下将得到错误的布局,正确的设置方法是子视图.center = 父视图宽高的一半

2、UILabel的AutoLayout

通常情况下,一个视图需要4个约束来决定它的大小和位置,而UILabel的AutoLayout则只需要3个约束就行了:

  • UILabel自适应宽度,只需要设置上左高度三个约束
  • UILabel自适应高度,只需要设置上左宽度三个约束,并设置numberOfLines为0即可

3、UIImageView相关

往项目里拖图片时,该勾选什么
  • Copy items if needed:是否复制一份图片到我们的项目中,推荐勾选。如果一张图片在我们的桌面上,我们拖到项目里的时候没有勾选这一项,那么我们的项目里就没有这张图片,合作开发时在别人的电脑上运行这个项目就加载不到这张图片了
  • Create groups:黄色文件夹、虚拟文件夹,按需勾选、通常就用这个。黄色文件夹其实都是虚拟文件夹,也就是说我们项目里的黄色文件夹其实在最终打成的ipa包里是不存在的,黄色文件夹里所有的东西——包括源代码文件、资源文件等都是直接存放在bundle里的,这也是为什么我们项目里明明有好几层黄色文件夹,读取资源的时候却只需要直接[[NSBundle mainbundle] pathForResources:直接写资源名 ofType:nil]就能读取到资源,而不是写资源名的全路径,就是因为资源在ipa包里是直接存放在bundle下的
  • Create folder references:蓝色文件夹、真实文件夹,按需勾选。蓝色文件夹才是真实文件夹,它会真真实实打在ipa包里,所以在勾选了它的时候,我们读取资源要写资源名的全路径,否则读取不到资源
  • Add to targets:不勾选的话,则该文件不会参与编译,也不会打到target对应的安装包中,建议勾选
UIImage加载图片的两种方式有什么区别
  • 首先imageNamed:既能加载Assets里的图片,也能加载项目目录下的图片;而imageWithContentOfFile:只能加载项目目录下的图片,不能加载Assets里的图片。这是因为项目目录下的图片是直接存在于项目bundle目录下的,也就是[NSBundle mainBundle]里,我们可以拿到图片的完整路径,而Assets里的图片则是存储在项目bundle目录下的一个Assets.car压缩文件里,我们没法拿到这个压缩文件里图片的完整路径,因此imageWithContentOfFile:不能加载Assets里的图片,因为它需要图片的完整路径
  • 其次系统会对imageNamed:加载的图片做内存缓存,也就是说就算一个UIImage对象已经没有任何一个UIImageView使用它了,它也不会销毁,而是仍然存在于内存中,也就是说将来无论有多少个UIImageView要显示这张图片,其实都指向的是同一个UIImage对象,除非我们杀掉App它们才会销毁,因此imageNamed:加载图片会更快,但是很可能会导致内存溢出;而系统不会对imageWithContentOfFile:加载的图片做内存缓存,也就是说这个UIImage对象对应的UIImageView一旦销毁,那么出了作用域这个UIImage对象也会随之销毁,将来几个UIImageView要显示这张图片其实就会创建多少个UIImage对象,因此imageWithContentOfFile:加载图片可能会相对较慢,但导致内存溢出的概率会大大降低
  • 那么在实际开发中,加载使用频率不是很高、而且内存很大的单张图片的场景下——如加载地图那种大图更推荐使用imageWithContentOfFile:,以便图片内存能够及时释放掉,从而降低内存溢出的概率,因此这类图片只能放在项目目录下;而加载频繁使用、而且内存很小的某一张图片的场景下——如tableview上的占位图则更推荐使用imageNamed:,因为它们共用的是同一个UIImage对象,避免重复创建,加载速度会更快,因此这类图片既可以放在项目目录下也可以放在Assets里、推荐放在Assets里
UIImageView如何显示一张聊天气泡那种拉伸的图片
聊天气泡拉伸图片效果实现原理

4、UIButton的imageView和titleLabel的布局样式

就是根据imageView在左,titleLabel在右 || imageView在下,titleLabel在上 || imageView在右,titleLabel在左 || imageView在上,titleLabel在下重新计算并设置一下imageView和titleLabel的frame,UIButton通过frame或AutoLayout都可以,不过需要注意的是button的大小要大于图片大小 + 文字大小 + space、否则显示不全,如果显示不全的话就考虑自定义一个view吧。

5、UITextField

UITextField的几个代理方法
#pragma mark - UITextFieldDelegate

// textField开始编辑了、光标开始闪动(textField成为第一响应者)
- (void)textFieldDidBeginEditing:(UITextField *)textField {
    NSLog(@"textField开始编辑了、光标开始闪动");
}

/// textField改变了内容
/// @param string 点击键盘想要输入的内容
/// @return 是否允许textField改变内容,默认YES,如果改成NO、textField就输入不了了、只有光标在闪动,可以通过这个返回值来达到拦截用户输入的效果
///
/// 只有像下面这样的顺序,才能实时获取到正确的text喔
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
    NSLog(@"点击键盘想要输入的内容===%@", string);
    
    if ([string isEqualToString:@"0"]) { // 假设我们不允许输入数字0
        return NO;
    }
    
    // 注意:如果直接在这个方法里通过textField.text获取textField的内容,是会少掉最后一个字符的,我们得通过如下方法来实时获取textField的内容
    NSString *text = [textField.text stringByReplacingCharactersInRange:range withString:string];
    if (text.length > 11) { // 假设我们只允许输入11位数字
        return NO;
    }
    
    NSLog(@"textField的内容===%@", text);
    
    return YES;
}

// textField结束编辑了、光标停止闪动(textField释放第一响应者)
- (void)textFieldDidEndEditing:(UITextField *)textField {
    NSLog(@"textField结束编辑了、光标停止闪动");
}

// 点击了键盘上的return键
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    [self.textField resignFirstResponder];
    return YES;
}
UITextField的inputView和inputAccessoryView属性

6、UIScrollView

UIScrollView的常见属性
  • scrollEnable:scrollView是否可以滚动
  • userInteractionEnabled:scrollView是否能接收用户事件,这个属性比scrollEnable管得更大,当它设置为NO时,scrollView不仅不能滚动,而且也不能响应添加在它身上的手势等事件
  • pagingEnabled:是否允许分页
  • bounces:当scrollView可以滚动时,滚动到边界是否有弹性效果
  • alwaysBounceVertical和alwaysBounceHorizontal:当scrollView不可以滚动时(比如没有数据时,scrollView就不能滚动了,但是我们想做下拉刷新效果,那就得靠这两个属性),滚动到边界是否有弹性效果
  • contenSize:scrollView内容的大小,只有它大于scrollView本身的大小时,scrollView才能滚动(当然前提是scrollEnable和userInteractionEnabled都为YES)
  • contentOffset:scrollView的顶点减去scrollView内容的顶点的偏移量
  • contentInset:scrollView的内边距
UIScrollView的常见代理方法
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    NSLog(@"即将开始拖拽scrollView");
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    NSLog(@"scrollView滚动中");
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    if (decelerate == NO) {
        NSLog(@"scrollView停止滚动");
    } else {
        NSLog(@"已经停止拖拽scrollView,但是scrollView由于惯性还在减速滚动");
    }
}

// 但是光靠这个方法来判定scrollView停止滚动是有一个bug的,那就是当我们的手指停止拖拽scrollView时、按住屏幕不放手、导致scrollView不滚动,是不会触发这个方法的,而是会触发scrollViewDidEndDragging:willDecelerate:方法,所以严谨来判断应该靠它俩联合
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    NSLog(@"scrollView停止滚动");
}

7、UITableView

  • UITableView的两种样式
  • UITableViewCell的四种样式
  • UITableView的性能优化

一旦使用了tableView,我们就得考虑它的性能问题,因为它可以展示无限条数据并且无限滚动。

1、首先我们要问的问题是:tableView会一次性创建多少个cell
因为我们可能需要展示无限条数据,那如果tableView一次性创建了无数个cell来展示这无限条数据的话,那势必会造成内存的暴涨,从而导致内存溢出。好在iOS系统为我们考虑到了这一点,它不会一次性创建所有的cell,而是屏幕一次性能展示几个cell它就只创建几个cell,当cell滑出屏幕的时候就销毁这个cell,当cell滑进屏幕的时候才创建一个新的cell,而不会导致内存溢出,而我们开发者只需要像使用普通View那样调用init或initWithFrame方法来创建cell即可,其它的什么都不用管。

2、然后我们要问的问题是:tableView有没有重用机制
从上面的问题可以看出iOS系统已经为tableView做了很好的性能优化————即内存中只有有限个cell,但是这样做还是有一个问题那就是cell反复地滑进滑出屏幕会导致cell频繁地创建和销毁,这是一个很耗性能的操作。好在iOS系统又为我们考虑到了这一点,它提供了一个cell缓存池,每当要创建cell时,它都会先去缓存池里看一看有没有现成的cell可用,有的话就直接拿来用,没有的话才去创建新cell,然后当一个cell滑出屏幕时就会把这个cell放进缓存池以备复用,这样就通过缓存池的办法解决掉了cell频繁创建销毁的性能问题,无限轮播图好像也是这个实现原理,而我们开发者只需要调用initWithStyle:reuseIdentifier:方法来创建cell即可————即开发中一定要使用tableView的重用机制、不用白不用,其它的什么都不用管。

3、第三个我们要问的问题是:tableView的滑动卡顿怎么解决
上面两个问题已经解决了tableView的展示性能,可以说iOS系统的这种做法已经做到了极致,那tableView可以无限滚动,怎么解决滑动卡顿问题呢?优化tableView滑动卡顿的方式有很多,不过效果最明显的还是计算和缓存行高,具体可以参考博客里的性能优化篇。

  • 自定义cell与cell高度自适应

// 自定义cell必须得重写这个初始化方法,因为外界创建cell时为了能重用cell,是会调用这个初始化方法来创建cell的,而不是调用init或initWithFrame,同时也不要重写init或initWithFrame,这样外界调用这俩方法时因为不会触发这个初始化方法而什么什么都加载不到,就可以隐形强制外界重用cell了

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
        
    }
    return self;
}

这里说一下我们实际开发中可能遇到的处理cell高度的三种情况:

1、cell是固定高度的
这没什么好说的,直接在heightForRow方法里返回一个固定的值即可

2.1、cell高度不固定:做法一————cell高度自适应
cell高度自适应这种做法是针对cell高度不固定这种场景的,具体的做法分为两步:
————首先得像聊天气泡那样添加约束
————然后在代码里写上下面这两句代码就可以了
tableView.estimatedRowHeight = 44;
在heightForRow方法里返回UITableViewAutomaticDimension

2.1、cell高度不固定:做法二————计算并缓存cell的高度
计算并缓存cell的高度这种做法也是针对cell高度不固定这种场景的,只不过相对cell高度自适应那种做法来说,这种做法可以提高tableView的滑动性能,因为这种做法是我们开发者在掌控cell高度的计算过程,因此我们就可以做到只计算一次就缓存下来,不用频繁地计算。可以去Swift DSWB这个项目里看看具体的示例代码,这里只描述一下实现思路:
————cell内部添加子控件不用多说,还是在initWithStyle:reuserIndentifier:里添加就可以了
————cell内部布局子控件不用多想,还是在layoutSubviews方法里做,你就当做cell是固定高度那样从上往下布局就行了,不用考虑像聊天气泡那种自适应高度的布局,因为最终我们计算好cell的高度返回时,cell的高度的确就是一个确定的值,这就可以保证cell上子控件和cell的底部边界是我们想要的效果
————至于计算并缓存cell的高度这一步则是在cell内部ViewModel的setter方法里做的,那怎么个计算法呢?其实我们并不会真得去把一个控件一个控件的高度给加起来,而是获取最下面一个控件的maxY就可以了。以微博cell举例,对于那些同步就能显示出来的文本控件(不管是固定高度还是不固定高度,比如昵称和微博正文),我们压根就不用管这些控件,直接拿着vm的数据往上赋值即可,对于那些异步但是高度已经固定好的图片控件(比如头像),我们也压根不用管这些控件,也是直接拿着vm的数据往上赋值即可,只要那些异步并且高度不固定的控件(比如正文里的图片,有可能是一张、三张、九张等),它们的高度我们需要自己算一下并且更新一下约束(这个约束在布局时可以随便先给个初始值),然后cell.layoutIfNeeded()强制布局一下 + 获取最后一个控件的maxY就算计算好cell的高度了(注意我们调用cell.layoutIfNeeded()其实强制刷新的是cell.contentView),赋值给vm.cellHieght属性就算缓存好cell的高度了
————接下来就是使用计算并缓存好的cellHeight了,因为cellForRow方法会比heightForRow方法先触发,而我们又是在cellForRow方法里给cell赋值ViewModel数据的,所以当触发heightForRow方法获取vm.cellHeight时肯定就已经计算并缓存好cell的高度了,直接拿来用即可

注意,这里我们说一下tableView.estimatedRowHeight这句代码的作用:
————当我们用在cell高度自适应这种做法的时候,它的作用其实是告诉系统是否要使用高度自适应,也就是这句话可以是随便一个大于0的值,起到YES/NO的效果
————当我们用在计算并缓存cell的高度这种做法的时候,它的作用其实是可以保证cellForRow永远比heightForRow方法先执行,这样就可以保证执行heightForRow方法时,我们已经计算并缓存好了每行cell的高度,因为在iOS11之前的系统上heightForRow方法是比cellForRow方法先执行的

  • UITableView主要代理方法的执行顺序
  • UITableView的全局刷新和局部刷新

1、UITableView的全局刷新
无论是增删改,都可以使用reloadData方法,只不过屏幕上显示了多少个cell就会触发多少次代理方法

2、UITableView的局部刷新
如果是增,可以使用insertRow和insertSection方法,只会触发有限次代理方法,性能更高
如果是删,可以使用deleteRow和deleteSection方法,只会触发有限次代理方法,性能更高
如果是改,可以使用reloadRow和reloadSection方法,只会触发有限次代理方法,性能更高

  • UITableView的一些编辑操作

8、UICollectionView

知道怎么使用FlowLayout计算cell的大小就行。


二、UIView和UIViewController的一些方法


1、UIView的一些方法

  • view是纯代码编写的时候
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    /*
     1、当我们的view是纯代码编写的时候,我们可以通过init或initWithFrame方法来创建它
     2、而view内部只需要重写initWithFrame方法来添加子视图(因为就算是通过init方法来创建view也会触发initWithFrame方法),重写layoutSubviews来布局子视图
     3、而如果我们想给view的子视图添加子视图,也是在initWithFrame方法里添加,在layoutSubviews方法里布局
     */
    RedView *redView = [[RedView alloc] init];
    redView.frame = CGRectMake(100, 100, 100, 100);
    [self.view addSubview:redView];
}

@end


#import "RedView.h"

@interface RedView ()

@property (nonatomic, strong) UIView *subview;
@property (nonatomic, strong) UIView *subview1;

@end

@implementation RedView

// 重写initWithFrame方法来添加子视图
- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        self.backgroundColor = [UIColor redColor];
        
        [self addSubview:self.subview];
        [self.subview addSubview:self.subview1];
    }
    return self;
}

// 重写layoutSubviews方法来布局子视图(因为init和initWithFrame里不一定能获取到当前view.frame,而这个方法里却肯定能获取到当前view.frame)
- (void)layoutSubviews {
    [super layoutSubviews];

    self.subview.frame = CGRectMake(0, 0, self.frame.size.width / 2, self.frame.size.height / 2);
    self.subview1.frame = CGRectMake(0, 0, self.subview.frame.size.width / 2, self.subview.frame.size.height / 2);
}

- (UIView *)subview {
    if (_subview == nil) {
        _subview = [[UIView alloc] init];
        _subview.backgroundColor = [UIColor greenColor];
    }
    return _subview;
}

- (UIView *)subview1 {
    if (_subview1 == nil) {
        _subview1 = [[UIView alloc] init];
        _subview1.backgroundColor = [UIColor blueColor];
    }
    return _subview1;
}

@end
  • view关联了xib时
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    /*     
     一、通过loadNib方式创建view,走的是initwithcoder和awakefromnib这一套
     
     1、当我们的view关联了xib时,我们就必须得通过loadNib的方式来创建它,loadNib方法对应于view内部的initwithcoder方法,而非init或initWithFrame方法
     2、而view内部添加子视图的操作就不用做了,因为肯定已经在xib做了,而view内部布局子视图的操作如果在xib里做了,那么就不用代码做了,如果想要用代码来布局,那么也得在layoutSubviews方法里做
     3、而如果我们想给view的子视图添加子视图,却不能在initwithcoder里做,因为initwithcoder方法里子视图还没被激活,只有视图本身被激活了,所以跟视图本身相关的操作我们可以在initwithcoder方法里做,但是跟子视图相关的操作我们必须在awakefromnib里做,当然跟视图本身相关的操作或跟子视图相关的操作我们都可以放在awakefromnib方法里做,在这个方法里它们肯定都被激活了,布局仍然是在layoutSubviews方法里布局
     
     
     二、init或initWithFrame方法来创建view,走的才是老一套
     
     1、当我们的view关联了xib时,外界依然可能会通过init或initWithFrame方法来创建view,所以为了兼容这两种方式,我们就得重写initWithFrame方法,在initWithFrame方法里再通过loadNib的方式来创建view并返回;这样外界就既可以通过loadNib,也可以通过init或initWithFrame来创建view了
     2、如果这么一兼容,当我们走通过init或initWithFrame方法来创建view这种方式时,就跟纯代码的没有任何区别了,我们像之前那样在initWithFrame方法里添加视图,在layoutsubviews里布局是就可以了
     */
//    YellowView *yellowView = [[[NSBundle mainBundle] loadNibNamed:@"YellowView" owner:nil options:nil] firstObject];
    YellowView *yellowView = [[YellowView alloc] init];
    yellowView.frame = CGRectMake(100, 100, 100, 100);
    [self.view addSubview:yellowView];
}

@end


#import "YellowView.h"

@interface YellowView ()

@property (weak, nonatomic) IBOutlet UIView *subview;
@property (nonatomic, strong) UIView *subview1;

@end

@implementation YellowView

#pragma mark - init或initWithFrame方法来创建view

// 兼容init或initWithFrame方法来创建view
- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:nil options:nil] firstObject];

        [self.subview addSubview:self.subview1];
    }
    return self;
}


#pragma mark - 通过loadnib的方法创建view

// 跟视图本身相关的操作在这里做
- (instancetype)initWithCoder:(NSCoder *)coder {
    if (self = [super initWithCoder:coder]) {
        
    }
    return self;
}

// 跟视图本身相关的操作、跟子视图相关的操作都可以在这里做
- (void)awakeFromNib {
    [super awakeFromNib];
    self.backgroundColor = [UIColor yellowColor];
    
    [self.subview addSubview:self.subview1];
}


#pragma mark - 布局视图

- (void)layoutSubviews {
    [super layoutSubviews];
    
    self.subview.frame = CGRectMake(0, 0, self.frame.size.width / 2, self.frame.size.height / 2);
    self.subview1.frame = CGRectMake(0, 0, self.subview.frame.size.width / 2, self.subview.frame.size.height / 2);
}


#pragma mark - setter, getter

- (UIView *)subview1 {
    if (_subview1 == nil) {
        _subview1 = [[UIView alloc] init];
        _subview1.backgroundColor = [UIColor blueColor];
    }
    return _subview1;
}

@end

2、UIViewController的一些方法

  • loadView方法和viewDidLoad方法

我们知道每个ViewController都有个自带的view,而这个view是懒加载的,正是在vc.view的懒加载里触发了loadView方法和viewDidLoad方法:

#pragma mark - getter

/// 伪代码
- (UIView *)view {
    if (_view == nil) {
        // 先调用loadView方法创建vc.view(或者叫加载vc.view)
        [self loadView];
        // vc.view加载完毕后,就调用viewDidLoad方法
        [self viewDidLoad];
    }
    return _view;
}
/// 作用:创建vc.view(或者叫加载vc.view)
/// 调用时机:第一次创建(或者叫第一次加载)vc.view时调用
///
/// 伪代码
- (void)loadView {    
    self.view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
}

/// 调用时机:vc.view加载完毕后调用
- (void)viewDidLoad {
    [super viewDidLoad];

}

程序启动时,我们肯定会在AppDelegate.m里写类似下面的代码,所以为了加快程序的启动速度,我们应该尽量少在rootVC的三个方法里写耗时代码:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 创建window
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    
    // 显示window
    [self.window makeKeyAndVisible];

    // 设置window的rootVC
    // 这句代码会把rootVC.view添加到window上,类似于执行[self.window addSubview:self.window.rootViewController.view]
    // 因此正是这句代码触发了rootVC.view的懒加载,进而触发rootVC的loadView方法和viewDidLoad方法
    // 所以此处会等rootVC的loadView方法和viewDidLoad方法执行完 + viewWillApper方法执行完才会回调回来
    [self.window setRootViewController:[[RootViewController alloc] init]];
        
    return YES;
}
  • viewWillAppear方法、viewDidAppear方法和viewWillDisappear方法、viewDidDisappear方法

假设有两个控制器A和B,当A push出B的时候,会首先触发A的viewWillDisappear方法,然后触发B的viewWillAppear方法,然后触发A的viewDidDisappear方法,然后触发B的viewDidAppear方法,B pop的时候也是同理,总之是一扭一扭的调用。

你可能感兴趣的:(【UI】基础知识)