如何自己实现一个 UIAppearance

💡这是字节面试出的题目,当时连UIAppearance怎么使用的都不知道,没回答出来,感觉愧对江东父老,特反省一下,写一个 demo 大家看看对不对

代码地址可查看:https://github.com/irobbin1024/UIAppearance-demo

一. UIAppearance是什么

UIAppearance是一个可以给 App 进行定制主题的工具

传统上,如果我们要全局改变按钮的样式,例如背景色改为灰色,还是比较麻烦的,可以采取的方式,例如写一个 UIButton 的子类,在子类中进行变更。使用UIAppearance就比较简单了。看一下它的 API

1
[[UIButton appearance] setBackgroundColor:[UIColor grayColor] forState:UIControlStateNormal];

如上所示,一行代码就搞定了,我们要做的就是这个效果。

接下来看一下我的 API

1
[[MyView xy_appearance] setBackgroundColor:[UIColor redColor]];

二. 分析要点

要实现UIAppearance的效果,有两点要注意的

第一就是针对目标 API 的调用,例如setBackgroundColor,我们可以换成任意的一个属于UIButton的方法,例如setAlpha。

第二就是我们在配置好效果之后,到底怎么应用到 UI 上,这里我们不讨论系统到底是怎么实现的,根据我的思路就是hook UIView 的 addSubView:方法,在添加之后调用刚才配置的方法即可

三. 具体的实现

1. API 调用

这里我们采用欺骗的方式来实现,xy_appearance方法会返回一个 instanceType,对于编译器来说,他会认为返回的是一个MyView的实例,我们也就可以调用任何 MyView 的方法了,顺便说一下,MyView 是一个 UIView 子类

首先,我们定义一个协议XYUIAppearance,里面是xy_appearance方法,MyView 实现这个协议

在外部调用 MyView 的方法,例如setBackgroundColor的时候,我们将方法保存起来,在添加到父类的时候进行调用。

为了让这一步更简单,我使用了 NSProxy,NSProxy是一个代理类,可以实现对其方法调用的捕获,再转发给被代理的对象。我们可以在捕获之后,保存起来

接下来是具体的代码

XYUIAppearance.h 👇

1
2
3
4
5
@protocol XYUIAppearance <NSObject>

+ (instancetype)xy_appearance;

@end

MyView.h 👇

1
2
3
@interface MyView : UIView<XYUIAppearance>

@end

MyView.m👇

1
2
3
4
5
6
7
@implementation MyView

+ (instancetype)xy_appearance {
return (MyView *)[XYProxy proxyWithTarget:self];
}

@end

XYProxy.h👇

1
2
3
4
5
6
7
@interface XYProxy : NSProxy

@property (nullable, nonatomic, strong) id target;

+ (instancetype)proxyWithTarget:(id)target;

@end

XYProxy.m👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@interface XYProxy ()

@property (nonatomic, copy) NSString * className;

@end

@implementation XYProxy

- (instancetype)initWithTarget:(id)target {
_target = target;
self.className = NSStringFromClass([(NSObject *)target class]);
return self;
}

+ (instancetype)proxyWithTarget:(id)target {
return [[XYProxy alloc] initWithTarget:target];
}

- (id)forwardingTargetForSelector:(SEL)selector {
return nil;
}

- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
// 获取方法之后,进行保存
[[XYAppearanceManager instance] appendWithClassName:self.className invocation:invocation];
// [invocation setReturnValue:&null];
}

// ... 省略了其他代码

@end

2. 应用效果

这里就比较简单了,首先 hook UIView 的 addSubView 方法,然后找到对应的 NSInvocation 进行调用。

我这里写了一个类XYAppearanceManager完成NSInvocation保存任务,一个 UIView 的分类来完成 hook 的任务

