第一篇:Objective-C 知识回顾的UI视图部分之一

前言:针对一些非常重要,又易错的逻辑,会给出代码练习题,先按照自己的思路把代码实现一遍,会有更大的收获。

1.1.UITableView 重用机制

核心知识点 1 -- 重用池

模拟 UITableView重用机制,使用代码自己构建一个重用池子,这样可以深入理解 cell 重用的原理。俗话说“源码面前无秘密”

Talk is cheap, show me the code:

// ViewReusePool.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

// 实现重用机制的类
@interface ViewReusePool : NSObject

// 从重用池当中取出一个可重用的view
- (UIView*)dequeueReusableView;

// 向重用池当中添加一个视图
- (void)addUsingView:(UIView*)view;

// 重置方法,将当前使用中的视图移动到可重用队列当中
- (void)reset;

@end
// ViewReusePool.m


#import "ViewReusePool.h"

@interface ViewReusePool ()
// 等待使用的队列
@property (nonatomic, strong) NSMutableSet *waitUseQueue;
// 使用中的队列
@property (nonatomic, strong) NSMutableSet *usingQueue;

@end

@implementation ViewReusePool

- (instancetype)init
{
    self = [super init];
    if (self) {
        _waitUseQueue = [[NSMutableSet alloc] init];
        _usingQueue = [[NSMutableSet alloc] init];
    }
    return self;
}

- (UIView *)dequeueReusableView {
    UIView *view = [_waitUseQueue anyObject];
    if (view) {
        [_waitUseQueue removeObject:view];
        [_usingQueue addObject:view];
    }
    return view;
}

- (void)addUsingView:(UIView *)view {
    [_usingQueue addObject:view];
}

- (void)reset {
    UIView *view = nil;
    while ((view = [_usingQueue anyObject])) {
        // 从使用中队列移除
        [_usingQueue removeObject:view];
        // 加入等待使用的队列
        [_waitUseQueue addObject:view];
    }
}

@end

思路简述:
构建了个 重用 池子,提供了三个 API 供给外部调用,从而实现 view 的重用。

核心知识点 2 -- 使用 Protocol 和 delegate 仿照 UITableView 实现自定义控件

自定义一个索引 indexBar,模拟 UITableView 的数据源代理模式,从而从代码的层次来理解 delegate 的使用和强大。

// IndexBar.h
#import <UIKit/UIKit.h>
@protocol IndexBarDataSource <NSObject>
- (NSArray *_Nullable)indexBarDataSoure;
@end

@interface IndexBar : UIView
@property(nonatomic, weak) id<IndexBarDataSource> dataSource;
- (void)reload;
@end
// IndexBar.m
#import "IndexBar.h"
#import "ViewReusePool.h"

@interface IndexBar ()
@property(nonatomic, strong) ViewReusePool *viewReusePool;
@property(nonatomic, strong) NSArray *dataArray;

@end

@implementation IndexBar

- (instancetype)init
{
    self = [super init];
    if (self) {
        _viewReusePool = [[ViewReusePool alloc] init];
        self.backgroundColor = [UIColor greenColor];
    }
    return self;
}


- (void)didMoveToSuperview {
    [super didMoveToSuperview];
    [self reloadData];
}

- (void)reloadData {
   
    if (self.dataSource && [self.dataSource respondsToSelector:@selector(indexBarDataSoure)]) {
           self.dataArray = [self.dataSource performSelector:@selector(indexBarDataSoure)];
       }
       
       for (int i = 0; i < self.dataArray.count; i++) {
           UIButton *btn = (UIButton *)[self.viewReusePool dequeueReusableView];
           if(!btn) {
               btn = [UIButton buttonWithType:UIButtonTypeCustom];
               NSLog(@"创建了 button");
           } else {
               NSLog(@"重用了 button");
           }

           [btn setTitle:[NSString stringWithFormat:@"%i",i] forState:UIControlStateNormal];
           btn.frame = CGRectMake(0, 80 * i, 70, 80);
           [self addSubview:btn];
           [self.viewReusePool addUsingView:btn];
       }
}

