UITextField是常用的文本输入控件,比如我们用的QQ的登录界面,词典输入要查询的单词都使用了文本框控件,如下图所示。之前介绍的UILabel可以在界面中显示文本,但用户无法选择或编辑UILabel中的文本,想接受用户输入文本,就可以使用UITextField控件。当我们在用户界面上点击文本框时,屏幕底部会弹出键盘,用于向文本框中输入文字。
下面的代码演示了如何定制一个输入Emoji图片字符的二级键盘。
说明:由于CSDN博客的代码不支持Emoji符号,如果需要该代码可以点击下载链接进行下载。
运行效果如下图所示。
UIResponder是UIKit框架中的一个抽象类,UIView、UIViewController、UIApplication都是它的子类。UIResponder定义了一系列方法,用于接收和处理用户事件,例如触摸事件、运动事件(摇晃手机)和功能控制事件(编辑文本或播放音乐)等。以上事件中,触摸事件显然应该由被触摸的视图负责处理,系统将触摸事件直接发送给被触摸的视图。其他类型的事件则会由第一响应者(first responder)负责处理,UIWindow有一个firstResponder属性指向第一响应者。如果希望启动应用程序时文本框直接获得焦点显示输入键盘,或者希望点击屏幕其他区域输入键盘消失可以通过下面的方法来设置第一响应者来实现。
Objective-C中有一个非常重要的概念叫协议(相当于Java和C#中的接口),它是实现委托回调的关键,如果一个类遵守这个协议,那么它拥有协议中规定的方法,我们就可以将它的对象设置为被委托放,来帮助委托方完成某些功能。如果一个类遵循UITextFieldDelegate协议,它的对象就可以作为被委托方帮我们处理文本框(委托方)编辑时引发的事件。
UITextFieldDelegate协议的源代码:
@protocol UITextFieldDelegate <NSObject>
@optional
// 可以通过返回NO阻止编辑
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField;
// 当文本框成为第一响应者时执行此方法
- (void)textFieldDidBeginEditing:(UITextField *)textField;
// 返回YES来允许结束编辑
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField;
// 当文本框放弃第一响应者时执行此方法
- (void)textFieldEndEndEditing:(UITextField *)textField;
// 返回NO可以阻止对文本框内容的修改(用于定制文本框输入内容)
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
// 清除文本框中的文本时执行
- (BOOL)textFieldShouldClear:(UITextField *)textField;
// 输入结束返回时执行
- (BOOL)textFieldShouldReturn:(UITextField *)textField;
@end
可以参考上面定制二级键盘的例子来了解如何使用委托回调方法。
需要强调的一个地方是实现在输入完成时关闭键盘,可以重写UITextFieldDelegate协议中的textFieldShouldReturn:方法。如果希望点击应用程序其他部分时键盘消失可以重写UIResponder中的touchesEnded:withEvent:方法,这在上面的例子中已经展示过了。
我们用一个完整的例子把刚才的知识点串联一下,App的运行效果和代码如下所示。
运行效果:
视图控制器的实现文件
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UILabel *greetingLabel = [[UILabel alloc] initWithFrame:CGRectZero];
greetingLabel.frame = CGRectMake(50, 100, 220, 40);
greetingLabel.text = @"Hello, world!";
greetingLabel.textAlignment = NSTextAlignmentCenter;
greetingLabel.textColor = [UIColor redColor];
greetingLabel.font = [UIFont systemFontOfSize:36];
greetingLabel.adjustsFontSizeToFitWidth = YES;
greetingLabel.tag = 101;
UITextField *nameField = [[UITextField alloc]initWithFrame:CGRectZero];
nameField.frame = CGRectMake(50, 200, 220, 40);
// 设置文本框的边框为圆角矩形
nameField.borderStyle = UITextBorderStyleRoundedRect;
// 设置键盘返回键的类型
nameField.returnKeyType = UIReturnKeyDone;
// 设置字体
nameField.font = [UIFont systemFontOfSize:24];
// 设置文本框的占位符
nameField.placeholder = @"请输入您的名字";
nameField.tag = 102;
// 由于视图控制器遵循了UITextFieldDelegate协议,因此可以作为被委托方处理
// 文本框因编辑引发的各种事件,这里是在为文本框设置委托对象
nameField.delegate = self;
[self.view addSubview:greetingLabel];
[self.view addSubview:nameField];
}
// 点击键盘的返回键时回调此方法
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
UILabel *greetingLabel = (id)[self.view viewWithTag:101];
greetingLabel.text = [NSString stringWithFormat:@"Hello, %@", textField.text];
textField.text = @"";
return YES;
}
// 点击其他文本框外的其他区域时让文本框放弃第一响应者(隐藏键盘)
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITextField *nameField = (id)[self.view viewWithTag:102];
[nameField resignFirstResponder];
}
@end
视图控制器是UIViewController类或其子类对象,每个视图控制器都负责管理一个视图层次结构,包含创建视图层次结构中的视图并处理相关用户事件,以及将整个视图层次结构添加到应用窗口。
MVC架构模式:模型-视图-控制器,UIViewController就是MVC模式中的C。
我们先通过代码看看和视图控制器生命周期相关的方法。
- (void)viewDidLoad {
[super viewDidLoad];
// 当控制器管理的视图被装载完成后,调用该方法,
}
- (void)viewWillAppear:(BOOL)animated {
// 当该控制器管理的视图将要显示出来时,系统会自动的调用该方法,
}
- (void)viewDidAppear:(BOOL)animated {
// 当该控制器管理的视图显示出来时,系统会自动的调用该方法
}
- (void)viewWillDisappear:(BOOL)animated {
// 当该控制器管理的视图将要隐藏或将要被移除窗口时,系统会自动的调用该方法
}
-(void)viewDidDisappear:(BOOL)animated {
// 当该控制器管理的视图被隐藏或移除窗口时,系统会自动的调用该方法
}
-(void)viewDidLayoutSubviews {
// 当该控制器管理的视图把它包含的所有子视图排列完成后,系统会自动的调用该方法
}
-(void)viewWillLayoutSubviews {
// 当该控制器管理的视图将要把它包含的所有子视图排列完成后,系统会自动的调用该方法
}
- (void)didReceiveMemoryWarning {
// 内存不足时调用的方法,开发者可在需要时释放一些暂不会使用的对象,进而释放内存
}
说明:loadView和viewDidLoad的区别就是,loadView执行时view还没有生成,viewDidLoad时,view已经生成了,loadView和viewDidLoad通常只会被调用一次,而其他方法可能会被调用多次,当view被添加到其他view中之前,会调用viewWillAppear,之后会调用viewDidAppear。当view从其他view中移除之前,调用viewWillDisAppear,移除之后会调用viewDidDisappear。当view不再使用或受到内存警告时,ViewController可能会将view释放并将其指向为nil。图中的viewDidUnload方法在iOS 6中已经过时,可以在下面的方法中处理收到内存警告时要做的事情。
回调UIViewController对象的didReceiveMemoryWarning方法。
通过模态化的方式切换视图
直接切换视图
通过管理子视图的方式切换视图
用一个视图控制器做容器,将其他的视图控制器加入到容器视图控制器中,再将第一个要呈现的视图控制器的视图贴到容器视图控制器上([self.view addSubiew:self.childViewControllers[0]])。当需要进行视图切换的时候,可以调用容器视图控制器的transitionFromViewController:toViewController:duration:options:animations:completion方法;如果要从容器视图控制器中移除当前视图控制器可以调用removeFromParentViewController方法。
可以在项目的通用设置(General)选项中设置"Device Orientation"勾选允许设备旋转的方向,如下图所示;也可以在info.plist文件设置"Supported interface orientations"键,可以使用的值包括:Portrait(bottom home button)、Landscape(left home button)、Landscape(right home button)、Portrait(top home button)
如果需要程序中的设置支持或禁止旋转,需要重写视图控制器的以下方法:
-supportedInterfaceOrientations方法:支持的旋转方向,可以使用枚举值进行指定,如果需要支持四个方向的转动,可以返回UIInterfaceOrientationMaskAll。
通过代码来旋转
有时候旋转屏幕之后需要对整个内容做出调整,因此我们需要监听视图控制器的旋转。可以通过创建一个通知中心来观察当前设备发生方向的旋转,然后绑定处理旋转事件的回调方法,在回调方法中通过旋转的方向来得到旋转的角度,然后通过视图的transform属性对视图进行变换。代码如下所示:
创建通知中心并添加观察者的代码。原来视图控制器中可以监听旋转的两个方法:willRotateToInterfaceOrientation:duration和didRotateFromInterfaceOrientation在iOS 8中已经废除了。
创建通知中心并添加观察者的代码如下所示:
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:@selector(didRotate) name:UIDeviceOrientationDidChangeNotification object:nil];
回调方法的代码如下所示:
- (void) didRotate {
UIDeviceOrientation orient = [UIDevice currentDevice].orientation;
CGFloat delta = 0;
switch(orient) {
case UIInterfaceOrientationPortrait:
delta = 0;
break;
case UIInterfaceOrientationLandscapeRight:
delta = M_PI_2;
break;
case UIInterfaceOrientationLandscapeLeft:
delta = -M_PI_2;
break;
case UIInterfaceOrientationPortraitUpsideDown:
delta = M_PI;
break;
default:
delta = 0;
}
// NSLog(@"%f", delta);
CGAffineTransform trans = CGAffineTransformMakeRotation(delta);
self.view.transform = trans;
}
1. 及时性传值
(1) 委托传值:提供数据(状态)的视图控制器(称为B)通过委托回调需要数据(状态)的视图控制器(称为A)中的方法。A作为被委托方需要遵循协议,B作为委托方需要一个指向遵循协议的对象的指针,当通过A启动B时,将该指针赋值为self,即由A充当B的委托,B在需要反向传值时通过该指针回调A提供的方法(协议中定义的由A实现的方法)。
(2) Block传值:需要数据的视图控制器(称为A)通过Block将一段代码传入提供数据的视图控制器(称为B)中,当B需要反向传值时回调A传入的代码。
A中创建Block并赋值给B中的Block类型的属性
- (void) okButtonClicked {
// 创建一个Block类型的变量
void (^fn)(UISwitch *) = ^(UISwitch * bt) {
UIButton *okButton = (id)[self.view viewWithTag:101];
[okButton setTitle:(bt.isOn? @"关闭蓝牙":@"打开蓝牙") forState:UIControlStateNormal];
self.flag = bt.isOn;
};
// ... ...
CDModalViewController *modalVC = [[CDModalViewController alloc]init];
modalVC.block = fn; // 为B设置Block属性
[self presentViewController:modalVC animated:YES completion:nil];
}
B需要向A反向传值时调用调用此Block
- (void) switchValueChanged:(UISwitch *)bt {
if (self.block) {
self.block(bt);
}
}
(3) 通知传值:需要数据的视图控制器(称为A)注册一个观察者,提供数据的视图控制器(称为B)向观察者发送通知完成反向传值,这是对观察者模式的应用。
A通过消息中心注册观察者
- (void)viewDidLoad {
[super viewDidLoad];
// ... ...
// A通过通知中心注册一个观察者
// 第一个参数是A自身,第二个参数是收到通知的回调方法
// 第三个参数是通知的名称,与通知发送方设置的通知名称一致
// 第四个参数是接受哪个对象发送的通知,如果为nil则不管哪个对象发送通知都接受
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showSwitchStatus:) name:@"SwitchValueChanged" object:nil];
}
// 收到通知后的回调方法
- (void) showSwitchStatus:(NSNotification *) not {
UIButton *okButton = (id)[self.view viewWithTag:101];
UISwitch *currentSwitch = not.object;
self.flag = currentSwitch.isOn;
[okButton setTitle:(self.flag? @"关闭蓝牙":@"打开蓝牙") forState:UIControlStateNormal];
}
在事件发生时向观察者发出通知
- (void) switchValueChanged:(UISwitch *)bt {
// B创建通知对象,其中第一个参数是通知的名称,
// 第二个参数是和通知绑定的对象
NSNotification *note = [[NSNotification alloc] initWithName:@"SwitchValueChanged" object:bt userInfo:nil];
// B发送通知
[[NSNotificationCenter defaultCenter] postNotification:note];
}
2. 非及时性传值
(1) AppDelegate传值
#import
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic, copy) NSString *myText;
@end
AppDelegate *appDel = [UIApplication sharedApplication].delegate;
// 获得AppDelegate对象后就可以操作myText属性完成赋值或取值操作
// 由于这种传值方式属于非及时性传值需要通过代码完成对视图的刷新
(2) 单例模式
单例模式对于熟悉面向对象和设计模式的人来说肯定不会陌生,如果使用单例对象来保存要传递的值,那么不同的视图之间就可以通过单例对象获得或修改这些值,道理跟使用AppDelegate传值完全一样。
补充:单例模式是一种常用的设计模式,它让一个类只能创建出唯一的对象,很多应用中都需要这种独一无二的对象,我们也可以将单例对象作为共享状态的对象来实现传值。实现代理模式的关键是不提供公开的初始化方法(构造方法),通过一个类方法向外界返回该类的唯一实例。