可以看一下具体的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)hook_addSubview:(UIView *)view {
// 在这里可以添加你自定义的代码,对addSubview方法进行hook操作
NSLog(@"Hooked addSubview: %@", view);

// 调用原始的addSubview方法
[self hook_addSubview:view];

NSInvocation * invocation = [[XYAppearanceManager instance].invocationMap objectForKey:NSStringFromClass(view.class)];
if (invocation) {
invocation.target = view;
[invocation invoke];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@implementation XYAppearanceManager

+ (instancetype)instance {
static XYAppearanceManager * obj = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
obj = [[self alloc] init];
});
return obj;
}

- (NSMutableDictionary<NSString *,NSInvocation *> *)invocationMap {
if (_invocationMap == nil) {
_invocationMap = [NSMutableDictionary dictionary];
}
return _invocationMap;
}

- (void)appendWithClassName:(NSString *)calssName invocation:(NSInvocation *)invocation {
self.invocationMap[calssName] = invocation;
}

@end

一个StoreKit引发的异步访问Crash记录

一. 现象

我们某个App出现上述crash报告,目前只发现一例,内存访问异常引起

闪退位置:

img

闪退位置代码是在给一个数组进行复制,value是请求到的SKProduct对象。

二. Crash分析

XYStoreProductService类用于请求商品生成SKProduct对象,具体过程是使用SKProductsRequest,根据identifier发起一个请求,在回调里面获取SKProduct

1
2
3
4
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{

}

但是这个回调是异步的,在子线程中返回,返回时间依赖于当前手机的网络环境,可能会比较慢。

在回调中会往XYStore中的数组products添加SKProduct对象

1
2
3
4
- (void)addProduct:(SKProduct*)product
{
self.products[product.productIdentifier] = product;
}

添加动作是在子线程的,所以就可能出现同步问题,具体例子如下:

用户进入购买页会请求所有商品项,还没有返回成功的情况下,用户点击购买按钮,因为此时商品还没有返回,所以又会出发请求商品项的动作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SKProduct *product = [self productForIdentifier:identifier];
if (product) {
success(product);
return;
}

// 若内存中没有,网络获取
NSSet *set = [[NSSet alloc] initWithArray:@[identifier]];

[self requestProducts:set success:^(NSArray *products, NSArray *invalidProductIdentifiers)
{
if (products.count > 0) {
if (success) {
success(products.firstObject);
}
}
} failure:failure];

此时会同时出现两次商品请求,两次返回,两个线程,同时对products进行设置操作,就有几率导致内存访问异常而crash。

这个crash的概率极低,需要满足以下条件:

  1. 第一次的request返回很慢
  2. 用户购买的商品的时候,商品项不存在
  3. 两次请求几乎同时返回

三. 如何解决

对products数组的访问添加同步方法即可,示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
- (SKProduct*)productForIdentifier:(NSString*)productIdentifier
{
@synchronized (self) {
return self.products[productIdentifier];
}
}
- (void)addProduct:(SKProduct*)product
{
@synchronized (self) {
self.products[product.productIdentifier] = product;
}
}

杭州摩托车科目1到拿证

杭州摩托车科目2到拿证

继上篇杭州摩托车增驾报名流程之后,更新一下后续的科目1到科目4流程

科目1

科目1题目刷完,要联系驾校考一个结业坚定,地点是航模驾校里面,其实就是走一个形式,工作人员会给你张题目给你做,做对就可以拿成绩单走人,不对也没关系,多做几遍。

科目一考试地点在九堡汽车站里面二楼,上去就能看到了。不用预约,去了直接考。8:30上班,周末貌似休息,所以我当时是早上考完再回去上班的,那边有地铁口,还比较方便。

进去门口先拿号,一定要第一时间拿,不然就算会有很多人排在你前面。

然后去最里面的窗口,告知他是考摩托车的,给他结业坚定和身份证就可以了。

后面就是排队进场考试,这个时候刷身份证闸机是通不过的,因为这是给汽车的人用的,摩托车因为没有预约,所以刷不进,告诉工作人员就可以进去了。进去之后把手机什么的寄存起来,依次进入考试即可。




