首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >六天完成一个简单iOS App - 第三天

六天完成一个简单iOS App - 第三天

作者头像
xx_Cc
发布2018-05-10 11:37:34
8640
发布2018-05-10 11:37:34
举报

第三天任务:

今天主要任务完成我的模块的搭建。

  1. 我的页面的搭建
  2. 清除缓存功能
  3. 方法抽取总结

我的页面的搭建

我们先来看一下我的界面内容

我的界面分析

通过上面图片可以看出,我的界面是一个非常简单的tableView,上面两个cell只需要简单设置图片,文字和最右边箭头就可以了,主要是最下面方块view的显示。这里我们有两种解决方案

一:可以是一个cell,如果最后一个是一个cell,稍微有些麻烦,因为最后一个cell比较特殊,需要与前两个cell区分,没有办法统一设置。

二:可以是一个tablefootView,这种方法比较简单,我们直接自定义view显示自己想要显示的内容,然后添加到tablefootView上面就可以了。

创建自定义view CLMeFooterView。首先分析,CLMeFooterView需要有哪些功能

  1. 请求数据,本着面向对象,谁的任务谁来负责的基本原则,我们将数据请求写在CLMeFooterView中
  2. 布局子控件,CLMeFooterView只管布局子控件和添加点击事件即可,至于子控件中的内容和字体大小颜色等等,都让子控件自己去管理,另外CLMeFooterView的宽度是固定的但是需要根据子控件的多少来设置自己的长度。
  3. 点击事件的实现,需要根据模型参数的不同,判断是调到其他界面还是进行http请求

我们通过重写CLMeFooterView的initWithFrame方法,在initWithFrame方法中请求数据和布局子控件。

代码中使用AFN来请求数据,使用MJExtension对数据进行对模型的转换。在请求数据时,可以现在请求成功之后,将服务器返回的数据写到plist文件中存放到桌面,这样便于我们对返回数据层次结构的理解和里面数据的查阅

// 写出plist文件到桌面 便于我们看
// [responseObject writeToFile:@"/Users/yangboxing/Desktop/me.plist" atomically:YES];

查看写在桌面的plist文件,

返回数据plist文件

返回数据plist文件

通过观察我们发现square_list中我们只需要用到icon , name ,url,三个属性就可以了其他的属性并不用到,也就没有必要去浪费内存存储用不到的数据,据此创建CLMeSquare模型,至于数据转模型MJExtension内部已经帮我们实现。

来看一下请求数据代码

// 参数
NSMutableDictionary *params = [NSMutableDictionary dictionary];
params[@"a"] = @"square";
params[@"c"] = @"topic";

[[AFHTTPSessionManager manager]GET:@"http://api.budejie.com/api/api_open.php" parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, NSDictionary *  _Nullable responseObject) {
  // 将字典中square_list对应的数据转化为模型数组
    NSArray *squares = [CLMeSquare mj_objectArrayWithKeyValuesArray:responseObject[@"square_list"]];
    [self createSquare:squares];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
    CLLog(@"请求失败");
}];

关于AFN的使用请参考iOS-网络编程(三)AFNetworking使用

而MJExtension内部通过RunTime来进行字典转模型,与KVC不同的是,RunTime字典转模型实现原理是遍历模型中的所有属性名,然后去字典查找,也就是以模型为准,模型中有哪些属性,就去字典中找那些属性。当服务器返回的数据过多,而我们只使用其中很少一部分时,没有用的属性就没有必要定义成属性了。

数据请求成功接下来就是子控件的布局,子控件的布局就是很简单的九宫格布局,需要注意的一点是,我们需要设置footView的高度就等于最后一个子控件的最大Y值,并且在tableView中,cell显示完毕后,在最低端会多出20的距离。如下图:

20的距离

解决的方法非常简单,当设置完footView的高度之后,拿到tableView重新刷新一下tableView就可以了

// 布局子控件
-(void)createSquare:(NSArray *)squares
{
    NSUInteger count = squares.count;
    int maxColsCount = 4;
    CGFloat buttonW = self.cl_width / 4;
    CGFloat buttonH = buttonW;
    for (int i = 0; i < count; i ++) {
        CLMeSquare *square = squares[i];
        CLMeSquareButton *button =[CLMeSquareButton buttonWithType:UIButtonTypeCustom];
        [button addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];
        // 设置button的模型属性
        button.square = square;
        // 设置button的frame
        button.cl_x = (i % maxColsCount) * buttonW;
        button.cl_y = (i / maxColsCount) * buttonH;
        button.cl_width = buttonW;
        button.cl_height = buttonH;
        button.backgroundColor = [UIColor whiteColor];
        [self addSubview:button];
    }
    self.cl_height = self.subviews.lastObject.cl_bottom;
    UITableView *tableView = (UITableView *)self.superview;
    tableView.tableFooterView = self;
    [tableView reloadData];  // 重新刷新数据也会重新计算 contentSize 就不会在最后在增加20了。
}

