iOS回顾笔记( 02 ) -- 由九宫格布局引发的一系列“惨案”
iOS回顾笔记( 02 ) -- 由九宫格布局引发的一系列“惨案”
前言(扯几句淡先)
回顾到学习UI过程中的九宫格布局时,发现当时学的东西真是不少。
这个阶段最大的特点就是:知识点繁多且琐碎。
我们的目标就是要将这琐碎的知识点灵活运用、融汇贯通,通过不同的实现方式来实现相同的功能,最后进行比较得到最好的那种方式。这个求知的过程就是我们最需要学习的,在过程中我们学会了自我思考,并且在自己的思考和比较中,我们的脑海里逐渐形成了自己的编程思想。
本文主要以九宫格购物车的实现为引子,从最基础的实现方法层层递进直到最完美的实现方式。代码从起初的低效、耦合度高到后期的层层分离、MVC各模块封装,高内聚、低耦合,更具扩展性等方面逐步深化和扩展;经过编程思想的层层深入和代码的步步完善最后完整的展现在用户面前。
本文大体目录
- 九宫格购物车demo
- 对应物品封装(View的简单封装)
- 数据的加载方式对比 与 演变
- 属性列表文件(plist)的创建和使用
- MVC 思想的引入和介绍
九宫格购物车Demo
九宫格是我们在开发过程中对于一些有规律的UI布局常用的一种布局算法。
九宫格特点:简单,封装性好,可复用性高,很适合一些页面同类型item数量动态变化UI页面布局
下面就实现一个买书购物车
基本要求和思路如下
思路:
1. 最初没有书(删除按钮不可用)
2. 点击添加就添加一本书(删除按钮可用)
3. 添加基本之后(提示购物车已满,添加按钮不可用)
4. 删除按钮点击(购物车不满的时候添加按钮又可以使用)
5. 添加书籍按九宫格的样式在页面中显示
先说说最直接的方法
// 添加书
- (IBAction)addBook:(id)sender {
// 1. 创建书图标
UIImageView *bookIcon = [UIImageView new];
bookIcon.image = [UIImage imageNamed:@"0"];
bookIcon.frame = CGRectMake(0, 0, 50, 50);
[self.shopView addSubview:bookIcon];
// 2.书名
UILabel *bookName = [UILabel new];
bookName.frame = CGRectMake(0, 50, 50, 20);
bookName.text = @"book1";
bookName.textAlignment = NSTextAlignmentCenter;
[self.shopView addSubview:bookName];
// 3.添加到数组(分开写好像很难明确如何添加这本书)
}
这是最直接的方法来添加的一本书,这样确实能添加一本书,但是这种直观、死板的思想是不对的。
- 面对这样的动态变化的UI页面,每次添加和删除书籍的操作都是用户随机的,所以代码每次也是根据对应的点击来计算对应书籍要添加的位置。
- 每本书的位置主要是和它对应的index来确定,这就涉及到“书”这个对象要每次计数,书是一个整体,所以书内部的东西应当封装起来。
- “书”在UI上表现是 图标 + 书名,也就是iamgeview + label,从用户看到的整体性上来说,每次添加和删除同一本书也需要对 图标 和 书名 分别计算两次来计算和排列。这也是非常不合理的,并且很容易计算出现问题。应该根据UIView 父子控件相关特性对“书”进行封装,添加/删除的时候统一处理父控件,至于内部属性都会根据父控件来自动布局,方便管理。
书的封装
了解以上说的直接把代码分开写的局限性之后,现在来封装一下“书”这个对象。
- UI层的封装,我们先分析UI布局:内部属性只有 图标(UIImagevView) 和 书名(UILabel)
- 父控件选取原则:父控件只是承载子控件的容器,应当简洁为主,所以选择 UIView
// 0. 创建书
UIView *book = [UIView new];
book.frame = CGRectMake(0, 0, 60, 70);
book.backgroundColor = [UIColor redColor];
[self.shopView addSubview:book];
// 1. 创建书图标
UIImageView *bookIcon = [UIImageView new];
bookIcon.image = [UIImage imageNamed:@"0"];
bookIcon.frame = CGRectMake(0, 0, 60, 50);
[book addSubview:bookIcon];
// 2.书名
UILabel *bookName = [UILabel new];
bookName.frame = CGRectMake(0, 50, 60, 20);
bookName.text = @"book1";
bookName.textAlignment = NSTextAlignmentCenter;
[book addSubview:bookName];
// 3.添加到数组(直接添加书这个对象)
[self.books addObject:book];
这样在创建和管理书的时候就方便多了,并且有一个数组来记录添加书的数量,在添加和删除的时候有计算的依据:可计算对应位置和两个按钮的可用情况
九宫格布局的思路和实现
书的对象已经封装好了,我们可以以整体思维来操作它,下面就是计算位置的思路。
- 书的数量不定,但列数是固定的,可以设置成变量 int clos = 3;
- 每本书之间可能有一定间距,横向间距 margin = ( width - clos * book.width)/ ( clos / 2) ;
- 每本书的位置(x,y)可根据下图发现规律 x = 列号 * (W + margin); y = 行号 * (H+margin)
- 行号规律 : 行号 = index / clos ;
- 列号规律 : 列号 = index % clos ;
有了上面的铺垫,就可以写动态代码了,只需要用户 设置一个列数,知道最终有多少本书,遍历每本书,根据书的索引来计算对应的书的位置即可。
废话不多说了,上代码
// 添加书
- (IBAction)addBook:(id)sender {
// 设置列数为 3
int clos = 3;
// 设置书的宽高分别为 W H
CGFloat W = 60;
CGFloat H = 70;
CGFloat iconH = 50;
// 0. 创建书
UIView *book = [UIView new];
book.backgroundColor = [UIColor redColor];
[self.shopView addSubview:book];
// 计算书的位置
// 获得索引
NSUInteger index = [self.books count];
// 计算横间距 margin
CGFloat margin = (self.shopView.frame.size.width - clos * W) / (clos - 1);
// 书 frame 的 X
CGFloat x = (index % clos) * (W + margin);
// 书 frame 的 Y
CGFloat y = (index / clos) * (H + margin);
book.frame = CGRectMake(x, y, W, H);
// 1. 创建书图标
UIImageView *bookIcon = [UIImageView new];
bookIcon.image = [UIImage imageNamed:@"0"];
bookIcon.frame = CGRectMake(0, 0, W, iconH);
[book addSubview:bookIcon];
// 2.书名
UILabel *bookName = [UILabel new];
bookName.frame = CGRectMake(0, iconH, W, H-iconH);
bookName.text = @"book1";
bookName.textAlignment = NSTextAlignmentCenter;
[book addSubview:bookName];
// 3.添加到数组(分开写好像很难明确如何添加这本书)
[self.books addObject:book];
}
效果图如下
书数据加载方式和对比
到这里购物车里的书已经可以随意添加了,并且可以根据用户的点击,无限制的添加。
如果项目需求修改了,比如变成5列了,宽高什么的也是根据项目自行修改就行。
九宫格布局的代码已经完成了。
现在需要注意的问题是,“书”的属性数据的添加问题,现在简单的来说,书属性有 :书名 + icon。
“书”的各属性值,应该如何赋值呢?
几种简单数据加载方法与对比
- 直接根据 index 来进行if判断,逐个添加数据
if (index == 0) {
bookIcon.image = [UIImage imageNamed:@"0"];
bookName.text = @"Book1";
}else if(index == 1)
{
bookIcon.image = [UIImage imageNamed:@"1"];
bookName.text = @"Book2";
}
else if(index == 2)
{
bookIcon.image = [UIImage imageNamed:@"2"];
bookName.text = @"Book3";
}
...
- 创建一个书的数组,数组存放对应的字典,作为书的属性方法的数据源,从中取值
// 0.创建书的数据源
self.books = @[
@{
@"icon":@"0",
@"name":@"book1"
},
@{
@"icon":@"1",
@"name":@"book2"
},
@{
@"icon":@"2",
@"name":@"book3"
},
@{
@"icon":@"3",
@"name":@"book4"
},
];
// 1.书图标
bookIcon.image = [UIImage imageNamed:self.books[index][@"icon"]];
// 2.书名
bookName.text = self.books[index][@"name"];
- 进一步分离数据,将数据信息放到其他文件中,用的时候从文件中读取
// 获取文件路径
NSString *books = [[NSBundle mainBundle] pathForResource:@"bookData" ofType:@"plist"];
// 加载路径中内容放到数组中
_books = [NSMutableArray arrayWithContentsOfFile:books];
// 1.书图标
bookIcon.image = [UIImage imageNamed:self.books[index][@"icon"]];
// 2.书名
bookName.text = self.books[index][@"name"];
对于三种方法简单的比较和点评
-
第一种直接if判断index的位置
- 直接把动态判断代码写到代码中,过于死板
- 代码耦合性太高,及其不利于后期扩展,如果加数据简直是恶魔
- 一些死布局并且元素个数极少(几个)的时候可以使用
- 代码重复性高,技术含量低
-
第二种把数据代码独立出来放到一个数组中
- 避免了第一种直接写到代码中的低效且恶心的做法
- 代码还是和数据耦合到一起了,如果后期修改需要修改代码
- 如果数据量大,及其不利于后期管理和数据的修改
- 相比第一种有明显的分离,是一种进步
-
第三种代码和数据分离,放到外部文件中
- 彻底代码和数据分离,耦合性低,易于扩展
- 数据放到文件中、后期添加或者修改数据无需改动代码、更加独立
- 代码更简洁直观,只需关注功能和布局
- 适合企业开发、这是一种通用方式(网络应用更是如此)
plist文件介绍和使用
简介
- plist文件全称:Property List文件,中文又叫属性列表文件。
- 是苹果平台上常用的一种资源描述类文件。
- 它存储的属性一般都是Xcode里面的基本数据类型或者OC里面的对应类型(NSDictionary,NSArray,NSString等)
- Xcode会自动进行解析成可以打开和合上的层叠格式
- 用文本文件打开直接就是XML格式的键值对
plist文件的创建
plist的创建一般直接用Xcode创建,然后在里面添加对应的key和value,(几乎没有人会手写plist文件)
plist文件的使用
plist使用就和其他的文件用法一样,先读取目标文件的路径在根据路径加载到数组中。
// 获取文件路径
NSString *books = [[NSBundle mainBundle] pathForResource:@"bookData" ofType:@"plist"];
// 加载路径中内容放到数组中
_books = [NSMutableArray arrayWithContentsOfFile:books];
plist使用注意
我们自己创建plist文件的时候不能使用info/Info.plist命名
由于每次创建工程的时候,系统会自动生成一个对该项目信息进行描述的info.plist,它会记录项目的一些包名、项目名、开发工具、版本和其他项目的基础信息。
所以我们自己创建plist文件的时候不能使用info/Info.plist
这是为了不和系统的info.plist混淆,实际上自己创建一个info.plist文件和系统重名之后项目是运行不了的。
懒加载(lazyLoad) -- 提升性能
懒加载又叫延迟加载,由于项目运行时候很多数据用不到,可以暂时不创建,等到用的时候在创建,这样节省系统性能。
如以上项目中,就是在添加书的方法中每次创建书籍数组
// 添加书
- (IBAction)addBook:(id)sender {
self.books = @[
@{
@"icon":@"0",
@"name":@"book1"
},
@{
@"icon":@"1",
@"name":@"book2"
},
@{
@"icon":@"2",
@"name":@"book3"
},
@{
@"icon":@"3",
@"name":@"book4"
},
];
// 设置列数为 3
int clos = 3;
// 设置书的宽高分别为 W H
CGFloat W = 60;
CGFloat H = 70;
CGFloat iconH = 50;
// 0. 创建书
UIView *book = [UIView new];
book.backgroundColor = [UIColor redColor];
这样其实非常耗性能,每次都要创建一个数组,然后用过一次就没有用了,对此可以进行一个优化,对于该书籍数据其实就是一个固定数据,每次用一下,用的时候再创建,不用的时候就不管了,这里正好用懒加载最好了。
懒加载实际上就是一个get方法
// 懒加载书籍数据
- (NSMutableArray *)books
{
if (_books == nil) {
// 获取文件路径
NSString *books = [[NSBundle mainBundle] pathForResource:@"bookData" ofType:@"plist"];
// 加载路径中内容放到数组中
_books = [NSMutableArray arrayWithContentsOfFile:books];
}
return _books;
}
// 使用的时候,直接用数组就行
bookIcon.image = [UIImage imageNamed:self.books[index][@"icon"]];
bookName.text = self.books[index][@"name"];
MVC 思想的介绍和引入
以上的小Demo,已经告一段落了,可以实现把文件中书籍数据,按九宫格的布局添加到购物车中,也可以一一删除。
思考
还有什么可以改进的吗?
数据就这样存放到数组中就完美了吗?
每次用书的数据的时候,直接从数组总取出来确实方便,但是这样真的足够完美吗?
现在的所有代码几乎都在一个文件中,如果项目大了也这样写吗?
上面把对应的数据加载方式分离到文件中了,其他的业务逻辑能不能也分离一下,能不能让项目的结构更加清晰?
待着这些思考,答案是肯定的,项目的代码还是非常耦合的,并且有个致命的缺点。
- 每次从数组中取书的信息时候都自己写key值,self.books[index][@"icon"] 如果这个“icon“手误写错,编辑器一点提示也没有
- 如果书有很多属性,每次手写key值很头痛,并且代码看起来很糟
所以我们需要一种新的方式:因为书是一个对象,可以把它封装成对象,它用有自己的各种属性和方法。这样做有几点好处:
- 便于管理,在使用的时候直接调用属性的get方法就行。
- 系统会自动提示get方法,安全性高不会出错,如果写错系统会报错。
- 代码封装性好,书就是书,而不是每次到数组中去取字典根据对应的key来取值
- 扩展性好,如果日后需要添加新的属性和方法,直接在”书“类中加就行
MVC介绍
经过上面的分析,可以确定的是书应该独立封装起来保存数据,页面逻辑也应该单独管理,至于ViewController恰好就是两者的桥梁。这样的设计模式就是MVC
- M : (Model)数据模型,用来存储和保存数据
- V : (View)UI视图,用来展示给用户看的页面,一些复杂的页面要封装起来放到里面单独管理。
- C: (Controller)控制器,是两者的桥梁,主要用来处理业务逻辑。
使用MVC模式可以很好的简化项目代码,对不同的模块进行封装,降低耦合性,扩展性也会得到提高,MVC是企业开发常用的设计模式。
MVC的分层封装和使用
经过分析可知,在此小项目中,书和展示的书的UI和ViewController对应MVC的关系:
- 书 -- Model:用来封装书的各种属性信息和方法
- 书UI -- View:封装书这个小表象,用于展示给用户看
- ViewController -- Controller:用来处理整体的业务逻辑,优化代码
废话不多说了,上各层的代码
- 下面是书的代码封装:存储书的数据和方法
头文件 XYBook.h
@interface XYBook : NSObject
// 书图标
@property (nonatomic, copy) NSString *icon;
// 书名字
@property (nonatomic, copy) NSString *name;
// 对象方法,返回自己对象
- (instancetype)initWithDict:(NSDictionary *)dict;
// 类方法,返回自己对象
+ (instancetype)bookWithDict:(NSDictionary *)dict;
@end
#import "XYBook.h"
@implementation XYBook
- (instancetype)initWithDict:(NSDictionary *)dict
{
if (self = [super init]) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
+ (instancetype)bookWithDict:(NSDictionary *)dict
{
return [[self alloc] initWithDict:dict];
}
@end
- 书的UI的封装:集中布局,减少控制器代码,优化控制器逻辑
XYBookView.h 头文件
#import <UIKit/UIKit.h>
@class XYBook;
@interface XYBookView : UIView
// 只放一个数据属性用来赋值,内部布局,放到.m 中自己管,不暴露给外界
@property (nonatomic, strong) XYBook *book;
@end
实现文件 .m文件
#import "XYBookView.h"
#include "XYBook.h"
@interface XYBookView ()
@property (nonatomic, weak) UIImageView *icon;
@property (nonatomic, weak) UILabel *label;
@end
@implementation XYBookView
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
// 1. 创建书图标
UIImageView *icon = [UIImageView new];
self.icon = icon;
[self addSubview:self.icon];
// 2.书名
UILabel *bookName = [UILabel new];
bookName.textAlignment = NSTextAlignmentCenter;
self.label = bookName;
[self addSubview:self.label];
}
return self;
}
// 重写布局
- (void)layoutSubviews
{
[super layoutSubviews];
CGSize size = self.frame.size;
self.icon.frame = CGRectMake(0, 0, size.width , size.height * 0.7);
self.label.frame = CGRectMake(0, size.height * 0.7, size.width, size.height *(1 - 0.7));
}
// 设置书的属性
- (void)setBook:(XYBook *)book
{
_book = book;
self.icon.image = [UIImage imageNamed:book.icon];
self.label.text = book.name;
}
@end
- 控制器:不管细节,专注处理逻辑
// 懒加载数据源
- (NSMutableArray *)books
{
if (_books == nil) {
_books = [NSMutableArray array];
// 获取文件路径
NSString *books = [[NSBundle mainBundle] pathForResource:@"bookdata" ofType:@"plist"];
// 加载路径中内容放到数组中
NSMutableArray *arrayM = [NSMutableArray arrayWithContentsOfFile:books];
for (NSDictionary *dict in arrayM) {
XYBook *book = [XYBook bookWithDict:dict];
[_books addObject:book];
}
}
return _books;
}
// 添加书
- (IBAction)addBook:(id)sender {
// 设置列数为 3
int clos = 3;
// 设置书的宽高分别为 W H
CGFloat W = 60;
CGFloat H = 70;
// 0. 创建书
XYBookView *bookView = [XYBookView new];
bookView.backgroundColor = [UIColor redColor];
// 计算书的位置
// 获得索引
NSUInteger index = [self.shopView.subviews count];
// 计算横间距 margin
CGFloat margin = (self.shopView.frame.size.width - clos * W) / (clos - 1);
// 书 frame 的 X
CGFloat x = (index % clos) * (W + margin);
// 书 frame 的 Y
CGFloat y = (index / clos) * (H + margin);
bookView.frame = CGRectMake(x, y, W, H);
[self.shopView addSubview:bookView];
// 给书的UI设置数据
bookView.book = self.books[index];
[self checkState];
self.removeBtn.enabled = YES;
}
// 移除书
- (IBAction)removeBook:(id)sender {
[[self.shopView.subviews lastObject] removeFromSuperview];
[self checkState];
self.addBtn.enabled = YES;
}
以上就是对于本Demo的最终MVC封装版
不同部分各司其职,负责自己的模块
项目的健壮性和封装性也也到了对应的提高
小记
一个简单的九宫格购物车的小Demo,真是麻雀虽小五脏俱全。
关于这个项目的完整代码,欢迎私聊或评论找我要,如果文章有任何问题或有其他技术问题,欢迎随时和我交流。
最后放一张项目效果图