科目2 & 科目3

科目1考完之后,联系驾校,教练会加你微信并把你拉进一个群里,群里面的同学就是一个班的了,同一批次的考生,一起训练,一起考试。

训练地点离驾校不远,大龙驹坞一直往里面走就能看到了。

考三轮的同学比较悲催,总共只有三辆车,车款还很差,大家轮流使用。二轮的就很幸福了,基本人手一辆车,随时都可以练。

科目二是S线绕桩,半坡起步,单边桥,熟练之后就可以不用来了。

科目三不用练,等考试的时候教练教一下就可以了,简单说就是从起点绕个圈再回来,中途有红绿灯,按照灯的颜色要停车等待,掉头要打灯,最后是靠边停车,也要记得打灯,重新起步之后把车开回出发的地方就可以了。

考试地点就在驾校里面,考官回来两三个,关键位置会有人盯着,没有电子监控,考试过程还是比较轻松的,挂科的同学也比较少,通过率基本是90%以上。我是一次过的,所以不清楚补考流程。

考试通过之后,要去考官那边领一张科目4的考试时间预约单,在上面规定的日期去考试,不过根据我的经验,并不一定要按照上面的日期来,我没拿到第二天的考试单,但我还是去考试了,没什么问题,就说单子忘记带了。






科目4拿证

科目4的考试地点跟科目1不一样,在古墩路车管所,八点半开门,提早一点去,考完去上班,美滋滋。
记的要带上证件照,身份证,驾驶证,如果有的话。考试的地方在二楼,安全文明考试点。

杭州摩托车增驾报名流程

起因

杭州主城区从2017年开始禁止二轮摩托车上路(之前只是禁止三轮车),理由是我大杭州是美丽的花园城市,且要承办亚运会,摩托车会污染环境,所以一禁了之。2021年还要逐步禁止电瓶车,明年抓到骑电瓶车的可能就要扣车了。总之,手上有一本摩托车驾照还是比较保险的,最多就罚款,而且以后政策松动,也有可能会允许摩托车上路,驾照在手,心中不慌。

报名

杭州摩托车驾校很少,主城区就一家,位于天目山路的航模驾校。也只能选择这家了。

我有C1驾照,增驾D照,三轮的,两轮的是E照,没有特殊需求的推荐E,考试比较简单。我是打算以后没工作了,还可以骑着三轮车去送快递,优势很大有没有,而且D照向下兼容E照,能一次性解决的就解决肯定没错。

以下只讨论增驾情况,单独考D、E的流程可能不太一样。

报名可以先不用去驾校,直接去位于河坊街的车管所,体检+报名。

河坊街车管所星期天也上班,对上班族比较友好,我是星期天下午去的,人不多,体检不用排队,全程下来一个小时不到,就是步骤有点多。

报名要携带的东西:

  • 身份证
  • 居住证或者告知单
  • 证件照

大概步骤:

  1. 体检预约+缴费
  2. 体检
  3. 驾校面签
  4. 缴纳理论培训费
  5. 理论培训登记及人脸采集
  6. 登录理论培训账号

全程支持支付宝,无需准备现金。

###体检预约+缴费

当场预约体检即可,无需提前预约。一楼进来的自主体检缴费机器旁边贴着一张二维码,扫码即可预约。记得要预约当天的。

预约成功之后在缴费机上选择增驾类型和已有准驾类型,比如我是C1+D。然后缴费,获取体检单。

6DE545F5-CA88-4911-9E60-8F2999640DEA_1_105_c

###体检

体检很简单,总共6个项目,上二楼,按照上面的房间号进去检测,能听、能看就可以了。体检完去16、17号窗口录入信息。

驾校面签

16、17号窗口旁边就是航模驾校的窗口,直接过去,把身份证、居住证、一张证件照给她,她会帮你都弄好。窗口上粘贴的说明可以拍下来,后面参考。面签这里就要开始交钱了,D照是1880,E照不清楚,不过肯定更便宜。都弄完会给你一张报名表。