而子控件的内容由子控件自己来设置,每一个子控件为正方形,可以显示图片文字,并且有点击事件,所以子控件可以使用Button。

创建自定义控件CLMeSquareButton,重写layoutSubviews来布置button中imageView和titleLabel的位置

-(void)layoutSubviews
{
    [super layoutSubviews];
    // 修改button 内imageView 和 label的位置
    self.imageView.cl_y = self.cl_height * 0.15;
    self.imageView.cl_width = self.cl_width * 0.5;
    self.imageView.cl_height = self.imageView.cl_width;
    self.imageView.cl_centerX = self.cl_width * 0.5;
    
    self.titleLabel.cl_x = 0;
    self.titleLabel.cl_y = self.imageView.cl_bottom;
    self.titleLabel.cl_width = self.cl_width;
    self.titleLabel.cl_height = self.cl_height - self.imageView.cl_bottom;
}

initWithFrame方法中设置button字体大小,颜色,居中,背景图片等。

-(instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        self.titleLabel.textAlignment = NSTextAlignmentCenter;
        self.titleLabel.font = [UIFont systemFontOfSize:15];
        [self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
        
        // 如果没有提供图片我们可以设置buton width height 分别减1;
        [self setBackgroundImage:[UIImage imageNamed:@"mainCellBackground"] forState:UIControlStateNormal];
    }
    return self;
}

另外,因为点击CLMeSquareButton,我们要拿到模型中的url,进行跳转或者http请求,所以给button添加一个CLMeSquare模型属性,并且可以通过CLMeSquare的set方法来给CLMeSquareButton中imageView和titleLabel赋值

-(void)setSquare:(CLMeSquare *)square
{
    // 通过bubtton 的属性 square的set方法,拿到square后给button的图片和文字赋值 。
    _square = square;
    // 设置所有button的图片和文字
    [self setTitle:square.name forState:UIControlStateNormal];
    [self sd_setImageWithURL:[NSURL URLWithString:square.icon] forState:UIControlStateNormal placeholderImage:[UIImage imageNamed:@"header_cry_icon"]];
}

最后就是按钮点击事件的实现,需要对参数url进行判断,根据不同的url进行不同的操作,如果是mod开头的就跳转到其他界面,如果是http开头的就需要加载网页。

  1. 对开头字母的判断
// 判断是否以http开头   
[square.url hasPrefix:@"http"]
//延伸: [square.url hasSuffix:@"http"] 判断是否以http结尾
  1. 如何加载webViewController,并跳转到webViewController 加载webViewController很简单,创建webViewController并将url赋值给他即可。
CLWebViewController *webVc = [[CLWebViewController alloc]init];
webVc.url = square.url;

在自定义的footView中,跳转到webViewController,首先需要拿到NavgationController通过push方法进行跳转,如果想拿到NavgationController,需要拿到tabBarController,tabBarController的selectedViewController,即可拿到当前选择的NavgationController,而tabBarController我们可以通过窗口的跟控制器拿到。

UITabBarController *tabBarVC = (UITabBarController *)self.window.rootViewController;
UINavigationController *naVC = tabBarVC.selectedViewController;
webVc.navigationItem.title = square.name;
[naVC pushViewController:webVc animated:YES];
  1. 另外iOS9之后引入#import <SafariServices/SafariServices.h>可以使用系统的Safari来进行网页加载,并且功能非常齐全,可以前进,后退,刷新还可以显示进度条。但是注意:只有mode出来的Safari才会显示进度条等控件。
SFSafariViewController *webView = [[SFSafariViewController alloc]initWithURL:[NSURL URLWithString:square.url]];
UITabBarController *tabBarVC = (UITabBarController *)self.window.rootViewController;
[tabBarVC presentViewController:webView animated:YES completion:nil];

此时,整个界面基本上已经完成了,接下来完成点击又上角设置按钮,进入设置界面,清除缓存功能。

