UIView
经过前几天的快速学习,我们初步了解的IOS开发的一些知识,中间因为拉的太急,忽略了很多基础知识点,这些知识点单独拿出来学习太过枯燥,我们在今后的项目中再逐步补齐,今天我们来学习APP视图相关知识。
视图即UIView对象,我们上次用的按钮UIButton、UILabel或者UITableView皆是其子类;视图知道如何绘制自己与前端一致有一个层次的概念。
任何一个应用都会有一个UIWindow对象,与浏览器对象一致,他作为容器角色而存在,负责装载所有的视图控件,每个加入的视图便是一个子视图:subview,视图嵌套便形成了我们看到的APP界面,这点类似与html的dom结构。
视图的绘制分为两步:
① 层次结构上的每个视图(包括根视图UIWindow)分别绘制自身,视图将自己绘制到图层(layer)上,每个UIView都拥有一个layer属性,指向一个CALayer对象
② 所有视图的图像最终组成为一幅图像,绘制到屏幕上
为了更好的了解UIView的相关知识,我们这里做一个简单的计算器来说明其知识点吧。
其实说明这个知识点我可以选择很多项目之所以会选择计算器是因为内部会碰到数字与字符串转换,四则运算问题,也会遇到控件绑定事件等行为,帮助我们巩固基础吧。
这里新建一个单页应用:Calculator-app,自从昨天我知道了拖控件居然需要以拖动的方式建立关联后,我便决定不再使用拖的方式,我们这里使用代码生成界面,所以今天的目的是:
① 了解OC的数据类型转换
② 了解UIView的知识
③ 了解如何代码构建视图并且绑定事件
至于计算器的逻辑,不必太过在意
画圆
首先,我们在我们的UIView中画一个同心圆来了解UIView的相关知识,因为OC并没有提供绘制同心圆的视图对象,该知识点可以帮助我们更好的了解UIView。
新建一个UIView的子类:MyUICircle
每个UIView的子类有两个方法需要我们关注,第一个为初始化方法:
MyUICircle *circle = [MyUICircle alloc] initWithFrame:<#(CGRect)#>
其中initWithFrame是继承下来的初始化方法,带有一个CGRect类型的参数,该参数会赋予UIView的frame属性:
@property (nonatomic) CGRect frame;
CGRect包含另外两个结构,origin=>x, y; size=>width, height。四个都是float的基本结构,我们在全局控制器中试试其作用:
#import "ViewController.h"
#import "MyUICircle.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad {
[super viewDidLoad]; //因为CGRect不是OC对象,所以不能接收OC的消息,也没有必要接收消息,一般这样建立一个OCRect对象
//注意,这里不是指针
CGRect frame = CGRectMake(, , , ); //创建基本视图对象
//这里已经确定好了坐标与尺寸
MyUICircle *circle = [[MyUICircle alloc] initWithFrame:frame]; //给一个背景,这里使用了颜色类的静态方法
circle.backgroundColor = [UIColor redColor]; //将视图加入主视图中
[self.view addSubview:circle]; } - (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
} @end
因为MyUICircle是一个空类,什么都没有做便没有贴代码了,这个是视图对应的主控制器,其与视图如何建立关联我们暂不关注,他提供一个方法viewDidLoad处理视图准备好的初始化动作,我们便将创建好的视图加入了其中,大家请看这块红斑:
这个时候的层次结构是
UIWIndow包含多个UIView,这里主UIView还包含一个MyUICircle对象,每个UIView可以根据superview属性找到自己的父对象
drawRect
drawRect为从UIView继承下来的第二个需要关注的方法,他负责在页面上绘图,我们可以重写drawRect达到自定义绘图的功能。
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
UIView的子类具有两个相似的属性,bounds与frame,我们都知道frame是用于视图本身布局用的,而bounds定义了一个矩形范围,表示视图的绘制区域。
视图在绘制时,会参考一个坐标系,bounds表示矩形位于自己的坐标系,而frame表示矩形位于父视图的坐标系,两个矩形大小是相等的。
-(CGRect)frame{
return CGRectMake(self.frame.origin.x,self.frame.origin.y,self.frame.size.width,self.frame.size.height);
}
-(CGRect)bounds{
return CGRectMake(,,self.frame.size.width,self.frame.size.height);
}
bounds是相对于左上角位置的,不太好理解就写demo,我这里新建了一个子View帮助理解
- (void)viewDidLoad {
[super viewDidLoad]; //因为CGRect不是OC对象,所以不能接收OC的消息,也没有必要接收消息,一般这样建立一个OCRect对象
//注意,这里不是指针
CGRect frame = CGRectMake(, , , ); //创建基本视图对象
//这里已经确定好了坐标与尺寸
MyUICircle *circle = [[MyUICircle alloc] initWithFrame:frame]; //给一个背景,这里使用了颜色类的静态方法
circle.backgroundColor = [UIColor redColor]; //circle.bounds = CGRectMake(200, 200, 100, 100); UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(, , , )];
view2.backgroundColor = [UIColor yellowColor];
[circle addSubview: view2]; //将视图加入主视图中
[self.view addSubview:circle]; }
可以看到,子view是相对于父View布局的,这里与我们的absolute一致,如果改变父View的bounds呢:
- (void)viewDidLoad {
[super viewDidLoad]; //因为CGRect不是OC对象,所以不能接收OC的消息,也没有必要接收消息,一般这样建立一个OCRect对象
//注意,这里不是指针
CGRect frame = CGRectMake(, , , ); //创建基本视图对象
//这里已经确定好了坐标与尺寸
MyUICircle *circle = [[MyUICircle alloc] initWithFrame:frame]; //给一个背景,这里使用了颜色类的静态方法
circle.backgroundColor = [UIColor redColor]; //父类设置该参数造成的影响
circle.bounds = CGRectMake(, , , ); UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(, , , )];
view2.backgroundColor = [UIColor yellowColor];
[circle addSubview: view2]; //与view2参照物
UIView *view3 = [[UIView alloc] initWithFrame:CGRectMake(, , , )];
view3.backgroundColor = [UIColor grayColor];
[self.view addSubview: view3]; //将视图加入主视图中
[self.view addSubview:circle]; }
可以看到,view2相对于父元素最初位置左移了10像素,所以修改了一个view的bounds边上修改了本地坐标的原点位置,最初的原点是0, 0。
那里view1修改了原点坐标让其右偏移了10像素,所以对应的子view便需要左偏,我理解下来是设置后将当前右上角的坐标变成了10像素,大概是这样吧。
这里为什么要强调这个bounds本地坐标呢,因为以后拖动相关的需求都会根据他做计算,这块有点绕,我们后续真实项目遇到再来纠结他吧,现在也说不清。
正式画圆
如果我们期待画一个铺满屏幕最大的圆,便需要通过bounds属性找到中心点:
- (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; //计算中心点
CGPoint center;
center.x = bounds.origin.x + bounds.size.width / 2.0;
center.y = bounds.origin.y + bounds.size.height / 2.0; //根据宽高计算较小值的半径
float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); }
有了中心点与半径便可以画圆了,这里使用UIBezierPath类绘制圆形:
- (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; //计算中心点
CGPoint center;
center.x = bounds.origin.x + bounds.size.width / 2.0;
center.y = bounds.origin.y + bounds.size.height / 2.0; //根据宽高计算较小值的半径
float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); UIBezierPath *path = [[UIBezierPath alloc] init]; //这里去看api吧,他们都说ios门槛高,原来什么都是英文啊
[path addArcWithCenter:center radius:radius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; //开始绘制
[path stroke];
}
于是到此圆基本就出来了,至此我们对UIView有了一个基本的了解,接下来就让我们开始真实的计算器逻辑了吧
计算器
最简单的计算器拥有10个数字,+-*/,小数点,重置键,一个显示结果的文本框,并不复杂,我们这里便使用代码设计其UI。
首先,我们将数字画出来,根据前端的经验,数字按钮肯定是一个循环组成,dom上会留下一些标识,并且给父元素绑定事件即可,但是ios我们并不熟悉,所以先用最土的方法是画10个数字,在此之前,我们先简单研究下UIButton控件。
UIButton
这一小节参考了:http://justcoding.iteye.com/blog/1467999
UIButton是一个标准的UIController,所谓UI控件便是对UIView的的增强,也就是说UI控件一般继承至UIView,所以View的一些统一特性都被继承了下来了。
继承下来的属性
enabled,控件是否可用,与html控件一致,禁用后仍然可见,只是不可点击。
selected,当用户选中控件时,控件的selected属性便为YES。
contentVerticalAlignment,控件在垂直方向布置自身内容的方式,以下是可设置的值:
① UIControlContentVerticalAlignmentCenter
② UIControlContentVerticalAlignmentTop
③ UIControlContentVerticalAlignmentBottom
④ UIControlContentVerticalAlignmentFill
contentHorizontalAligment,为水平对齐的方式,可设置的值为:
① UIControlContentHorizontalAlignmentCenter
② UIControlContentHorizontalAlignmentTop
③ UIControlContentHorizontalAlignmentBottom
④ UIControlContentHorizontalAlignmentFill
事件通知
UIControll提供一个标准机制,来进行事件订阅和发布,当控件触发特定事件后,便会触发对应回调。
[myControl addTarget: myDelegate action:@selector(myActionmethod:) forControlEvents:UIControlEventValueChanged ];
可以使用addTarget绑定多个事件,这类事件一般来说都是标准的,比如js中的click事件,move等事件
UIControlEventTouchDown,点击事件,类似与js中的click
UIControlEventTouchDownRepeat,当点击数大于1时触发,类似doubleclick
UIControlEventTouchDraglnside,当触摸控件在窗口滚动时触发
UIControlEventTouchChanged,当控件的值发生变化时触发
UIControlEventEditingDidBegin,当开始编辑时触发,类似获取焦点吧
UIControlEventEditingChanged,当文本控件中的文本被改变时发送通知。
UIControlEventEditingDidEnd,当文本控件中编辑结束时发送通知。
......
与addTarget方法对应的是removeTarget,表达的是删除事件
使用allTargets获取一个控件的所有事件列表
之前是关于UIButton父类的事件,这里我们回到UIButton,首先说下两种初始化方法:
initWithFrame
UIButton *tmp;
CGRect frame; frame = CGRectMake(, , , );
tmp = [[UIButton alloc] initWithFrame:frame];
buttonWithType
这个是UIButton一个特有的类方法
UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeRoundedRect]; typedef enum {
UIButtonTypeCustom = , // no button type 自定义,无风格
UIButtonTypeRoundedRect, // rounded rect, flat white button, like in address card 白色圆角矩形,类似偏好设置表格单元或者地址簿卡片
UIButtonTypeDetailDisclosure,//蓝色的披露按钮,可放在任何文字旁
UIButtonTypeInfoLight,//微件(widget)使用的小圆圈信息按钮,可以放在任何文字旁
UIButtonTypeInfoDark,//白色背景下使用的深色圆圈信息按钮
UIButtonTypeContactAdd,//蓝色加号(+)按钮,可以放在任何文字旁
} UIButtonType;
属性
这种方法创建是没有位置信息的,所以需要设置frame值:
CGRect btn2Frame = CGRectMake(10.0, 10.0, 60.0, 44.0);
btn2.frame =btn2Frame;
除此之外,我们需要设置按钮的文字,采用setTitle,带一个字符串和一个当前按钮的状态:
[btn1 setTitle:@"BTN1" forState:UIControlStateNormal];
我们也可以使用一个图片作为按钮:
[btn2 setImage:[UIImage imageNamed:@"pic"] forState:UIControlStateNormal];
最后可以为每种按钮设置标题的颜色和阴影,以及背景,
[btn1 setTitleColor:[UIColor redColor] forState:UIControlStateNormal];//设置标题颜色
[btn1 setTitleShadowColor:[UIColor grayColor] forState:UIControlStateNormal ];//阴影
[btn1 setBackgroundImage:[UIImage imageNamed:@"PIC"] forState:UIControlStateHighlighted];//背景图像
forstate决定了按钮将在何种状态下显示:
enum {
UIControlStateNormal = , //常态
UIControlStateHighlighted = << , // used when UIControl isHighlighted is set 高亮
UIControlStateDisabled = << , //禁用
UIControlStateSelected = << , // flag usable by app (see below) 选中
UIControlStateApplication = 0x00FF0000, // additional flags available for application use 当应用程序标志使用时
UIControlStateReserved = 0xFF000000 // flags reserved for internal framework use 为内部框架预留的
};
typedef NSUInteger UIControlState;
当按钮高亮或者禁用,UIButton可以调整自己的样式
添加事件(动作)
按钮还是需要绑定事件,他的事件绑定直接继承自UIView:
-(void)btnPressed:(id)sender{
UIButton* btn = (UIButton*)sender;
//开始写你自己的动作
}
[btn1 addTarget:self action:@selector(btnPressed:) forControlEvents:UIControlEventTouchUpInside];
这里我们再次回到计算器,我们首先生成数字的UI,然后为数字绑定事件,点击每个按钮在后台打印出数字:
- (void)onNumClick:(id)sender
{
self.msg.text = [sender currentTitle];
} - (void)viewDidLoad {
[super viewDidLoad]; //生成10个数字先,一行4个,一字排开
UIButton *tmp;
CGRect frame;
self.msg = [[UILabel alloc] initWithFrame:CGRectMake(, , , )];
[self.view addSubview:self.msg]; self.msg.text = @"数字"; for (int i = ; i < ; i++) {
frame = CGRectMake(, + * i, , );
tmp = [[UIButton alloc] initWithFrame:frame];
[tmp setTitle:[NSString stringWithFormat:@"%d", i] forState:UIControlStateNormal]; //不设置背景就是白色就看不到啦
tmp.backgroundColor = [UIColor grayColor]; //这里绑定事件,点击每个数字便重置msg
[tmp addTarget:self action:@selector(onNumClick:) forControlEvents:UIControlEventTouchDown]; [self.view addSubview:tmp];
}
}
如此一来,我们简陋的界面就出来了:
其中事件绑定的回调函数的sender参数,应该对应js函数中的e,在此我们知道了如何布局以及绑定事件,于是我们将界面稍微美化点,因为我这里用到了OC的字典,这里先插一段字典的知识吧。
NSDictionary
NSDictionary对应JS中的对象字面量,自从json对象出来后,这个对象非常重要,估计是逃不掉的啦。
NSDictionary代表不可变字典,意思是一旦初始化结束就不能增删元素了,这是其初始化的方法:
//创建多个字典
NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys:
@"value1", @"key1",
@"value2", @"key2",
@"value3", @"key3",
@"value4", @"key4",
nil]; //根据现有的字典创建字典
NSDictionary *dic2 = [NSDictionary dictionaryWithDictionary:dic];
NSLog(@"dic2 :%@", dic2); //根据key获取value
NSLog(@"key3 value :%@", [dic objectForKey:@"key3"]); //获取字典数量
NSLog(@"dic count :%d", [dic count]); //所有的键集合
NSArray *keys = [dic allKeys];
NSLog(@"keys :%@", keys); //所有值集合
NSArray *values = [dic allValues];
NSLog(@"values :%@", values);
-- ::35.540 Calculator-app[:] dic2 :{
key1 = value1;
key2 = value2;
key3 = value3;
key4 = value4;
}
-- ::35.540 Calculator-app[:] key3 value :value3
-- ::35.540 Calculator-app[:] dic count :
-- ::35.541 Calculator-app[:] keys :(
key3,
key1,
key4,
key2
)
-- ::35.541 Calculator-app[:] values :(
value3,
value1,
value4,
value2
)
NSMutableDictionay
NSMutableDictionary是NSDictionary的子类,所以继承了NSDictionary的所有特性,并且可以使用方法动态添加:
[dictionary setObject:@"value" forKey:@"key"];
这里我们回到我们的计算器,因为我们知道到底有多少字符,所以这里直接使用不可变字典即可,但是最后转念一想好像数组也是可行的,所以就数组吧......
因为这里是学习ios开发,各位就不要在意小数点之类的细节了,至此基本UI便出来了,虽然很丑:
#import <UIKit/UIKit.h> @interface ViewController : UIViewController @property(retain,nonatomic) UILabel *msg; -(void) initLayout; @end #import "ViewController.h"
#import "MyUICircle.h" @interface ViewController () @end @implementation ViewController //计算机布局
//思考,感觉这里没有做到很好的分离
-(void) initLayout{
//生成10个数字先,一行4个,一字排开
UIButton *tmp;
CGRect frame;
self.msg = [[UILabel alloc] initWithFrame:CGRectMake(, , , )];
[self.view addSubview:self.msg];
self.msg.text = @"数字"; NSMutableArray *array = [[NSMutableArray alloc] init]; //插入5个数字,这里发生了装箱
for (int i = ; i < ; i++) {
NSString *n = [NSString stringWithFormat:@"%d", i];
[array addObject:n];
} [array addObject: @"+"];
[array addObject: @"-"];
[array addObject: @"*"];
[array addObject: @"/"];
[array addObject: @"="];
[array addObject: @"c"]; for(int i = ; i < ; i++) {
for(int j = ; j < ; j++){
if(i * + j > [array count] - ) break; frame = CGRectMake( + j * , + * i, , ); tmp = [[UIButton alloc] initWithFrame:frame];
[tmp setTitle: [array objectAtIndex:(i * + j)] forState:UIControlStateNormal]; //不设置背景就是白色就看不到啦
tmp.backgroundColor = [UIColor grayColor]; //这里绑定事件,点击每个数字便重置msg
[tmp addTarget:self action:@selector(onNumClick:) forControlEvents:UIControlEventTouchDown]; [self.view addSubview:tmp]; }
}
} - (void)onNumClick:(id)sender {
self.msg.text = [sender currentTitle];
} - (void)viewDidLoad {
[super viewDidLoad];
[self initLayout];
} - (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
} @end
这个时候我们来简单写一下其业务逻辑,逻辑肯定有问题,简单看功能就好:
#import <UIKit/UIKit.h> @interface ViewController : UIViewController @property(retain,nonatomic) UILabel *msg; //存取字符串显示的字符串
@property(retain,nonatomic) NSMutableString *resultStr; //存储最近一次有效的指令
@property(retain,nonatomic) NSMutableString *commondStr; @property(assign, nonatomic) int num1;
@property(assign,nonatomic) int num2; -(void) initLayout; -(void) calculatorNum; @end #import "ViewController.h"
#import "MyUICircle.h" @interface ViewController () @end @implementation ViewController //计算机布局
//思考,感觉这里没有做到很好的分离
-(void) initLayout{
//生成10个数字先,一行4个,一字排开
UIButton *tmp;
CGRect frame;
self.msg = [[UILabel alloc] initWithFrame:CGRectMake(, , , )];
[self.view addSubview:self.msg]; _resultStr = [[NSMutableString alloc]initWithString:@""];
_commondStr = [[NSMutableString alloc]initWithString:@""]; self.msg.text = _resultStr; NSMutableArray *array = [[NSMutableArray alloc] init]; //插入5个数字,这里发生了装箱
for (int i = ; i < ; i++) {
NSString *n = [NSString stringWithFormat:@"%d", i];
[array addObject:n];
} [array addObject: @"+"];
[array addObject: @"-"];
[array addObject: @"*"];
[array addObject: @"/"];
[array addObject: @"="];
[array addObject: @"c"]; for(int i = ; i < ; i++) {
for(int j = ; j < ; j++){
if(i * + j > [array count] - ) break; frame = CGRectMake( + j * , + * i, , ); tmp = [[UIButton alloc] initWithFrame:frame];
[tmp setTitle: [array objectAtIndex:(i * + j)] forState:UIControlStateNormal]; //不设置背景就是白色就看不到啦
tmp.backgroundColor = [UIColor grayColor]; //这里绑定事件,点击每个数字便重置msg
[tmp addTarget:self action:@selector(onNumClick:) forControlEvents:UIControlEventTouchDown]; [self.view addSubview:tmp]; }
}
} - (void)onNumClick:(id)sender { NSString *tmp = [sender currentTitle];
int flag = ; //此处监控输入,适合使用switch
if([tmp isEqualToString:@"c"]){
_resultStr = [[NSMutableString alloc]initWithString:@""];
//点击数字则重置
_commondStr = [[NSMutableString alloc]initWithString:@""]; } else if ([tmp isEqualToString:@"+"] || [tmp isEqualToString:@"-"] || [tmp isEqualToString:@"*"] || [tmp isEqualToString:@"/"]){
flag = ;
//如果之前已经有了命令,需要先显示计算结果,再给予新的命令
if([_commondStr isEqualToString:@""]) {
_commondStr = tmp;
} else {
_num2 = [_resultStr intValue];
[self calculatorNum ];
_num1 = ;
_num2 = ;
} } else if ([tmp isEqualToString:@"="]){
_num2 = [_resultStr intValue];
[self calculatorNum ];
_num1 = ;
_num2 = ;
} else {
//这个时候是输入数字的情况
if([_resultStr isEqualToString:@""]) {
_resultStr = [[NSMutableString alloc]initWithString:@""];
}
[_resultStr appendString:tmp];
} //第一次输入命令,便记录为第一个数字
if(flag == ) {
_num1 = [_resultStr intValue];
_resultStr = [[NSMutableString alloc]initWithString:@""];
} self.msg.text = _resultStr; } - (void)calculatorNum {
int r;
if ([_commondStr isEqualToString:@"+"]){
r = _num1 + _num2;
} else if([_commondStr isEqualToString:@"-"]) {
r = _num1 - _num2;
} else if([_commondStr isEqualToString:@"*"]) {
r = _num1 * _num2;
} else if([_commondStr isEqualToString:@"/"]) {
//不要在意那些逻辑了
r = _num1 / _num2;
} _commondStr = [[NSMutableString alloc]initWithString:@""];
_resultStr = [NSString stringWithFormat:@"%d", r]; } - (void)viewDidLoad {
[super viewDidLoad];
[self initLayout];
} - (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
} @end