753E38D2-9D96-4AF9-93D3-6177D2FE4C08_1_105_c

F74F1B4C-AC59-423E-9420-347F699CF123

缴纳理论培训费

3楼有个财务室,去那里缴纳理论培训费,150块,缴费成功后小姐姐会给你一张付款票据。

理论培训登记及人脸采集

接下来回到2楼,楼梯口是进行理论培训登记和人脸采集的2010室。登记好不会有培训教材的,直接走吧。什么你问我150块干嘛用的???🤷‍♂️

登录理论培训账号

确认能够登录账号才算完,网址是 http://hz.5u5u5u5u.com ,账号是身份证号,密码是123456。能登录并观看视频就大功告成了。

iOS开发Tip之二

iOS开发Tip之二

  1. 如何给NSArray添加弱引用对象

    使用NSValue做一个包装,再添加到NSArray

    1
    NSValue * weakValue = [NSValue valueWithNonretainedObject:[NSObject new]];
  2. 将main.m 展开为C代码

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

  3. 在子线程里面开启runloop的话,必须要添加autoreleasepool,不然会引发内存泄漏

    每个子线程在开始的地方会有一个autoreleasepool,结束的时候清空pool,但是因为runloop导致线程无法结束,所以如果不手动添加pool,那么必然会内存泄漏

  4. 给一个可变对象的@property使用copy修饰,必须override这个属性,并用mutableCopy代替copy

  5. 如何调试EXC_BAD_ACCESS错误

    一般情况下是因为访问了已经被释放的对象造成的。这种对象称之为僵尸对象(Zombie Objects)。

    我们可以打开Scheme->Diagnostics中的Enable Zombie Objects选项。这样程序运行过程中所有引用计数为0的对象都不会被销毁,当对僵尸对象发送消息的时候就会触发错误,并在控制台中打印出相应的错误。

    以下是模拟僵尸对象的代码

    1
    2
    3
    4
    5
    NSString * str = [NSString stringWithCString:"hello world" encoding:NSUTF8StringEncoding];
    __unsafe_unretained NSString * zombie = str;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [zombie uppercaseString];
    });

    打开Enable Zombie Objects选项之后终端输出为:

    1
    2
    objc[57939]: Class _NSZombie_OS_xpc_endpoint is implemented in both ?? (0x60c0000495d0) and ?? (0x60c0000497e0). One of the two will be used. Which one is undefined.
    2018-08-08 23:08:21.682727+0800 demo1[57939:39012491] *** -[CFString uppercaseString]: message sent to deallocated instance 0x60c00003b3a0
  6. 用@property写一个单例,逼格更高

    1
    @property (nonatomic, class, readonly, nonnull) SDImageCachesManager *sharedManager;

    实现和平常一样

    1
    2
    3
    4
    5
    6
    7
    8
    + (SDImageCachesManager *)sharedManager {
    static dispatch_once_t onceToken;
    static SDImageCachesManager *manager;
    dispatch_once(&onceToken, ^{
    manager = [[SDImageCachesManager alloc] init];
    });
    return manager;
    }

    用法

    1
    [SDImageCachesManager.sharedManager addCache:newCache];
  7. 绝对禁止给Category覆盖父类方法

    因为Category中的方法无法调用父类方法,所以会造成不可预料的覆盖。理论上Category只能新增方法。

开发Tip之一