清除缓存功能

首先来看一下设置界面

设置界面

首先设置界面涉及到两种不同类型cell共存的问题,很明显第一行清除缓存cell与下面的cell类型不同,如果所有cell放到同一个缓存池中,当清除缓存cell复用到下面的cell时,需要去掉右边箭头,当清除缓存cell重新加载时,又需要加上右边箭头,并且清除缓存内部是需要做清除缓存功能的,而其他cell不需要这个功能,所以当一个cell是特有的,与其他cell不一样,业务逻辑也需要被独立的封装起来,为了避免复杂重复的操作,这种cell最好独立出来,并且不要循环给别的cell。

我们通过使用两种独立类型的cell,使用不同的标识来区分两种cell,一种标识就对应一种cell 通过一种标识来找一种cell的时候,如果没有那么创建一个cell,通过另外一种标识来找cell 的时候,就会创建另外一种cell,如果缓存池中有就去自己标识的缓存池中取。

由此类推多种不同的cell,对应多种不同的标识。每种类型的cell,创建并缓存到自己对应标识的缓存池中。

这里设置界面自定义两种cell,清除缓存的CLClearCacheCell,其他类型的CLSettingCell,两种cell都需要进行注册

static NSString * const CLClearCacheCellId = @"CLClearCacheCell";
static NSString * const CLSettingCellId = @"CLSettingCellId";

// 注册cell
[self.tableView registerClass:[CLClearCacheCell class] forCellReuseIdentifier:CLClearCacheCellId];
[self.tableView registerClass:[CLSettingCell class] forCellReuseIdentifier:CLSettingCellId];

当使用时,按照不同的行区分需要显示的不同类型的cell

// 按照不同的标识 重用不同的cell
// 取出cell,这里第0行是清除缓存cell,其他行是其他cell
if (indexPath.row == 0) {
    CLClearCacheCell *cell = [tableView dequeueReusableCellWithIdentifier:CLClearCacheCellId];
    return cell;
}else{
    CLSettingCell *cell = [tableView dequeueReusableCellWithIdentifier:CLSettingCellId];
    cell.textLabel.text = @"haha";
    return cell;
}

另外,我们需要给CLClearCacheCell添加tap手势,确保缓存文件大小计算完毕之后,才可以点击CLClearCacheCell清除缓存,当给cell添加tap手势之后,就会自动覆盖cell的代理方法tableView: didSelectRowAtIndexPath

接下来是将清除缓存业务逻辑封装到CLClearCacheCell中,首先清除缓存是清除沙盒中Caches中的文件,并且通过代码删除是不可逆的。来看一下沙盒中Caches文件内容

沙盒中Caches文件内容

其中custom是我们自己创建的用来缓存的文件夹,default是SD创建的图片缓存文件,我们需要将这两个文件夹内容大小计算出来,计算文件夹的大小,本质上就是遍历文件夹里面所有文件并计算文件大小,最后累加计算出文件夹总的大小。之后就是清除缓存,清除缓存的本质就是删掉这两个文件,并重新创建新的文件夹。

SD提供了计算dufault文件大小和删除文件的方法。引入#import <SDImageCache.h>

// 获取文件大小
[SDImageCache sharedImageCache].getSize
// 
// clear清除所有图片文件 
[[SDImageCache sharedImageCache] clearDiskOnCompletion:^{
    // 清除之后要做的事儿
}];
// clearn 只清除时间超过一周的文件
[[SDImageCache sharedImageCache] cleanDiskWithCompletionBlock:^{
    // 清除之后要做的事儿
}];

接下来我们要仿照SD清除缓存的内部实现来做我们自己创建的文件custom的清除缓存功能。首先计算文件大小

// 总大小
unsigned long long size = 0;
// 获取缓存文件路径
NSString *cachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject;
// 拼接全文件路径
NSString *filePath = [cachesPath stringByAppendingPathComponent:@"custom"];
// 创建文件管理者
NSFileManager *manager = [NSFileManager defaultManager];
// 使用遍历器获得custom文件下所有文件的子路径
NSDirectoryEnumerator *enumerator = [manager enumeratorAtPath:filePath];
for (NSString *subpath in enumerator) {
    // 拼接成完整路径
    NSString *fullParh = [filePath stringByAppendingPathComponent:subpath];
    // 获取文件属性字典
    NSDictionary *attribute = [manager attributesOfItemAtPath:fullParh error:nil];
   // 累加文件大小
    size += attribute.fileSize;
}

