2020-09-03 iOSUI浅析及使用经验分享

  • 原理部分

    1. UIView与CALayer

      iOS中所有的UI控件都有一个共有父类UIView,而提起UIView,始终有一个CALayer对象伴随。那么UIView与CALayer之间关系就是研究UI的一个关键。

      先说结论:CALyaer是UIView的属性,UIView是CALayer的代理。

      如何理解这句话呢,我们先看两个类UIResponder和CALayer

      UIResponder类是专门用来响应用户的操作处理各种事件的,包括触摸事件(Touch Events)、运动事件(Motion Events)、远程控制事件(Remote Control Events,如插入耳机调节音量触发的事件)。

      CALayer包含在QuartzCore框架中,这是一个跨平台的框架,既可以用在iOS中又可以用在Mac OS X中,用来绘制内容与处理各种动画。

      我们知道手机屏幕有三层,最外层是保护玻璃,中间是解控层,最下面是显示层。那么UIResponder对应的就是解控层,CALayer对应的就是显示层。UIView继承自UIResponder,拥有处理各种触摸事件的能力,又把layer对象当属性,有了显示内容的能力,然后在内部把两者结合从而成了UIView,拥有了通过触摸屏幕改变显示内容的能力。可以通俗的理解为,我们看到的都是layer,摸到的都是UIView。

      layer负责显示那么我们关于显示操作的相关属性,其实本质都是在操作calyer,下面的一些截取UIKit源码可以说明这些。

      - (CGRect)frame
      {
          return _layer.frame;
      }
      
      - (void)setFrame:(CGRect)newFrame
      {
          if (!CGRectEqualToRect(newFrame,_layer.frame)) {
              CGRect oldBounds = _layer.bounds;
              _layer.frame = newFrame;
              [self _boundsDidChangeFrom:oldBounds to:_layer.bounds];
              [[NSNotificationCenter defaultCenter] postNotificationName:UIViewFrameDidChangeNotification object:self];
          }
      }
      
      - (CGRect)bounds
      {
          return _layer.bounds;
      }
      
      - (void)setBounds:(CGRect)newBounds
      {
          if (!CGRectEqualToRect(newBounds,_layer.bounds)) {
              CGRect oldBounds = _layer.bounds;
              _layer.bounds = newBounds;
              [self _boundsDidChangeFrom:oldBounds to:newBounds];
              [[NSNotificationCenter defaultCenter] postNotificationName:UIViewBoundsDidChangeNotification object:self];
          }
      }
      
      

      这部分是最常用的设置frame bounds的源码,可以到内部实现就是在改变layer的frame和bounds

      - (void)addSubview:(UIView *)subview
      {
          NSAssert((!subview || [subview isKindOfClass:[UIView class]]), @"the subview must be a UIView");
      
          if (subview && subview.superview != self) {
              UIWindow *oldWindow = subview.window;
              UIWindow *newWindow = self.window;
              
              [subview _willMoveFromWindow:oldWindow toWindow:newWindow];
              [subview willMoveToSuperview:self];
      
              if (subview.superview) {
                  [subview.layer removeFromSuperlayer];
                  [subview.superview->_subviews removeObject:subview];
              }
              
              [subview willChangeValueForKey:@"superview"];
              [_subviews addObject:subview];
              subview->_superview = self;
              [_layer addSublayer:subview.layer];
              [subview didChangeValueForKey:@"superview"];
      
              if (oldWindow.screen != newWindow.screen) {
                  [subview _didMoveToScreen];
              }
              
              [subview _didMoveFromWindow:oldWindow toWindow:newWindow];
              [subview didMoveToSuperview];
              
              [[NSNotificationCenter defaultCenter] postNotificationName:UIViewDidMoveToSuperviewNotification object:subview];
      
              [self didAddSubview:subview];
          }
      }
      

      这是UIView必不可少的addSubView 方法,可以看到内部最主要的就是[_layer addSublayer:subview.layer];这一句话。

      - (void)setHidden:(BOOL)h
      {
          if (h != _layer.hidden) {
              _layer.hidden = h;
              [[NSNotificationCenter defaultCenter] postNotificationName:UIViewHiddenDidChangeNotification object:self];
          }
      }
      
      - (BOOL)isHidden
      {
          return _layer.hidden;
      }
      

      综上代码可以看出,UIView凡是看到的都是layer,凡是操作与显示有关的本质都是在操作layer

      UIView又是layer的代理,可以UIView里自定义绘制,流程如下

      flow
      st=>start: [_layer drawInContext: (CGContextRef)context]
      op1=>operation: [_delegate drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx]
      op2=>operation: [view drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx]
      op3=>operation: [view drawRect]
      e=>end
      st->op1->op2->op3->e
      
      
      

      UIView的laye对象绘制时会通过代理调用view的drawLayer方法,而在drawlayer方法里会调用drawRect方法,我们自定义UI时,继承UIView 实现drawRect方法即可。

    2. 关于frame和bounds

      UIView有个必用的属性就是frame,是个CGRect类型。由一组坐标和一组size组成。size就是表示宽和高,坐标是基于父view,以父view左上为圆点。frame的概念非常好理解。但是bounds虽说也是CGRect类型的值,但是跟frame的概念完全不一样。bounds表示的是UIView的显示区域。改变x会让View的显示区域向左或向右偏移,改变y会让view的显示区域向上或者向下偏移。那改变size就是改变可显示区域的大小。我们熟悉的UIScrollView就是通过改变bounds实现的,scrollView使用时会设置它的contentSize,一般contentSize是大于scrollView本身的size的,scrollView基本原理就是添加一个拖动手势,在拖动时改变scrollView的bounds,来显示不同的内容。

    3. 关于autolayout

      autolayout是现在主要的UI书写方式,frame最大的问题就是是个绝对布局,性能很好,就是非常不灵活,碰到宽高动态变化时就会非常的麻烦。autolayout是相对布局,通过各view之前添加约束,形成一个链条,当其中一个变化时,其它也会跟着自动变化,是比较优雅的UI书写方式。

      autolayout并不是一种新UI实现方式,它最后还是以frame的方式落地。 它的原理就是在解多元一次方程,通过各约束条件构建方程,最后得到惟一的解,即它的最终frame。这就是父view可以通过上下左右的约束来确定子view的大小,子view也同样可以通过上下左右的约束来反推出父view的大小的理论依据。

  • 应用部分

    1. UIView 固有大小(intrinsicContentSize)

      在写UI代码时, 我们会发现UILabel UIButton只需要添加位置确定的约束即可,无需添加大小相关的约束,这是因为UIView的intrinsicContentSize的属性,意为固有大小。作用就是当view没有明确大小时,就按照此属性的值来显示。UILabel,UIImageView,UIButton等这些组件及某些包含它们的系统组件都有 Intrinsic Content Size 属性,也就说他们都有自己计算size的能力。

      我们也可利用此属性,来实现动态size变化的view。

      实例说明:

      需求:view包含有左右两个label,各占一半宽度,左右两个label高度随文字多少变化而变化,要求view高度取两个label最高高度。

      实现:左右label各添加上下左右的边界约束,width equal,无需添加高度的约束。当某中一个label高度变化时,view的高度将会自动会依据较高的label来计算。

      原理:当view的size是由子view推算出来时,view总是会取最大值,而又由于intrinsicContentSize的特殊性,所以当view的大小被确定时,其子view的size会根据父view的大小来确定。

    2. 优先级

      AutoLayout添加的约束中也有优先级(Priority),优先级的数值1~1000,分为两种情况:

      • 一种情况是我们经常添加的各种约束,默认值1000(最大值)优先执行,条件允许的话系统会自动满足我们的约束需求。
      • 第二种就是固有约束(intinsic content size)严格说这种更像UILabel和UIButton的一种属性,但是在Autolayout中需要满足属性取值与约束优先级属性结合才能完成图形的绘制

      当UILabel显示的内容过长或太短,控件就会被拉伸和压缩,当我们不想让控件被拉伸压缩时,就需要设置控件的固有约束(intinsic content size)来实现我们的需求。固有约束分为两种:

      • 扛拉伸优先级(Content Hugging Priority):默认251,优先级越高越不易被拉伸
      • 防压缩优先级(Content Compression Resistance Prority):默认750,优先级越高越不易被压缩
    3. systemLayoutSizeFittingSize

      有autolayout我们大多时候不需要关心宽高的问题,但总有些情况我们需要知道一些UI的总高度。比方说,tableViewheaderView,不支持autolayout,我们总需要给它设置好frame才能正确显示。

      当我们用autolaout实现一个headerview时,就要知道它所需要的高度。此时就可用UIView 的一个方法

      [view systemLayoutSizeFittingSize:CGSizeZero];需要一个CGSize参数,是预估大小,并不起决定性作用。使用此方法,要得到预期的真实大小,最底层的父view上下左右的约束一定要设置好。

      注意:当一个View使用intrinsicContentSize来作为它的size,label或button之类的,那么使用此API时可能会得不到预期的效果,这时需要给它一个确定的宽或高,来获取预期的size。

    4. view均分

      需求:一个view有若干子view,子view大小相同,均匀的排布。

      有两种实现方案:

      • 各子view通过上下左右约束相互连接,宽度各相等的约束。

        代码如下:

            UIView *fatherView = [[UIView alloc]init];
            UIView *tempView;
            for (int i = 0 ; i < 3; i ++) {
                UIView *view = [[UIView alloc]init];
                [fatherView addSubview:view];
                [view mas_makeConstraints:^(MASConstraintMaker *make) {
                    make.top.bottom.mas_equalTo(0);
                    if (tempView) {
                        make.left.mas_equalTo(tempView.mas_right);
                        make.width.mas_equalTo(tempView);
                    }
                    else{
                        make.left.mas_equalTo(0);
                    }
                }];
                tempView = view;
            }
            [tempView mas_makeConstraints:^(MASConstraintMaker *make) {
                make.right.mas_equalTo(0);
            }];
        
      • view宽度基于父view,通过centerX改变位置,centerX 可用公式来计算:CGFloat multi = (i*2.0+1.0)/totalCount;

        代码如下:

            UIView *fatherView = [[UIView alloc]init];
            for (int i = 0 ; i < 3; i ++) {
                UIView *view = [[UIView alloc]init];
                view.backgroundColor = [UIColor blueColor];
                [fatherView addSubview:view];
                [view mas_makeConstraints:^(MASConstraintMaker *make) {
                    make.top.bottom.mas_equalTo(0);
                    make.width.mas_equalTo(fatherView).multipliedBy(1.0/3.0);
                     CGFloat multi = (i*2.0+1.0)/3;
                    make.centerX.mas_equalTo(0).multipliedBy(multi);
                }];
            }
        

        第一种是通过相互的位置关系得到确定位置,可以随意设置各自的大小,间距等,但是比较适用于一行排满的情况。

        第二种,是通过计算得到中心点以及宽度,比较适合一行分成多份,但是元素数量不够的情况。

        在项目中,提供一个自定义UIFBUniformAutoWrapView,就是专门处理这种UI。

    5. 代码书写方案

      在写UI代码时,推荐这种书写方式

      - (void)darwView{
          UIView *tempView;
          tempView = [self drawAViewWith:tempView];
          if (1) {
              tempView = [self drawBViewWith:tempView];
          }
          tempView = [self drawCViewWith:tempView];
          tempView = [self drawDViewWith:tempView];
      }
      - (UIView *)drawAViewWith:(UIView *)topView{
          return nil;
      }
      - (UIView *)drawBViewWith:(UIView *)topView{
          return nil;
      }
      - (UIView *)drawCViewWith:(UIView *)topView{
          return nil;
      }
      - (UIView *)drawDViewWith:(UIView *)topView{
          return nil;
      }
      

      这么写有以下好处

      • 代码的书写顺序就是真实的显示顺序,一一对应,更易阅读
      • 只需要改变代码书写顺序,加逻辑判断,删除修改,都简单,更易维护
      • 每块view只需关心传过来参数view的相对位置即可,传过来谁都一样,更易实现

你可能感兴趣的:(2020-09-03 iOSUI浅析及使用经验分享)