iOS 开发Tip之一

  1. 获取当前执行函数的名字

    1
    NSStringFromSelector(_cmd)

    _cmd 可以拿到当前函数的Selector

  2. 数组的最大值、最小值、求和快捷操作

    1
    2
    3
    [@[@(1), @(2), @(3)] valueForKeyPath:@"@ max.self "]; // 最大值
    [@[@(1), @(2), @(3)] valueForKeyPath:@"@ min.self "]; // 最小值
    [@[@(1), @(2), @(3)] valueForKeyPath:@"@ sum.self "]; // 求和
  3. 几种空指针

    1
    2
    3
    4
    NULL // C和C++里面的空地址
    nil // OC对象空指针
    NSNull // 表示为空的对象
    Nil // 类对象空指针
  4. 结构体快捷构建

    1
    CGRect visibleRect = (CGRect){.origin = {100,100}, .size = self.view.frame.size};
  5. 给协议添加属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // step 1 添加一个属性申明
    @property (nonatomic, strong) NSString * object;

    // step 2 添加一个静态属性作为标识(静态属性地址唯一)
    static char * objectIdentifier;

    // step 3 添加get方法
    - (NSString *)object {
    return objc_getAssociatedObject(self, &objectIdentifier);
    }

    // step 4 添加set方法
    - (void)setObject:(NSString *)object {
    objc_setAssociatedObject(self, &objectIdentifier, object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    不需要为了KVO而手动调用 willChangeValueForKey:, didChangeValueForKey: 方法,这两个方法底层会自动调用,可以正常KVO

  6. __block关键字添加的属性要小心初始值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    __block BOOL isGoodDay;

    dispatch_async(dispatch_get_main_queue(), ^{
    if (isGoodDay) {
    NSLog(@"today is good day");
    }
    });

    // isGoodDay在某些机型上有可能是YES

    解决办法是所有的属性,申明的时候必须初始化值

  7. 不要给NSString设置strong修饰符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @property (nonatomic, strong) NSString * name;

    NSMutableString * str = [NSMutableString stringWithFormat:@"jack"];
    self.name = str;

    NSLog(@"name is %@", self.name);

    [str appendString:@" tome"];

    NSLog(@"name is %@", self.name);

    // 输出:
    /*
    2017-12-12 22:49:26.221790+0800 TouchDemo[66149:20611122] name is jack
    2017-12-12 22:49:26.221890+0800 TouchDemo[66149:20611122] name is jack tome
    */

    类型定为NSString,通常来说是不希望值改变的,但事实上值改变了,如果真的需要mutable的特性,最好把类型定为NSMutableString,否则使用copy修饰符

  8. 暂停和恢复UIView动画

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    - (void)pauseAction:(id)sender {
    CFTimeInterval time = [self.animView.layer convertTime:CACurrentMediaTime() fromLayer:nil];
    self.animView.layer.timeOffset = time;
    self.animView.layer.speed = 0.0f;
    }

    - (void)resumeAction:(id)sender {
    if (self.animView.layer.speed > 0.0f) {
    return ;
    }
    CFTimeInterval pauseTime = self.animView.layer.timeOffset;
    CFTimeInterval timeSincePause = CACurrentMediaTime() - pauseTime;
    self.animView.layer.timeOffset = 0;
    self.animView.layer.beginTime = timeSincePause;
    self.animView.layer.speed = 1;
    }
  9. 去掉UITableView因为数据不够多出来的Cell和横线

    1
    self.tableView.footerView = [UIView new];
  10. 点击任意位置收起键盘

1
2
3
4
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
[self.view endEditing:YES];
}

需要设置view的userInteractionEnabled为YES(默认就是YES)

  1. UIButton 对齐

    UIButton 默认是垂直、横向居中对齐的,有时候达不到我们的要求,这时候可以通过设置 contentHorizontalAlignmentcontentVerticalAlignment 属性来改变对齐方式

    1
    button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight;

AVPlayer缓存-OTPlayerCache的介绍

AVPlayer缓存-OTPlayerCache的介绍

##0x00 AVPlayer 实现缓存的两种方式

AVPlayer 实现缓存主要有两种方式,第一种是在 App 内置一个 HTTP 服务器,采用代理的方式实现缓存,这种实现方案需要引入第三方库,实现上也较为复杂,优点是通用性强,不局限于 AVPlayer 的缓存,理论上支持任意播放器,甚至是接口请求和图片的缓存都可以做。

Read More

生成资源bundle的流程

生成资源bundle的流程

组件化之后,每个模块作为一个pod被引用,那么资源放在一个bundle里面会比较方便。资源包括图片、视频、xib编译之后的nib、storyboard、国际化字符串等文件。

xib和storyboard是需要编译的,所以我们需要建立一个可编译的bundle。理论上来说就算当时没有xib和storyboard,为了以后可能会引入这些文件,我们也应该用编译型bundle,有备无患。

1. 添加resource target

进入target list 列表页面,点击加号

Read More

in工作写的一些组件

我写的一些组件

[TOC]

RBAirSandbox - 沙盒展示组件

基于开源库RBAirSandbox

  • 添加了长按压缩文件夹发送到Mac功能
  • 添加了图片浏览、文本浏览
  • 计划添加更多功能

INPopKit - 弹窗组件

INPopKit是一套弹窗框架,程序中所有使用弹窗的地方,理论上都应该使用INPopKit,也能够使用INPopKit来显示所有的自定义弹窗,自定义你需要的View、遮挡级别、StatusBar的颜色等。你也可以自定义出现的动画和动画时间。

  • 通过协议、继承、抽象类使得整个框架非常灵活,容易定制
  • 可以选择使用模糊背景,或者灰色背景
  • 自由使用出现、消失的动画,包含但不局限于飞入、飞出、抖动、缩放、渐变
  • 自由使用window或者UIView的形式显示弹窗
  • 定制顶部信号条StautsBar的显示颜色
  • 自由选择使用AutoLayout或者Frame布局
  • 连续出现多个弹窗的情况,可以在不改变背景的情况下依次弹出
  • 框架内部处理键盘弹出时自动上移的操作
  • 可以控制点击外部自动隐藏功能
  • 抽象类功能强大,可以处理绝大多数从底部弹出的弹窗业务,比如分享菜单,列表选择等

可以从in app中找到INPopKit的使用例子:

  • 列表选择:发图->继续->点击关闭
  • 居中弹窗:in记->选择图片->右上角更多->点击删除
  • 分享弹窗:in记->选择图片->右上角更多
  • 处理键盘:关注->更多->举报->其他

INImageView - 图片显示组件

设计目标:app中所有显示图片的地方都是用这个组件

为什么:组件可以优化性能,包括处理圆角,缩放,gif播放,frame帧播放,手势处理(单机,双击),标签处理,网络下载和本地加载,webp处理,下载进度,失败placehold,

INGifView - Gif展示组件

  • 封面图显示
  • 下载进度展示
  • 加载本地或者网络gif图片
  • 指定播放帧
  • 暂停和恢复播放
  • 重新播放功能

INVideoView - 视频播放器

实现了一套和业务无关的视频播放组件,基础播放组件INVideoView不包含任何控件,所有业务处理逻辑由依附于INVideoView之上的一个我称之为XXControlView的subview提供,所有的代理也会给XXControlView。这样就实现了完全解耦,播放也展示分离。

实现了一套缓存逻辑,包含两部分,一部分处理AVPlayer,另一部分实现了本地的缓存逻辑和缓存策略。

  • JYVideoPlayerView 是跟业务无关的播放器,INVideoView是跟业务有关的播放器
  • 播放本地或者网络视频,提供基本功能,比如播放,暂停,获取视频长度和视频size
  • 缓存已经播放过的视频
  • 精细的缓存管理策略,比如获取缓存大小、控制最大缓存限制、清除老旧文件、根据URL来获取视频源文件、根据URL来清除视频文件等等
  • 控制系统音量调节的显示,因为系统音量视图会挡住视频内容,所以INVideoView在播放的时候可以选择自定义音量显示方式
  • 插拔耳机暂停和恢复播放
  • 程序前后台暂停和恢复播放
  • 全屏播放功能

具体代码可以参见开源库https://github.com/irobbin/OTPlayerCache

INSlideView - 小in片轮播组件

类似于幻灯片播放,有两种样式。

第一种:缩放、移动、渐变混合

第二种:快速变换预览

小in片的动画看似简单,实则逻辑复杂,所有的逻辑都封装在INSlideView中,只需要传递图片和播放方式即可。

  • 首先播放暂停和视频一样,推到后台和回到前台需要暂停播放
  • 滑动列表需要暂停播放,手指移开继续播放
  • 先下载所有的图片,才会开始播放。第一张图片优先下载,然后先显示到View中
  • 有下载进度显示功能,此时会有灰色朦层显示,用户无法操作
  • 滑动距离和速度计算较为复杂,还有上下左右的动画不能连续执行,比如上次是向左,下一张必须是向右
  • INSlideView提供清空内存功能,用来清除不用的图片,释放内存
  • 使用POP做动画
  • INSlideView代码行数高达1015行

INOption - 测试菜单组件

这是一个内部使用的组件,主要用来提供测试菜单项目,例如

  • CPU、内存、网络信息
  • 服务器类型选择,QA或者线上
  • LogCatcher的服务器地址
  • 清除App的所有缓存,用来初始化App,而不需要重新安装
  • 请求是否需要开启加密
  • 重新登录别的账号
  • 添加网络请求的额外参数
  • 可以自由接入其他测试菜单项
  • 与此配合使用的是一个Mac软件,LogCatcher

LogCatcher - Mac端Log抓取App

LogCatcher的主要作用是抓埋点和Socket通信,因为这两项在Charles中都比较难以抓取。

除此之外LogCatcher也可以抓取所有经过INLogSocketManager发送的分级Log信息,且可以在LogCatcher中进行简单过滤,是一个非常好用的小工具。

INLogSocketManager是一个分级的、封装的很好的Log系统,不过暂时只和LogCatcher进行协议配合。

INFlowLayoutView - 横排组件

一个横向布局组件,类似UICollectionView,但是比UICollectionView轻量级,和业务相关。

实现的功能是所有的subView会像写字一样从左往右依次排开,不够显示的换行。

可以在in,路径 发图->…->保存页面->话题选择(#号按钮)->热门话题下面找到

实现细节:

主要是计算,有一个二维数组linesData保存每一行,每一行中有若干个view信息,根据这个二维数组可以计算得出INFlowLayoutView的宽高,每一行中的每一个元素都保存着view,和frame,有了这些信息就可以把所有的view布局出来。

计算规则:计算行内元素的时候使用最大吞并原则,先把当前元素加进来,然后计算此元素的frame,如果frame超过了宽度限制,那么另起一行,for循环倒退一个元素

INLineLayoutView - 单排组件

这是一个横向布局组件,API简单,只需要简单往里面添加View,那么就可以将所有的subView居中显示

原理:复写addSubview方法,每添加进来一个元素,会重新进行居中布局,当然你需要设置间距属性margin

INPhotoLayoutView - 图片布局组件

INPhotoLayoutView组件主要是为了应付产品上多变的图片布局形式而创造的。

理论上INPhotoLayoutView可以应对所有的图片布局

实现原理是调用者提供一个布局类INPhotoLayoutModel过来,这个类提供了每张图片的frame,和布局的高度,这样INPhotoLayoutView就可以根据这些信息来精确布局

INPickerView - 选择组件

这是最近在同城趴App中写的一个组件。属于对UIPickerView的封装,使其更加易于使用和符合同城趴的UI设计标准。

主要功能有:

  • 日期选择
  • 男女选择(可扩展为列表选择,自定义列表项目)
  • INLocationPickerView省市区选择组件通过子类化INPickerView而实现

截图展示

RBAirSandbox

IMG_1412

IMG_1414

屏幕快照 2018-01-14 下午11.04.37