1.Http访问,创建CXHttpManager继承AFHttpSessionManager
- (void)sendHTTPGetRequest:(NSString *)URLString params:(NSDictionary *)params resultBlock:(CXHTTPManagerResultBlock)resultBlock;
- (void)sendHTTPPostRequest:(NSString *)URLString params:(NSDictionary *)params resultBlock:(CXHTTPManagerResultBlock)resultBlock;
- (void)sendHTTPPutRequest:(NSString *)URLString params:(NSDictionary *)params resultBlock:(CXHTTPManagerResultBlock)resultBlock;
- (void)sendHTTPDeleteRequest:(NSString *)URLString params:(NSDictionary *)params resultBlock:(CXHTTPManagerResultBlock)resultBlock;
然后创建CXNetworkRequestManager,持有CXHttpManager作为属性,对应不同的模块就是创建CXNetworkRequestManager的category,不同模块类似订单Order,资源Resource,一个模块一个category,在其中加对应的接口,调用CXHttpManger来实现请求,其中CXNetworkRequestManager中还持有Reachability对象监听网络情况,以及NetworkStatus获取不同状态码
最后创建NSObject的分类CXNetworkRequest,其中持有CXNetworkRequestManager(这里要用objc_setAssociatedObject来关联CXNetworkRequestManager对象),就可以在ViewModel中,直接调用对应的CXNetworkRequestManager对象来调用各种接口了。
2.圆角加载
dispatch_async(dispatch_get_global_queue(0, 0), ^{
UIImage *resizedImage = [image cx_imageScaleAspectToSize:inRect.size];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(inRect.size.width, inRect.size.height), NO, 0);
UIBezierPath *circlePath = [self circlePathWithRect:inRect];
[circlePath addClip];
[resizedImage drawInRect:inRect];
if (borderWidth != 0 && borderColor) {
[borderColor setStroke];
circlePath.lineWidth = borderWidth;
[circlePath stroke];
}
UIImage *circleImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
self.image = circleImage;
});
});
3.音频下载缓存
1)使用CXRecordDownloadManager管理所有下载文件,以CXRecordDownloadModel方式展示
将一个个音频文件先利用对应的CXRecordDownloadModel来存储信息,currentFileSize,totalFileSize,对应的资源信息,然后本地保存三个数组,分别是保存本地的录音文件数组,下载中的录音文件数组和下载后的录音文件数组,然后使用一个本地文件 沙盒路径/recordDownloadManagerConfigFileName 来保存本地的所有录音数组,再根据文件的currentFileSize和totalFileSize来判断是否下载完成,放置不同的数组。
当用户需要添加下载的时候,先请求对应链接,通过服务器反馈获取文件大小,从而生成RecordDownloadModel对象,然后添加到下载中数组中,同时更新将每个数组的数据(每个数据从model转成NSDictionary)然后写入本地文件(沙盒路径/recordDownloadManagerConfigFileName中),更新本地数据。
删除下载录音的时候,一样,a)先删除对应的文件。b)同时清理对应的数组,c)更新本地文件
2)使用CXRecordDownloadModel实现下载操作
通过
- (void)getFilesizeWithBlock:(FileSizeCompeletion)compeletion{
//开启新线程获取对应下载URL的response,然后从response的参数中获取文件大小
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.recordModel.recordLink]];
request.HTTPMethod = @"HEAD";
[request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
if(compeletion) {
// if(connectionError || data == nil || data.length == 0) {
if(connectionError || data == nil) {
CXLog(@"data :%@",data);
compeletion(nil);
}
compeletion(response);
}
}];
});
}
获取文件大小
持有NSURLConnection来实现断点下载
持有NSFileHandle来写入文件
使用UInt64 downloadFileEnd来保存文件的尾部
断点下载的代码
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.recordModel.recordLink]];
NSString *value = [NSString stringWithFormat:@"bytes=%lld-%lld", self.currentFileSize, self.downloadFileEnd];
[request setValue:value forHTTPHeaderField:@"Range"];
[request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
//开始下载(在上面已经配置好了断点下载)
//切换到其他线程
_downloadProgramURLConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[_downloadProgramURLConnection setDelegateQueue:[[NSOperationQueue alloc] init]];
[_downloadProgramURLConnection start];
// _downloadProgramURLConnection = [NSURLConnection connectionWithRequest:request delegate:self];
if(completion) {
completion();
}
在对应的delegate中,写入文件,更新状态等
#pragma mark - NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// NSLog(@"开始下载");
self.isDownloading = true;
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
if(self.currentFileSize >= self.totalFileSize) {
CXLog(@"self.currentFileSize >= self.totalFileSize");
return;
}
self.isDownloading = true;
//移动到文件的尾部
[self.writeHandle seekToFileOffset:self.currentFileSize];
//从当前移动的位置(文件尾部)开始写入数据
[self.writeHandle writeData:data];
//累加长度
self.currentFileSize += data.length;
//更新文件大小
[self.writeHandle truncateFileAtOffset:self.currentFileSize];
CXLog(@"self.currentFileSize = %lld , self.totalSize = %lld",self.currentFileSize,self.totalFileSize);
if(self.currentFileSize == self.totalFileSize) {
//下载完成
CXLog(@"download finished");
[[NSNotificationCenter defaultCenter] postNotificationName:kZBYRecordDownloadFinishedNotification
object:nil];
}
[[CXRecordDownloadManager defaultManager] updateDownloadProgramModel:self];
//回调更新UI
if([self.delegate respondsToSelector:@selector(downloadSizeDidChange:)]) {
[self.delegate downloadSizeDidChange:self];
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
//关闭连接(不再输入数据到文件中)
[self.writeHandle closeFile];
self.writeHandle = nil;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
}
4.启动页动画
UIView 动画,CABasicAnimation,CGAffineTransform,CAKeyframeAnimation,CAAnimationGroup都尝试过,类似网易云音乐那个
5.内存泄漏
用Command+I instrument里面的leak泄露,然后录制,查看leak处,查看相关代码,寻找原因
6.类似MJExtension的CXBaseModel
- (instancetype)initWithResponseObject:(NSDictionary *)responseObject error:(NSError **)error;
- (instancetype)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error;
+ (void)load {
//这里筛选出所有NSInteger的属性,并用runtime为CXBaseModel类添加validate:error:的方法
unsigned int propertiesCount = 0;
objc_property_t *properties = class_copyPropertyList([self class], &propertiesCount);
for (NSInteger i = 0; i < propertiesCount; i++) {
objc_property_t property = properties[i];
//属性类型
NSString *propertyType = @(property_copyAttributeValue(property, "T"));
BOOL isNSIntegerType = (strcmp([propertyType UTF8String], @encode(NSInteger)) == 0);
BOOL isNSArrayType = (strcmp([propertyType UTF8String], @encode(NSArray)) == 0);
if (!isNSIntegerType || !isNSArrayType) {
continue;
}
//获取属性名字,生成validate:error:的SEL
NSString *propertyName = @(property_getName(property));
//capitalizedString
NSString *validateMethodName = [NSString stringWithFormat:@"validate%@:error:", propertyName.capitalizedString];
if (isNSIntegerType) {
class_addMethod([self class], NSSelectorFromString(validateMethodName), (IMP)ZBYValidateNSInteger, "B^@^@");
continue;
}
if (isNSArrayType) {
class_addMethod([self class], NSSelectorFromString(validateMethodName), (IMP)ZBYValidateNSArray, "B^@^@");
}
}
}
在load的时候,使用方法添加,添加对应的方法
- (instancetype)initWithResponseObject:(NSDictionary *)responseObject error:(NSError *__autoreleasing *)error {
_JSONAdapter = [[CXJSONAdapter alloc] initWithModelClass:[self class]];
NSDictionary *modelDictionary = [_JSONAdapter modelDictionaryWithResponseDictionary:responseObject];
self = [self initWithDictionary:modelDictionary error:error];
return self;
}
- (instancetype)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
self = [self init];
if (!self) {
#if DEBUG
NSLog(@"%@ model is nil", _JSONAdapter.modelClass);
#endif
return nil;
}
for (NSString *key in dictionary) {
// Mark this as being autoreleased, because validateValue may return
// a new object to be stored in this variable (and we don't want ARC to
// double-free or leak the old or new values).
id value = [dictionary objectForKey:key];
if ([value isEqual:[NSNull null]]) {
value = nil;
}
BOOL success = ZBYValidateAndSetValue(self, key, value, YES, error);
if (!success) {
#if DEBUG
NSLog(@"%@ model is nil", _JSONAdapter.modelClass);
#endif
return nil;
}
}
return self;
}
这里有将model专成dictionary
- (NSDictionary *)cx_dictionaryTransformation {
NSMutableDictionary *modelMutableDictionary = [NSMutableDictionary dictionary];
unsigned int propertiesCount = 0;
objc_property_t *properties = class_copyPropertyList([self class], &propertiesCount);
for (NSInteger i = 0; i < propertiesCount; i++) {
objc_property_t property = properties[i];
NSString *propertyName = @(property_getName(property));
if([propertyName isEqualToString:@"superclass"] || [propertyName isEqualToString:@"hash"]
||[propertyName isEqualToString:@"debugDescription"]
||[propertyName isEqualToString:@"description"]) {
continue;
}
id propertyValue = [self valueForKey:propertyName];
if (!propertyValue) {
continue;
}
//如果是model类则继续转换
if ([[propertyValue class] isSubclassOfClass:[CXBaseModel class]]) {
propertyValue = [((CXBaseModel *)propertyValue) cx_dictionaryTransformation];
}
//如果是数组类则判断数组元素类型
if ([propertyValue isKindOfClass:[NSArray class]]) {
propertyValue = [self arrayTransformWithModelsArray:propertyValue];
}
modelMutableDictionary[propertyName] = propertyValue;
}
return [modelMutableDictionary copy];
}
实际就是用runtime获取model的一个个属性,然后便利出来获得key和value放进去dictionary,如果对应的value是一个model那么就继续递归,直到全部属性便利完,返回对应的字典即可
7.LimitInputNumberView
原理:在- (void)textViewDidChange:(UITextView *)textView中,获取对应的字符,这里做了一个温馨处理,如果是中文汉字(通过检测键盘输入模式 如果是中文输入),那么不会限制能否继续输入,直到你输入完成,我们再判断中文汉字是否超过字符长度,用一个持有的Bool值来判断,更新这个bool值,然后在
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text 这个接口中来判断用户可否继续敲入。
同时在textViewDidChange可以做剩余字数的UI更新
8.CXTextView
1)直接用一个UILabel 加在UITextView上,通过textViewDidChange来判断UILabel是否要显示
每个属性设置调用setNeedsLayout
2)在drawRect的时候,绘制界面
每个属性设置调用setNeedsDisplay
//- (void)drawRect:(CGRect)rect {
// if(self.hasText) return;
//
// NSMutableDictionary * attrs = [NSMutableDictionary dictionary];
// attrs[NSFontAttributeName] = self.font;
// attrs[NSForegroundColorAttributeName] = self.placeholderColor ? self.placeholderColor : [UIColor grayColor];
//
// CGFloat x = self.drawRectX;
// CGFloat w = rect.size.width;
// CGFloat y = self.drawRectY;
// CGFloat h = rect.size.height;
// CGRect placeholderRect = CGRectMake(x, y, w, h);
// [self.placeholder drawInRect:placeholderRect withAttributes:attrs];
//}
9.CXPhotoBrowser
在MWPhotoBrowser的前提下,封装好,对应的图片浏览器
10.音频播放器
使用AVPlayer来播放音频,使用AVPlayerItem来初始化播放器
2)首先初始化AVPlayer,然后监听AVPlayerItem 的status,如果是可以播放,就获取总时间长度,更新UI,同时通过持有id 类型的currentTimeObserver来更新滑动条的状态,同时设置音频为后台依旧可以播放,就类似于播放器那样,退回首页,还是可以听到音频
3)同时通过kvo监听AVPlayerItem的loadedTimeRanges属性,来更新缓冲条的UI,获取当前缓冲时长,计算缓冲的比例
4)下载成功后会更新Model的下载状态,通过kvo监听,判断如果下载完更新UI,提示用户下载音频完成。
最后就是在离开的时候,关闭timerObserver,avplayer=nil,avplayerItem=nil
实际测试 释放完全,没有大问题
11.视频播放器
其实是在音频的基础上
1)播放器的切换屏幕是
通过transform动画来实现旋转,同时更新约束来实现放大缩小效果。
也是通过kvo监听status来播放视频,loadedTimeRanges来更新缓冲进度,playbackBufferEmpty来判断缓冲的状态,playbackLikelyToKeepUp来判断当前是否在播放视频中
2)视频并没做下载缓存,不过应该和音频缓存一个道理