学习目标
1.【理解】九宫格坐标计算
2.【理解】应用添加子控件
3.【理解】应用子控件添加数据
4.【理解】字典转模型
5.【掌握】xib初体验
6.【理解】初识MVC设计模式
7.【理解】根据MVC模式封装我们的应用
一、九宫格坐标计算
实现以九宫格的形式展示应用信息,点击按钮后能监听按钮单击事件。类似这种类型app往往都是动态加载应用数据,所以我们不可能将数据写死,因为我们不确定应用数量,所以就无法确认控件的数量。最终效果图如下:
界面分析:
一个控件需要显示在界面上,必须为其设置frame和一些必要属性。如果将每一个控件单独的添加到view中,每一个控件的坐标都不同而且计算非常繁琐,也不方便后期坐标的修改。所以我们考虑为每一个应用创建一个单独的view,然后为这个view添加三个子控件,因为每个view中的三个子控件中的坐标都是基于这个单独view而且都是固定的,这样无论是坐标计算还是后期修改子控件坐标都非常方便。
创建项目,拖入使用到的图片素材和plist文件。
app.plist中是一个数组,有12个元素,每个元素有拥有两个字典键值对,分别表示应用的名称、应用图标名。
声明一个数组属性,用于存储plist文件中加载的应用数据并实现加载过程,这里我使用懒加载方式,在需要加载的时候才加载数据,并且只会加载一次。
ViewController.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 | #import "ViewController.h" @interface ViewController ( ) @property ( strong , nonatomic ) NSArray *apps ; @end @implementation ViewController - ( void ) viewDidLoad { [ super viewDidLoad ] ; } - ( void ) didReceiveMemoryWarning { [ super didReceiveMemoryWarning ] ; } //重写_apps属性的get方法,实现懒加载plist文件中的数据 - ( NSArray * ) apps { //当属性为nil的时候才加载,这样保证数据只会加载一次 if ( _apps == nil ) { //加载app.plist中的应用数据赋值给数组属性 _apps = [ NSArray arrayWithContentsOfFile : [ [ NSBundle mainBundle ] pathForResource : @"app.plist" ofType :nil ] ] ; } return _apps ; } @end |
根据九宫格公式,计算每个应用的坐标并显示到屏幕上,计算x坐标和y坐标的原理相同。如下图:
1 2 3 | x = 1个横向边距 + ( 1个宽 + 1个横向边距 ) * (当前下标 %每行个数 ) ; y = 1个纵向边距 + ( 1个高 + 1个纵向边距 ) * (当前下标 /每行个数 ) ; |
假如每个应用宽为viewW,高为viewH,横向边距为appMarginRow,纵向边距为appMarginCol,每行3个应用,则得到下面结果:
第一个应用:
1 2 3 | x = appMarginRow + ( viewW + appMarginRow ) * ( 0 % 3 ) = appMarginRow ; y = appMarginCol + ( viewH + appMarginCol ) * ( 0 / 3 ) = appMarginCol ; |
第二个应用:
1 2 3 | x = appMarginRow + ( viewW + appMarginRow ) * ( 1 % 3 ) = appMarginRow + ( viewW + appMarginRow ) * 1 ; y = appMarginCol + ( viewH + appMarginCol ) * ( 1 / 3 ) = appMarginCol ; |
第三个应用:
1 2 3 | x = appMarginRow + ( viewW + appMarginRow ) * ( 2 % 3 ) = appMarginRow + ( viewW + appMarginRow ) * 2 ; y = appMarginCol + ( viewH + appMarginCol ) * ( 2 / 3 ) = appMarginCol ; |
第四个应用:
1 2 3 | x = appMarginRow + ( viewW + appMarginRow ) * ( 3 % 3 ) = appMarginRow ; y = appMarginCol + ( viewH + appMarginCol ) * ( 3 / 3 ) = appMarginCol + ( viewH + appMarginCol ) * 1 ; |
第五个应用:
1 2 3 | x = appMarginRow + ( viewW + appMarginRow ) * ( 4 % 3 ) = appMarginRow + ( viewW + appMarginRow ) * 1 ; y = appMarginCol + ( viewH + appMarginCol ) * ( 4 / 3 ) = appMarginCol + ( viewH + appMarginCol ) * 1 ; |
第六个应用:
1 2 3 | x = appMarginRow + ( viewW + appMarginRow ) * ( 5 % 3 ) = appMarginRow + ( viewW + appMarginRow ) * 2 ; y = appMarginCol + ( viewH + appMarginCol ) * ( 5 / 3 ) = appMarginCol + ( viewH + appMarginCol ) * 1 ; |
......
由此发现了一个规律,我们可以给每一行计算出一个行索引,每一列计算出一个列索引。
1 2 3 4 5 6 7 | rowIndex =当前下标 %每行个数 ; colIndex =当前下标 /每行个数 ; x = 1个横向边距 + ( 1个宽 + 1个横向边距 ) * rowIndex ; y = 1个纵向边距 + ( 1个高 + 1个纵向边距 ) * colIndex ; |
ViewController.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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | #import "ViewController.h" @interface ViewController ( ) @property ( strong , nonatomic ) NSArray *apps ; @end @implementation ViewController - ( void ) viewDidLoad { [ super viewDidLoad ] ; //应用显示的总列数(每行多少个) int appCol = 3 ; //每个应用所占的宽、高固定 CGFloat viewW = 90 ; CGFloat viewH = 90 ; //一横排(一行)每个应用之间的间距 = ( 大view宽度(屏幕宽度) - 应用总列数 * 每个应用的的宽度 ) / (应用总列数 + 1) CGFloat appMarginRow = ( self . view . frame . size . width - appCol * viewW ) / ( 3 + 1 ) ; //每一排应用直接的间距给一个固定值25 CGFloat appMarginCol = 25 ; //self.apps.count表达式中调用了_apps的get方法,应用数据自动加载到_apps数组中,数组中每个元素是一个字典 for ( int i = 0 ; i < self . apps . count ; i ++ ) { //创建应用view UIView *view = [ [ UIView alloc ] init ] ; //计算行索引、列索引 int rowIndex = i % appCol ; int colIndex = i / appCol ; //计算应用view的x、y坐标 CGFloat viewX = appMarginRow + ( appMarginRow + viewW ) * rowIndex ; CGFloat viewY = appMarginCol + ( appMarginCol + viewH ) * colIndex ; //设置应用view的frame属性 view . frame = CGRectMake ( viewX , viewY , viewW , viewH ) ; //设置应用view的背景色 view . backgroundColor = [ UIColor redColor ] ; //将应用view添加到父容器 [ self . view addSubview :view ] ; } } - ( void ) didReceiveMemoryWarning { [ super didReceiveMemoryWarning ] ; } //重写_apps属性的get方法,实现懒加载plist文件中的数据 - ( NSArray * ) apps { //当属性为nil的时候才加载,这样保证数据只会加载一次 if ( _apps == nil ) { //加载app.plist中的应用数据赋值给数组属性 _apps = [ NSArray arrayWithContentsOfFile : [ [ NSBundle mainBundle ] pathForResource : @"app.plist" ofType :nil ] ] ; } return _apps ; } @end |
二、应用添加子控件
现在应用view已经创建好了,接下来要创建每个应用view中的子控件,包括应用图标、应用名称和下载按钮。应用view的子控件坐标是以它为基准的,并且每个应用的子控件都是不变的。
ViewController.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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | #import "ViewController.h" @interface ViewController ( ) @property ( strong , nonatomic ) NSArray *apps ; @end @implementation ViewController - ( void ) viewDidLoad { [ super viewDidLoad ] ; //应用显示的总列数(每行多少个) int appCol = 3 ; //每个应用所占的宽、高固定 CGFloat viewW = 90 ; CGFloat viewH = 90 ; //一横排(一行)每个应用之间的间距 = ( 大view宽度(屏幕宽度) - 应用总列数 * 每个应用的的宽度 ) / (应用总列数 + 1) CGFloat appMarginRow = ( self . view . frame . size . width - appCol * viewW ) / ( 3 + 1 ) ; //每一排应用直接的间距给一个固定值25 CGFloat appMarginCol = 25 ; //self.apps.count表达式中调用了_apps的get方法,应用数据自动加载到_apps数组中,数组中每个元素是一个字典 for ( int i = 0 ; i < self . apps . count ; i ++ ) { //创建应用view UIView *view = [ [ UIView alloc ] init ] ; //计算行索引、列索引 int rowIndex = i % appCol ; int colIndex = i / appCol ; //计算应用view的x、y坐标 CGFloat viewX = appMarginRow + ( appMarginRow + viewW ) * rowIndex ; CGFloat viewY = appMarginCol + ( appMarginCol + viewH ) * colIndex ; //设置应用view的frame属性 view . frame = CGRectMake ( viewX , viewY , viewW , viewH ) ; //设置应用view的背景色 view . backgroundColor = [ UIColor redColor ] ; //将应用view添加到父容器 [ self . view addSubview :view ] ; //-----------创建应用图标控件----------- UIImageView *iconView = [ [ UIImageView alloc ] init ] ; //计算应用图标控件的宽高、坐标,并设置frame属性 CGFloat iconViewW = 50 ; CGFloat iconViewH = 50 ; CGFloat iconViewX = ( viewW - iconViewW ) / 2 ; //让图标居中 CGFloat iconViewY = 0 ; //顶部靠着应用view的,所以y坐标为0 iconView . frame = CGRectMake ( iconViewX , iconViewY , iconViewW , iconViewH ) ; //图标的颜色,到时候替换成数据 iconView . backgroundColor = [ UIColor yellowColor ] ; //添加应用图标控件到应用view,注意不是添加到self.view [ view addSubview :iconView ] ; //-----------创建应用名称控件----------- UILabel *nameView = [ [ UILabel alloc ] init ] ; //计算应用名称控件的宽高、坐标,并设置frame属性 CGFloat nameViewW = viewW ; //应用名称宽度和应用view一样宽 CGFloat nameViewH = ( viewH - iconViewH ) / 2 ; //让应用名称宽度和下载按钮宽度相同 CGFloat nameViewX = 0 ; //左边靠着应用view,所以x坐标为0 CGFloat nameViewY = iconViewH ; //y坐标就是图标的高度 nameView . frame = CGRectMake ( nameViewX , nameViewY , nameViewW , nameViewH ) ; //应用名称控件背景色,先填充让效果显示出来 nameView . backgroundColor = [ UIColor blackColor ] ; //添加应用名称控件到应用view,注意不是添加到self.view [ view addSubview :nameView ] ; //-----------创建应用下载按钮控件----------- UIButton *downView = [ UIButton buttonWithType :UIButtonTypeCustom ] ; //计算下载按钮控件的宽高、坐标,并设置frame属性 CGFloat downViewW = iconViewW ; //和应用图标控件一样宽 CGFloat downViewH = nameViewH ; //和应用名称控件一样高 CGFloat downViewX = iconViewX ; //居中显示 CGFloat downViewY = iconViewH + nameViewH ; //应用图标控件和应用名称控件的高度和就是按钮的y坐标 downView . frame = CGRectMake ( downViewX , downViewY , downViewW , downViewH ) ; //下载按钮控件背景色,先填充让效果显示出来 downView . backgroundColor = [ UIColor grayColor ] ; //添加应用下载控件到应用view,注意不是添加到self.view [ view addSubview :downView ] ; } } - ( void ) didReceiveMemoryWarning { [ super didReceiveMemoryWarning ] ; } //重写_apps属性的get方法,实现懒加载plist文件中的数据 - ( NSArray * ) apps { //当属性为nil的时候才加载,这样保证数据只会加载一次 if ( _apps == nil ) { //加载app.plist中的应用数据赋值给数组属性 _apps = [ NSArray arrayWithContentsOfFile : [ [ NSBundle mainBundle ] pathForResource : @"app.plist" ofType :nil ] ] ; } return _apps ; } @end |
三、应用子控件添加数据
现在应用管理界面大体框架已经搭建好了,而数据在_apps数组中,每个元素都是一个字典(NSDictionary)。我们可以通过依次取出数组中的每个元素,也就是取出数组中的每个字典,然后通过字典取出字典中的应用图标、应用名称为子控件赋值。
ViewController.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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | #import "ViewController.h" @interface ViewController ( ) @property ( strong , nonatomic ) NSArray *apps ; @end @implementation ViewController - ( void ) viewDidLoad { [ super viewDidLoad ] ; //应用显示的总列数(每行多少个) int appCol = 3 ; //每个应用所占的宽、高固定 CGFloat viewW = 90 ; CGFloat viewH = 90 ; //一横排(一行)每个应用之间的间距 = ( 大view宽度(屏幕宽度) - 应用总列数 * 每个应用的的宽度 ) / (应用总列数 + 1) CGFloat appMarginRow = ( self . view . frame . size . width - appCol * viewW ) / ( 3 + 1 ) ; //每一排应用直接的间距给一个固定值25 CGFloat appMarginCol = 25 ; //self.apps.count表达式中调用了_apps的get方法,应用数据自动加载到_apps数组中,数组中每个元素是一个字典 for ( int i = 0 ; i < self . apps . count ; i ++ ) { //创建应用view UIView *view = [ [ UIView alloc ] init ] ; //计算行索引、列索引 int rowIndex = i % appCol ; int colIndex = i / appCol ; //计算应用view的x、y坐标 CGFloat viewX = appMarginRow + ( appMarginRow + viewW ) * rowIndex ; CGFloat viewY = appMarginCol + ( appMarginCol + viewH ) * colIndex ; //设置应用view的frame属性 view . frame = CGRectMake ( viewX , viewY , viewW , viewH ) ; //将应用view添加到父容器 [ self . view addSubview :view ] ; //依次取出数组中的每个字典元素 NSDictionary *dict = self . apps [ i ] ; //-----------创建应用图标控件----------- UIImageView *iconView = [ [ UIImageView alloc ] init ] ; //计算应用图标控件的宽高、坐标,并设置frame属性 CGFloat iconViewW = 50 ; CGFloat iconViewH = 50 ; CGFloat iconViewX = ( viewW - iconViewW ) / 2 ; //让图标居中 CGFloat iconViewY = 0 ; //顶部靠着应用view的,所以y坐标为0 iconView . frame = CGRectMake ( iconViewX , iconViewY , iconViewW , iconViewH ) ; //添加应用图标数据 iconView . image = [ UIImage imageNamed :dict [ @"icon" ] ] ; //添加应用图标控件到应用view,注意不是添加到self.view [ view addSubview :iconView ] ; //-----------创建应用名称控件----------- UILabel *nameView = [ [ UILabel alloc ] init ] ; //计算应用名称控件的宽高、坐标,并设置frame属性 CGFloat nameViewW = viewW ; //应用名称宽度和应用view一样宽 CGFloat nameViewH = ( viewH - iconViewH ) / 2 ; //让应用名称宽度和下载按钮宽度相同 CGFloat nameViewX = 0 ; //左边靠着应用view,所以x坐标为0 CGFloat nameViewY = iconViewH ; //y坐标就是图标的高度 nameView . frame = CGRectMake ( nameViewX , nameViewY , nameViewW , nameViewH ) ; //添加应用名称数据 nameView . text = dict [ @"name" ] ; //设置文字字体大小、居中显示 nameView . font = [ UIFont systemFontOfSize : 12 ] ; nameView . textAlignment = NSTextAlignmentCenter ; //添加应用名称控件到应用view,注意不是添加到self.view [ view addSubview :nameView ] ; //-----------创建应用下载按钮控件----------- UIButton *downView = [ UIButton buttonWithType :UIButtonTypeCustom ] ; //计算下载按钮控件的宽高、坐标,并设置frame属性 CGFloat downViewW = iconViewW ; //和应用图标控件一样宽 CGFloat downViewH = nameViewH ; //和应用名称控件一样高 CGFloat downViewX = iconViewX ; //居中显示 CGFloat downViewY = iconViewH + nameViewH ; //应用图标控件和应用名称控件的高度和就是按钮的y坐标 downView . frame = CGRectMake ( downViewX , downViewY , downViewW , downViewH ) ; //设置按钮默认、高亮状态下的文字、背景图片 [ downView setTitle : @"下载" forState :UIControlStateNormal ] ; [ downView setTitle : @"已下载" forState :UIControlStateHighlighted ] ; [ downView setBackgroundImage : [ UIImage imageNamed : @"buttongreen" ] forState :UIControlStateNormal ] ; [ downView setBackgroundImage : [ UIImage imageNamed : @"buttongreen_highlighted" ] forState :UIControlStateHighlighted ] ; //设置按钮文字大小 downView . titleLabel . font = [ UIFont systemFontOfSize : 14 ] ; //添加应用下载控件到应用view,注意不是添加到self.view [ view addSubview :downView ] ; } } - ( void ) didReceiveMemoryWarning { [ super didReceiveMemoryWarning ] ; } //重写_apps属性的get方法,实现懒加载plist文件中的数据 - ( NSArray * ) apps { //当属性为nil的时候才加载,这样保证数据只会加载一次 if ( _apps == nil ) { //加载app.plist中的应用数据赋值给数组属性 _apps = [ NSArray arrayWithContentsOfFile : [ [ NSBundle mainBundle ] pathForResource : @"app.plist" ofType :nil ] ] ; } return _apps ; } @end |
四、字典转模型
以上面这种方式添加数据,如果在调用字典时key值写错Xcode将不会报任何错误,这样有时会造成非常坑爹的BUG。所以如果我们将字典中的数据以对象的方式来进行调用,就能避免类似问题还可以更好的封装数据,也就是MVC中的封装数据模型(Model)。
创建一个类JFApp继承自NSObject,用于封装应用信息数据。类的属性类型根据封装数据的类型来确定,这里app.plist中每个元素是拥有两个键值对的字典,所以定义两个NSString类型的属性分别封装应用图标、应用名称。
JFApp.h文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #import <Foundation/Foundation.h> @interface JFApp : NSObject //应用图标、应用名称属性 @property ( copy , nonatomic ) NSString *icon ; @property ( copy , nonatomic ) NSString *name ; //快速创建模型对象的对象方法和类方法 - ( instancetype ) initWithDictionary : ( NSDictionary * ) dict ; + ( instancetype ) appWithDictionary : ( NSDictionary * ) dict ; //返回模型数组 + ( NSArray * ) apps ; @end |
JFApp.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 | #import "JFApp.h" @implementation JFApp //快速创建模型对象的对象方法和类方法 - ( instancetype ) initWithDictionary : ( NSDictionary * ) dict { if ( self = [ super init ] ) { self . icon = dict [ @"icon" ] ; self . name = dict [ @"name" ] ; } return self ; } + ( instancetype ) appWithDictionary : ( NSDictionary * ) dict { return [ [ self alloc ] initWithDictionary :dict ] ; } //返回模型数组 + ( NSArray * ) apps { //加载app.plist中的数据到数组 NSArray *array = [ NSArray arrayWithContentsOfFile : [ [ NSBundle mainBundle ] pathForResource : @"app.plist" ofType :nil ] ] ; //定义一个可变数组用于存储模型 NSMutableArray *arrayM = [ NSMutableArray array ] ; for ( NSDictionary *dict in array ) { JFApp *app = [ JFApp appWithDictionary :dict ] ; //将创建好的模型一次添加到可变数组 [ arrayM addObject :app ] ; } //返回模型数组 return arrayM ; } @end |
懒加载返回的数组中存储的是模型对象,所以我们需要修改ViewController.m文件中懒加载代码和所有用到字典获取数据的代码。
ViewController.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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | #import "ViewController.h" #import "JFApp.h" @interface ViewController ( ) @property ( strong , nonatomic ) NSArray *apps ; @end @implementation ViewController - ( void ) viewDidLoad { [ super viewDidLoad ] ; //应用显示的总列数(每行多少个) int appCol = 3 ; //每个应用所占的宽、高固定 CGFloat viewW = 90 ; CGFloat viewH = 90 ; //一横排(一行)每个应用之间的间距 = ( 大view宽度(屏幕宽度) - 应用总列数 * 每个应用的的宽度 ) / (应用总列数 + 1) CGFloat appMarginRow = ( self . view . frame . size . width - appCol * viewW ) / ( 3 + 1 ) ; //每一排应用直接的间距给一个固定值25 CGFloat appMarginCol = 25 ; for ( int i = 0 ; i < self . apps . count ; i ++ ) { //创建应用view UIView *view = [ [ UIView alloc ] init ] ; //计算行索引、列索引 int rowIndex = i % appCol ; int colIndex = i / appCol ; //计算应用view的x、y坐标 CGFloat viewX = appMarginRow + ( appMarginRow + viewW ) * rowIndex ; CGFloat viewY = appMarginCol + ( appMarginCol + viewH ) * colIndex ; //设置应用view的frame属性 view . frame = CGRectMake ( viewX , viewY , viewW , viewH ) ; //将应用view添加到父容器 [ self . view addSubview :view ] ; //依次取出数组中的每个模型对象 JFApp *app = self . apps [ i ] ; //-----------创建应用图标控件----------- UIImageView *iconView = [ [ UIImageView alloc ] init ] ; //计算应用图标控件的宽高、坐标,并设置frame属性 CGFloat iconViewW = 50 ; CGFloat iconViewH = 50 ; CGFloat iconViewX = ( viewW - iconViewW ) / 2 ; //让图标居中 CGFloat iconViewY = 0 ; //顶部靠着应用view的,所以y坐标为0 iconView . frame = CGRectMake ( iconViewX , iconViewY , iconViewW , iconViewH ) ; //添加应用图标数据 iconView . image = [ UIImage imageNamed :app . icon ] ; //添加应用图标控件到应用view,注意不是添加到self.view [ view addSubview :iconView ] ; //-----------创建应用名称控件----------- UILabel *nameView = [ [ UILabel alloc ] init ] ; //计算应用名称控件的宽高、坐标,并设置frame属性 CGFloat nameViewW = viewW ; //应用名称宽度和应用view一样宽 CGFloat nameViewH = ( viewH - iconViewH ) / 2 ; //让应用名称宽度和下载按钮宽度相同 CGFloat nameViewX = 0 ; //左边靠着应用view,所以x坐标为0 CGFloat nameViewY = iconViewH ; //y坐标就是图标的高度 nameView . frame = CGRectMake ( nameViewX , nameViewY , nameViewW , nameViewH ) ; //添加应用名称数据 nameView . text = app . name ; //设置文字字体大小、居中显示 nameView . font = [ UIFont systemFontOfSize : 12 ] ; nameView . textAlignment = NSTextAlignmentCenter ; //添加应用名称控件到应用view,注意不是添加到self.view [ view addSubview :nameView ] ; //-----------创建应用下载按钮控件----------- UIButton *downView = [ UIButton buttonWithType :UIButtonTypeCustom ] ; //计算下载按钮控件的宽高、坐标,并设置frame属性 CGFloat downViewW = iconViewW ; //和应用图标控件一样宽 CGFloat downViewH = nameViewH ; //和应用名称控件一样高 CGFloat downViewX = iconViewX ; //居中显示 CGFloat downViewY = iconViewH + nameViewH ; //应用图标控件和应用名称控件的高度和就是按钮的y坐标 downView . frame = CGRectMake ( downViewX , downViewY , downViewW , downViewH ) ; //设置按钮默认、高亮状态下的文字、背景图片 [ downView setTitle : @"下载" forState :UIControlStateNormal ] ; [ downView setTitle : @"已下载" forState :UIControlStateHighlighted ] ; [ downView setBackgroundImage : [ UIImage imageNamed : @"buttongreen" ] forState :UIControlStateNormal ] ; [ downView setBackgroundImage : [ UIImage imageNamed : @"buttongreen_highlighted" ] forState :UIControlStateHighlighted ] ; //设置按钮文字大小 downView . titleLabel . font = [ UIFont systemFontOfSize : 14 ] ; //添加应用下载控件到应用view,注意不是添加到self.view [ view addSubview :downView ] ; } } - ( void ) didReceiveMemoryWarning { [ super didReceiveMemoryWarning ] ; } //重写_apps属性的get方法,实现懒加载 - ( NSArray * ) apps { //当属性为nil的时候才加载,这样保证数据只会加载一次 if ( _apps == nil ) { //调用类方法返回一个模型数组 _apps = [ JFApp apps ] ; } return _apps ; } @end |
五、xib初体验
应用管理界面搭建、调用数据都已经完成,但是创建应用的子控件的操作非常的繁琐,我们可以利用苹果提供的一款工具xib来简化我们的操作。
创建一个xib文件,因为我们是要描述一个应用view,所以命名为JFAppView,其中JF是前缀。
拖拽一个UIView控件,修改Size选项为Freeform,Status Bar为None。
和操作我们熟悉的Main.storyboard一样,设置View的宽、高都为90,然后再拖拽UIImage、UILabel、 UIButton,并设置其尺寸、字体大小、按钮背景图等等。设置的尺寸和我们上面代码写的子控件尺寸一样。并为每个子控件设置对应的tag值,我这里设 置的是UIImage为1、Label为2、Button为3。
加载xib文件并调用我们创建好的View控件
1 2 3 4 5 | //在app安装后的根目录找出xib文件JFAppView,注意这里不能加扩展名,加载后返回一个数组 NSArray *viewArray = [ [ NSBundle mainBundle ] loadNibNamed : @"JFAppView" owner :self options :nil ] ; //取出数组最后一个元素,因为数组第一个元素是系统自己的view,取出最后一个就能获取到我们自己创建的view UIView *view = [ viewArray lastObject ] ; |
根据子控件的tag值调用view的对象方法来创建子控件
1 2 3 4 5 6 7 8 | //创建应用图标控件,方法返回值是UIView类型,所以需要强转 UIImageView *iconView = ( UIImageView * ) [ view viewWithTag : 1 ] ; //创建应用名称控件,方法返回值是UIView类型,所以需要强转 UILabel *nameView = ( UILabel * ) [ view viewWithTag : 2 ] ; //创建应用下载按钮控件,方法返回值是UIView类型,所以需要强转 UIButton *downView = ( UIButton * ) [ view viewWithTag : 3 ] ; |
ViewController.m文件(已加载xib中的控件,简写代码)
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | #import "ViewController.h" #import "JFApp.h" @interface ViewController ( ) @property ( strong , nonatomic ) NSArray *apps ; @end @implementation ViewController - ( void ) viewDidLoad { [ super viewDidLoad ] ; //应用显示的总列数(每行多少个) int appCol = 3 ; //每个应用所占的宽、高固定 CGFloat viewW = 90 ; CGFloat viewH = 90 ; //一横排(一行)每个应用之间的间距 = ( 大view宽度(屏幕宽度) - 应用总列数 * 每个应用的的宽度 ) / (应用总列数 + 1) CGFloat appMarginRow = ( self . view . frame . size . width - appCol * viewW ) / ( 3 + 1 ) ; //每一排应用直接的间距给一个固定值25 CGFloat appMarginCol = 25 ; for ( int i = 0 ; i < self . apps . count ; i ++ ) { //创建应用view //在app安装后的根目录找出xib文件JFAppView,注意这里不能加扩展名,加载后返回一个数组 NSArray *viewArray = [ [ NSBundle mainBundle ] loadNibNamed : @"JFAppView" owner :self options :nil ] ; //取出数组最后一个元素,因为数组第一个元素是系统自己的view,取出最后一个就能获取到我们自己创建的view UIView *view = [ viewArray lastObject ] ; //计算行索引、列索引 int rowIndex = i % appCol ; int colIndex = i / appCol ; //计算应用view的x、y坐标 CGFloat viewX = appMarginRow + ( appMarginRow + viewW ) * rowIndex ; CGFloat viewY = appMarginCol + ( appMarginCol + viewH ) * colIndex ; //设置应用view的frame属性 view . frame = CGRectMake ( viewX , viewY , viewW , viewH ) ; //将应用view添加到父容器 [ self . view addSubview :view ] ; //依次取出数组中的每个模型对象 JFApp *app = self . apps [ i ] ; //-----------创建应用图标控件----------- UIImageView *iconView = ( UIImageView * ) [ view viewWithTag : 1 ] ; //添加应用图标数据 iconView . image = [ UIImage imageNamed :app . icon ] ; //添加应用图标控件到应用view,注意不是添加到self.view [ view addSubview :iconView ] ; //-----------创建应用名称控件----------- UILabel *nameView = ( UILabel * ) [ view viewWithTag : 2 ] ; //添加应用名称数据 nameView . text = app . name ; //添加应用名称控件到应用view,注意不是添加到self.view [ view addSubview :nameView ] ; //-----------创建应用下载按钮控件----------- UIButton *downView = ( UIButton * ) [ view viewWithTag : 3 ] ; //添加应用下载控件到应用view,注意不是添加到self.view [ view addSubview :downView ] ; } } - ( void ) didReceiveMemoryWarning { [ super didReceiveMemoryWarning ] ; } //重写_apps属性的get方法,实现懒加载 - ( NSArray * ) apps { //当属性为nil的时候才加载,这样保证数据只会加载一次 if ( _apps == nil ) { //调用类方法返回一个模型数组 _apps = [ JFApp apps ] ; } return _apps ; } @end |
这样做只是简化了大量创建子控件、设置子控件属性的代码,但还是没有达到我们最终优化的目的。先来了解下MVC设计模式概念,再继续优化我们的代码。
六、初识MVC设计模式
MVC全名是Model View Controller,是模型(model)、视图(view)、控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。
视图:数据的展现
视图是用户看到并与之交互的界面。视图向用户显示相关的数据,并能接收用户的输入数据,但是它并不进行任何实际的业务处理。视图可以向模型查询业务状态,但不能改变模型。视图还能接受模型发出的数据更新事件,从而对用户界面进行同步更新。
模型:应用对象
模型是应用程序的主体部分。模型代表了业务数据和业务逻辑。当数据发生改变时,它要负责通知视图部分。一个模型能为多个视图提供数据。由于同一个模型可以被多个视图重用,所以提高了应用的可重用性。
控制器:逻辑处理、控制实体数据在视图上展示、调用模型处理业务请求
当用户单击了应用管理中的下载按钮触发一个单击事件时,控制器接收请求并调用相应的模型去处理请求,然后调用相应的视图来显示模型返回的数据。
七、根据MVC模式封装我们的应用
根据MVC模式设计思想,我们继续优化我们的应用管理app。让模型、视图与控制器分开,独立完成各自的工作。
先来为我们的xib文件新建一个视图类JFAppView继承自UIView,用于操作xib中的控件和显示控件的数据。注意一定要为xib指定对应的视图类才能进行控件拖线操作。
控件连线操作,需要被访问的控件就设置为属性,能触发事件的控件就设置为方法。
为视图类提供一个快速创建view的类方法,在方法中加载xib文件并为调用者(控制器)返回一个view。而不是让调用者(控制器)自己加载xib创建view。
1 2 3 4 | //快速创建一个view + ( JFAppView * ) appview { return [ [ [ NSBundle mainBundle ] loadNibNamed : @"JFAppView" owner :self options :nil ] lastObject ] ; } |
再为视图类提供一个为子控件赋值数据的方法,接收数据模型,并将模型中的数据赋值给对应的子控件。这里也可以将模型设置为视图类的属性,重写这个属性的set方法,在set方法中为模型属性赋值的同时也为子控件赋值。
1 2 3 4 5 | //接收模型对象为子控件加载数据 - ( void ) setApp : ( JFApp * ) app { self . iconView . image = [ UIImage imageNamed :app . icon ] ; self . nameView . text = app . name ; } |
JFAppView.h文件
1 2 3 4 5 6 7 8 9 10 11 | #import <UIKit/UIKit.h> #import "JFApp.h" @interface JFAppView : UIView //快速创建一个view + ( JFAppView * ) appview ; //接收模型对象为子控件加载数据 - ( void ) setApp : ( JFApp * ) app ; @end |
JFAppView.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 | #import "JFAppView.h" //给视图类添加一个延展,封装属性、方法 @interface JFAppView ( ) @property ( weak , nonatomic ) IBOutlet UIImageView *iconView ; @property ( weak , nonatomic ) IBOutlet UILabel *nameView ; //下载按钮触发的事件 - ( IBAction ) downAction ; @end @implementation JFAppView //下载按钮触发的事件 - ( IBAction ) downAction { } //快速创建一个view + ( JFAppView * ) appview { return [ [ [ NSBundle mainBundle ] loadNibNamed : @"JFAppView" owner :self options :nil ] lastObject ] ; } //接收模型对象为子控件加载数据 - ( void ) setApp : ( JFApp * ) app { self . iconView . image = [ UIImage imageNamed :app . icon ] ; self . nameView . text = app . name ; } @end |
ViewController.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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | #import "ViewController.h" #import "JFApp.h" #import "JFAppView.h" @interface ViewController ( ) @property ( strong , nonatomic ) NSArray *apps ; @end @implementation ViewController - ( void ) viewDidLoad { [ super viewDidLoad ] ; //应用显示的总列数(每行多少个) int appCol = 3 ; for ( int i = 0 ; i < self . apps . count ; i ++ ) { //创建应用view JFAppView *view = [ JFAppView appview ] ; //依次取出数组中的每个模型对象 JFApp *app = self . apps [ i ] ; //传递给视图类中的方法为子控件加载数据 [ view setApp :app ] ; //计算行索引、列索引 int rowIndex = i % appCol ; int colIndex = i / appCol ; //取出view的宽高 CGFloat viewW = view . frame . size . width ; CGFloat viewH = view . frame . size . height ; //计算view的横向边距、纵向边距 CGFloat appMarginRow = ( self . view . frame . size . width - appCol * viewW ) / ( 3 + 1 ) ; CGFloat appMarginCol = 25 ; //计算view的坐标 CGFloat viewX = appMarginRow + ( appMarginRow + viewW ) * rowIndex ; CGFloat viewY = appMarginCol + ( appMarginCol + viewH ) * colIndex ; //设置应用view的frame属性 view . frame = CGRectMake ( viewX , viewY , viewW , viewH ) ; //将应用view添加到父容器 [ self . view addSubview :view ] ; } } - ( void ) didReceiveMemoryWarning { [ super didReceiveMemoryWarning ] ; } //重写_apps属性的get方法,实现懒加载 - ( NSArray * ) apps { //当属性为nil的时候才加载,这样保证数据只会加载一次 if ( _apps == nil ) { //调用类方法返回一个模型数组 _apps = [ JFApp apps ] ; } return _apps ; } @end |
控制台中只是创建了一个应用view并设置其frame,接收模型兑现传递给view,并显示应用到屏幕上。这样就完成了简单MVC模式的应用,我们再调整一下Xcode导航结构。