- (void)reload {
    [[self subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
    [self.viewReusePool reset];
    [self reloadData];
}
@end
温馨提示:如果说到重用池、protocol、delegate,心中没有一个明确的使用方案,建议先不要看代码,直接下载代码先运行,然后用你的方法和思路,使用上面的三个知识点来先用你自己的代码实现运行的内容。然后进行对比一下,相信你对这块知识就会了然于胸。
项目传送门:LSPUISecionIndexBar(注意观察日志输出)

1.2.UITableView 数据源同步问题

如何解决 UITableview 在多线程的情况下修改、删除、远程读取数据,导致的数据源同步问题?

解决方式两种

  • 并行队列&数据拷贝 (拷贝数据到子线程,子线程进行耗时操作,然后主线程对用户的删除操作进行记录,最后子线程完成耗时操作也进行前面记录的删除操作。这样就保证了数据源同步问题。)缺点:拷贝数据如果量大就比较耗时,而且需要额外的记录操作的工作。

  • 串行队列(把这些操作都放到串行队列中,子线程完成了耗时操作,才允许用户进行删除操作,这样一直使用一份数据,也就保证了数据源同步)缺点:用户的删除操作可能需要等待的时长更长。

1.3. UIView 和 CALayer

UIView 和 CALayer关系

每一个UIView实例中,都有一个默认的支持图层 layer,UIView 负责创建并且管理这个图层。实际上 UIView 之所以能够显示,就是因为它里面有一个 CALayer,才具有显示的功能,UIView 仅仅是对它的一层封装,实现了 CALayer 的 delegate,提供了处理事件交互的具体功能,还有动画底层方法的高级 API。可以说 CALayer 是 UIView 的内部实现细节。

UIView 和 CALayer区别

  • UIView 继承于 UIResponder,可以响应事件,CALayer 继承于 NSObject 不能响应事件。
  • UIView 主要是对显示内容的管理,而 CALayer 主要侧重显示内容的绘制。(从设计模式上来讲体现了单一职责的设计原则

1.4. 事件传递与视图响应链(非常重要,重中之重)

事件传递的两个方法:

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event;  // 供给外部调用
-(BOOL)pointInside(CGPoint)point withEvent:(UIEvent*)event; // 供给 hitTest:withEvent:调用

事件相应的四个方法,来自 UIResponder :

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
事件的传递流程
事件传递注意事项
  • 上图所示的流程非常重要,要牢记。
  • view 遍历子视图是倒序遍历
hitTest:withEvent: 程序内部逻辑图
逻辑图注意事项
  • 上图所示的流程非常重要,要牢记。
  • view 遍历子视图的时候,要进行 point 的坐标转换。
事件传递,代码练习
事件传递理解习题要求
部分代码说明
// CustomButton.m文件
#import "CustomButton.h"

@implementation CustomButton

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    // Drawing code
}
*/

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    //1.先判断是否能交互/隐藏/透明度,来决定此 view 是否具备事件传递的资格前提.
    if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01 ) {
        return nil;
    }
    //2.调用 pointInside:withEvennt:方法判断点击的范围是否在此 view 之内.
    if ([self pointInside:point withEvent:event]) {
        //3.反序遍历子视图,调用hitTest:withEvent:方法,查找最合适的 view.
        __block UIView *tempView = nil;
        __weak typeof(self) weakSelf = self;
        [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            //4.这里要记得对 point 进行坐标系的转换.
            CGPoint newPoint = [weakSelf convertPoint:point toView:obj];
            tempView = [obj hitTest:newPoint withEvent:event];
            if (tempView) {
                *stop = YES;
            }
        }];
        //5.如果找到就返回子视图中找到的 view,没有就返回自己.
        if (tempView) {
            return tempView;
        } else {
            return self;
        }
    } else {
        return nil;
    }
    
}
//

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // 根据坐标系计算距离中心点的直线距离,来判断
    float xValue = point.x - self.frame.size.width / 2.0 ;
    float yValue = point.y - self.frame.size.width / 2.0;
    if (xValue < 0) {
        xValue = -xValue;
    }
    if (yValue < 0) {
        yValue = -yValue;
    }
    if (xValue*xValue + yValue*yValue > (self.frame.size.width / 2.0) * (self.frame.size.width / 2.0)) {
        return NO;
    } else {
        return YES;
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"CustomButton touchesBegan");
}

@end
// CustomView.m 文件
#import "CustomView.h"

@implementation CustomView


- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
//    NSLog(@"%@",NSStringFromCGPoint(point));
    float xValue = point.x - self.frame.size.width / 2.0;
    float yValue = point.y - self.frame.size.height / 2.0;
    if (xValue < 0) {
        xValue = -xValue;
    }
    if (yValue < 0) {
        yValue = -yValue;
    }
    if (xValue*xValue + yValue*yValue > (self.frame.size.width / 2.0) * (self.frame.size.height / 2.0)) {
        return NO;
    } else {
        return YES;
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"CustomView");
}

@end
// ViewController.m 调用文件,把项目中的注释代码去掉会有其他的一些发现。
#import "ViewController.h"
#import "CustomView.h"
#import "CustomButton.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor lightGrayColor];
//
    
//    eventView.clipsToBounds = YES;
//    eventView.layer.cornerRadius = 50;
    
    
    CustomButton *button = [CustomButton buttonWithType:UIButtonTypeCustom];
    [self.view addSubview:button];
    button.backgroundColor = [UIColor yellowColor];
    button.frame = CGRectMake(100, 100, 100, 100);
    
    [button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
    

    CustomView *eventView = [[CustomView alloc] initWithFrame:CGRectMake(25, 25, 50, 50)];
    [button addSubview:eventView];
}

- (void)buttonClicked:(UIButton*)btn {
    NSLog(@"此区域可以接收到点击事件");
}
@end
项目传送门:UIEventDemo(注意观察日志输出)
回顾系列文章下一篇:Objective-C 知识回顾的UI视图部分之二
回顾系列文章上一篇:Objective-C 知识回顾之背景前言