也可以通过获得子路径数组进行遍历

NSArray *subpaths = [manager subpathsAtPath:filePath];
for (NSString *subpath in subpaths) {
    // 拼接成完整路径
    NSString *fullParh = [filePath stringByAppendingPathComponent:subpath];
    // 获取文件属性字典
    NSDictionary *attribute = [manager attributesOfItemAtPath:fullParh error:nil];
    //size += [attribute[NSFileSize] unsignedIntegerValue];
    // fileSize方法返回的就是 NSFileSize 对应的key
    //累加文件大小
    size += attribute.fileSize;
}

删除costum文件并重新创建空的文件夹

#define CLCustomCacheFile [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:@"custom"]
NSFileManager *manager = [NSFileManager defaultManager];
// 删除文件
[manager removeItemAtPath:CLCustomCacheFile error:nil];
// 创建文件 withIntermediateDirectories:YES 表示如果没有中间文件会自动创建,NO 表示不会自动创建中间文件,如果发现没有文件则不会创建
[manager createDirectoryAtPath:CLCustomCacheFile withIntermediateDirectories:YES attributes:nil error:nil];

注意:计算文件大小和删除文件并重新创建都数据耗时操作,要放到子线程中去执行。

自定义CLClearCacheCell还有一些其他的逻辑需要注意。

  1. 等设置完文字之后在禁止cell点击,如果直接禁止点击,字体颜色会被渲染成灰色,文件大小计算完毕之后在开启点击。
  2. 先显示正在计算的小菊花,等计算完毕之后关闭小菊花,显示箭头,这里有一个注意点,accessoryView比accessoryType优先级要高,所以显示箭头的时候,需要先将accessoryView至为空然后在设置accessoryType。并且当正在计算时,将第一行cell滑出屏幕,在返回时发现小菊花已经不在了,我们可以通过重写cell的layoutSubviews,重新设置cell小菊花start,因为每当cell显示的时候都会调用layoutSubviews方法。
  3. 计算文件大小,显示在cell上,根据不同的大小显示不同的单位GB,MB,KB等。
  4. 点击cell清除缓存,可以先清除SD的图片缓存,SD缓存清除完毕之后在,在开子线程清除其他文件的缓存,之后在回到主线程刷新cell的内容。
  5. cell的销毁时刻,当进入设置控制器,正在计算文件大小时,返回,此时设置控制器已经被销毁。但是cell会等子线程任务执行完毕之后才会被销毁,因为还要用到cell且block中强引用了strong的对象,所以不会让cell销毁。所以在block中使用弱引用,block内部就不会对那个对象产生强引用。其该释放的时候就会被释放,虽然已经释放,但是代码还是会往下面执行,此时对象为空。
  6. 点击清除按钮的时候使用SVProgressHUD弹出提醒框,清除完毕之后关闭提醒框。

理解了这些来看一下CLClearCacheCell的代码

#define CLCustomCacheFile [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:@"custom"]

@implementation CLClearCacheCell

-(instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
        self.textLabel.text = @"清除缓存(正在计算文件大小...)";
        // 等设置完文字之后在禁止点击,如果直接禁止点击 字体颜色会被渲染成灰色
        self.userInteractionEnabled = NO;
        // 设置小菊花
        UIActivityIndicatorView *indicatorView =[[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
        [indicatorView startAnimating];
        self.accessoryView = indicatorView;
        // 创建弱引用对象
        __weak typeof(self) weakSelf = self;
       // 开启子线程计算文件大小
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
              //  mac中换算MB 除以1000,并不是以1024为单位
              // 总大小
              unsigned long long size = 0;
              NSArray *subpaths = [manager subpathsAtPath:CLCustomCacheFile];
              for (NSString *subpath in subpaths) {
                  // 拼接成完整路径
                  NSString *fullParh = [filePath stringByAppendingPathComponent:subpath];
                  // 获取文件属性字典
                  NSDictionary *attribute = [manager attributesOfItemAtPath:fullParh error:nil];
                  // 累加文件大小
                  size += attribute.fileSize;
        }
        size =  size+ [SDImageCache sharedImageCache].getSize;
            // 判断cell是否已经被销毁,如果销毁了就直接返回
            if (weakSelf == nil) {
                return ;
            }
            NSString *sizeText = nil;
            if (size >= pow(10, 9)) {
                sizeText = [NSString stringWithFormat:@"%.1fGB",size / 1000.0 / 1000.0 / 1000.0];
            }else if (size >= pow(10, 6)){
                sizeText = [NSString stringWithFormat:@"%.1fMB",size / 1000.0 / 1000.0];
            }else if (size >= pow(10, 3)){
                sizeText = [NSString stringWithFormat:@"%.1fKB",size / 1000.0];
            }else{
                sizeText = [NSString stringWithFormat:@"%zdB",size];
            }
            NSString *text = [NSString stringWithFormat:@"清除缓存(%@)",sizeText];
            // 回到主线程刷新cell
            dispatch_async(dispatch_get_main_queue(), ^{
                weakSelf.textLabel.text = text;
                weakSelf.accessoryView = nil;
                weakSelf.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
                weakSelf.userInteractionEnabled = YES;
                // 等计算完缓存大小之后在添加手势,保证正在计算过程中cell 点击无效
                [weakSelf addGestureRecognizer:[[UITapGestureRecognizer alloc]initWithTarget:weakSelf action:@selector(celltap:)]];
            });
        });
    }
    return self;
}
// cell点击手势
-(void)celltap:(UITapGestureRecognizer *)tap
{
    [SVProgressHUD showWithStatus:@"正在清除缓存"];
    // 清除所有图片文件 clearn 只清除时间超过一周的文件
    [[SDImageCache sharedImageCache] clearDiskOnCompletion:^{
        // 清除之后要做的事儿
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSFileManager *manager = [NSFileManager defaultManager];
            [manager removeItemAtPath:CLCustomCacheFile error:nil];
            //withIntermediateDirectories:YES ,表示中间文件如果没有会自动创建,NO 也不会自动创建中间文件,如果发现没有文件则不会创建
            [manager createDirectoryAtPath:CLCustomCacheFile withIntermediateDirectories:YES attributes:nil error:nil];
            dispatch_async(dispatch_get_main_queue(), ^{
               // 隐藏指示器
                [SVProgressHUD dismiss];
                self.textLabel.text = @"清除缓存(0B)";
            });
        });
    }];
}
// 每当cell 重新显示在桌面上 ,都会调用laoutsubviews
-(void)layoutSubviews
{
    [super layoutSubviews];
    UIActivityIndicatorView *indicator = (UIActivityIndicatorView *)self.accessoryView;
    [indicator startAnimating];  
}
@end

方法抽取总结

获取文件大小需要经常用到的,可以通过给NSString添加分类方法将获取文件大小的方法抽取出来,使用文件路径直接调用fileSize方法即可获得文件大小。

NSString+CLExtension.h

#import <Foundation/Foundation.h>
@interface NSString (CLExtension)
-(unsigned long long)fileSize;
@end

NSString+CLExtension.m

#import "NSString+CLExtension.h"

@implementation NSString (CLExtension)
-(unsigned long long)fileSize
{
    unsigned long long size = 0;
    NSFileManager *manager = [NSFileManager defaultManager];
    BOOL directory = NO;
    BOOL exists = [manager fileExistsAtPath:self isDirectory:&directory];
    // 如果地址为空
    if (!exists) return size;
    // 如果是文件夹
    if (directory) {
        NSDirectoryEnumerator *enumerator = [manager enumeratorAtPath:self];
        for (NSString *path in enumerator) {
            NSString *fullPath = [self stringByAppendingPathComponent:path];
            NSDictionary *attr = [manager attributesOfItemAtPath:fullPath error:nil];
            size += attr.fileSize;
        }
    }else{
        size = [manager attributesOfItemAtPath:self error:nil].fileSize;
    }
    return size;
}
@end

这样当需要获取文件大小的时候,直接使用路径.fileSize就可以获得文件大小了,非常方便。并且这个方法在别的项目中也经常会用到,快将这个方法添加到你的代码库中吧。

总结

今天主要完成了我的界面的搭建,主要内容CocoaPods的使用以及AFN,SD,MJExtension等第三方框架的简单使用,tableView的footView的布局和显示,webView的加载,多种cell共存的实现,清除缓存功能的实现等,其中有许多细节问题需要注意。

第三天效果如下

第三天效果


文中如果有不对的地方欢迎指出。我是xx_cc,一只长大很久但还没有二够的家伙。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016.10.08 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第三天任务:
  • 我的页面的搭建
  • 清除缓存功能
  • 方法抽取总结
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档