第8章 按钮和标签
在本章前面的内容中,已经讲解了文本控件和文本视图控件的基本知识,本章将进一步讲解iOS的基本控件。在本章的内容中,将详细介绍iOS应用中的按钮控件和标签控件的基本知识,为读者步入本书后面知识的学习打下基础。
8.1 标签(UILabel)
在iOS应用中,使用标签(UILabel)可以在视图中显示字符串,这一功能是通过设置其text属性实现的。标签中可以控制文本的属性有很多,例如字体、字号、对齐方式以及颜色。通过标签可以在视图中显示静态文本,也可显示我们在代码中生成的动态输出。在本节的内容中,将详细讲解标签控件的基本用法。
8.1.1 标签(UILabel)的属性
标签(UILabel)有如下5个常用的属性。
(1)font属性:设置显示文本的字体。
(2)size属性:设置文本的大小。
(3)backgroundColor属性:设置背景颜色,并分别使用如下3个对齐属性设置了文本的对齐方式。
UITextAlignmentLeft:左对齐。
UITextAlignmentCenter:居中对齐。
UITextAlignmentRight:右对齐。
(4)textColor属性:设置文本的颜色。
(5)adjustsFontSizeToFitWidth属性:如将adjustsFontSizeToFitWidth的值设置为YES,表示文本文字自适应大小。
8.1.2 实战演练——使用标签(UILabel)显示一段文本
在本节下面的内容中,将通过一个简单的实例来说明使用标签(UILabel)的方法。
(1)新打开Xcode,建一个名为“UILabelDemo”的“Single View Applicatiom”项目,如图8-1所示。
图8-1 新建Xcode项目
(2)设置新建项目的工程名,然后设置设备为“iPhone”,如图8-2所示。
图8-2 设置设备
(3)设置一个界面,整个界面为空,效果如图8-3所示。
图8-3 空界面
(4)编写文件 ViewController.m,在此创建了一个UILabel对象,并分别设置了显示文本的字体、颜色、背景颜色和水平位置等。并且在此文件中使用了自定义控件UILabelEx,此控件可以设置文本的垂直方向位置。文件 ViewController.m的实现代码如下所示:
- (void)viewDidLoad
{
[superviewDidLoad];
#if 0
//创建
- (void)viewDidLoad
{
[superviewDidLoad];
#if 0
//创建UIlabel对象
UILabel* label = [[UILabel alloc]
initWithFrame:self.view.bounds];
//设置显示文本
label.text = @"This is a UILabel Demo,";
//设置文本字体
label.font = [UIFont fontWithName:@"Arial" size:35];
//设置文本颜色
label.textColor = [UIColor yellowColor];
//设置文本水平显示位置
label.textAlignment = UITextAlignmentCenter;
//设置背景颜色
label.backgroundColor = [UIColor blueColor];
//设置单词折行方式
label.lineBreakMode = UILineBreakModeWordWrap;
//设置label是否可以显示多行,0则显示多行
label.numberOfLines = 0;
//根据内容大小,动态设置UILabel的高度
CGSize size = [label.text sizeWithFont:label.font constrainedToSize:self.view.
bounds.size lineBreakMode:label.lineBreakMode];
CGRect rect = label.frame;
rect.size.height = size.height;
label.frame = rect;
#endif
#if 1
//使用自定义控件UILabelEx,此控件可以设置文本的垂直方向位置
UILabelEx* label = [[UILabelExalloc] initWithFrame:self.view.bounds];
label.text = @"This is a UILabel Demo,";
label.font = [UIFontfontWithName:@"Arial"size:35];
label.textColor = [UIColoryellowColor];
label.textAlignment = UITextAlignmentCenter;
label.backgroundColor = [UIColorblueColor];
label.lineBreakMode = UILineBreakModeWordWrap;
label.numberOfLines = 0;
label.verticalAlignment = VerticalAlignmentTop;//设置文本垂直方向顶部对齐
#endif
//将label对象添加到view中,这样才可以显示
[self.view addSubview:label];
[label release];
}
(5)接下来开始看自定义控件UILabelEx的实现过程。首先在文件UILabelEx.h中定义一个枚举类型,在里面分别设置了顶部、居中和底部对齐3种类型。具体代码如下所示:
#import <UIKit/UIKit.h>
//定义一个枚举类型,顶部,居中,底部对齐,3种类型
typedef enum {
VerticalAlignmentTop,
VerticalAlignmentMiddle,
VerticalAlignmentBottom,
} VerticalAlignment;
@interface UILabelEx : UILabel
{
VerticalAlignment _verticalAlignment;
}
@property (nonatomic, assign) VerticalAlignment verticalAlignment;
@end
然后看文件 UILabelEx.m,在此设置了文本显示类型,并重写了两个父类。具体代码如下所示:
@implementation UILabelEx
@synthesize verticalAlignment = _verticalAlignment;
-(id) initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
self.verticalAlignment = VerticalAlignmentMiddle;
}
return self;
}
//设置文本显示类型
-(void) setVerticalAlignment:(VerticalAlignment)verticalAlignment
{
_verticalAlignment = verticalAlignment;
[selfsetNeedsDisplay];
}
//重写父类(CGRect) textRectForBounds:(CGRect)bounds
limitedToNumberOfLines:(NSInteger)numberOfLines
-(CGRect) textRectForBounds:(CGRect)bounds
limitedToNumberOfLines:(NSInteger)numberOfLines
{
CGRect textRect = [supertextRectForBounds:bounds
limitedToNumberOfLines:numberOfLines];
switch (self.verticalAlignment) {
caseVerticalAlignmentTop:
textRect.origin.y = bounds.origin.y;
break;
caseVerticalAlignmentBottom:
textRect.origin.y = bounds.origin.y + bounds.size.height -
textRect.size.height;
break;
caseVerticalAlignmentMiddle:
default:
textRect.origin.y = bounds.origin.y + (bounds.size.height -
textRect.size.height) / 2.0;
}
return textRect;
}
//重写父类 -(void) drawTextInRect:(CGRect)rect
-(void) drawTextInRect:(CGRect)rect
{
CGRect realRect = [selftextRectForBounds:rect
limitedToNumberOfLines:self.numberOfLines];
[super drawTextInRect:realRect];
}
@end
这样整个实例讲解完毕,执行后的效果如图8-4所示。
图8-4 执行效果
8.1.3 实战演练——在屏幕中显示指定字体和指定大小的文本
在iOS应用中,使用UILabel控件可以在屏幕中显示文本。在本实例中,使用UILabel控件的font属性设置了显示文本的字体,并使用其size属性设置了文本的大小。
实例文件UIKitPrjSimple.m的具体实现代码如下所示:
#import "UIKitPrjSimple.h"
@implementation UIKitPrjSimple
- (void)viewDidLoad {
[super viewDidLoad];
UILabel* label = [[[UILabel alloc] init] autorelease];
label.frame = self.view.bounds;
label.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
label.text = @"good";
label.textAlignment = UITextAlignmentCenter;
label.backgroundColor = [UIColor blackColor];
label.textColor = [UIColor whiteColor];
label.font = [UIFont fontWithName:@"Zapfino" size:48];
[self.view addSubview:label];
}
@end
执行后的效果如图8-5所示。
图8-5 执行效果
8.1.4 实战演练——设置屏幕中文本的对齐方式
本实例还是使用了UILabel控件,首先在屏幕中显示了3段文本。然后使用 backgroundColor设置了背景颜色,并分别使用如下3个对齐属性设置了文本的对齐方式。
UITextAlignmentLeft:左对齐。
UITextAlignmentCenter:居中对齐。
UITextAlignmentRight:右对齐。
实例文件UIKitPrjAlignment.m的具体实现代码如下所示:
#import "UIKitPrjAlignment.h"
@implementation UIKitPrjAlignment
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"UITextAlignment";
self.view.backgroundColor = [UIColor blackColor];
UILabel* label1 = [[[UILabel alloc] initWithFrame:CGRectMake( 0, 10, 320, 30 )]
autorelease];
UILabel* label2 = [[[UILabel alloc] initWithFrame:CGRectMake( 0, 50, 320, 30 )]
autorelease];
UILabel* label3 = [[[UILabel alloc] initWithFrame:CGRectMake( 0, 90, 320, 30 )]
autorelease];
label1.textAlignment = UITextAlignmentLeft;
label2.textAlignment = UITextAlignmentCenter;
label3.textAlignment = UITextAlignmentRight;
label1.text = @"UITextAlignmentLeft";
label2.text = @"UITextAlignmentCenter";
label3.text = @"UITextAlignmentRight";
[self.view addSubview:label1];
[self.view addSubview:label2];
[self.view addSubview:label3];
}
@end
执行后的效果如图8-6所示。
图8-6 执行效果
8.2 按钮(UIButton)
在iOS应用中,最常见的与用户交互的方式是检测用户轻按按钮(UIButton)并对此作出反应。按钮在iOS中是一个视图元素,用于响应用户在界面中触发的事件。按钮通常用Touch Up Inside事件来体现,能够抓取用户用手指按下按钮并在该按钮上松开发生的事件。当检测到事件后,便可能能触发相应视图控件中的操作(IBAction)。在本节的内容中,将详细讲解按钮控件的基本知识。
8.2.1 按钮基础
按钮有很多用途,例如在游戏中触发动画特效,在表单中触发获取信息。虽然到目前为止我们只使用了一个圆角矩形按钮,但通过使用图像可赋予它们以众多不同的形式。其实在iOS中可以实现样式各异的按钮效果,并且市面中诞生了各种可用的按钮控件,例如图8-7显示了一个奇异效果的按钮。
图8-7 奇异效果的按钮
在iOS应用中,使用UIButton控件可以实现不同样式的按钮效果。通过使用方法 ButtonWithType可以指定几种不同的UIButtonType的类型常量,用不同的常量可以显示不同外观样式的按钮。UIButtonType属性指定了一个按钮的风格,其中有如下几种常用的外观风格。
UIButtonTypeCustom:无按钮的样式。
UIButtonTypeRoundedRect:一个圆角矩形样式的按钮。
UIButtonTypeDetailDisclosure:一个详细披露按钮。
UIButtonTypeInfoLight:一个信息按钮,有一个浅色背景。
UIButtonTypeInfoDark:一个信息按钮,有一个黑暗的背景。
UIButtonTypeContactAdd:一个联系人添加按钮。
另外,通过设置Button控件的setTitle:forState:方法可以设置按钮的状态变化时标题字符串的变化形式。例如setTitleColor:forState:方法可以设置标题颜色的变化形式,setTitleShadowColor:forState:方法可以设置标题阴影的变化形式。
8.2.2 实战演练——按下按钮后触发一个事件
在本实例中,设置了一个“危险!请勿触摸!”按钮,按下按钮后会执行buttonDidPush方法,弹出一个对话框,在对话框中显示“哈哈,这是一个笑话!!”。
实例文件 UIKitPrjButtonTap.m的具体实现代码如下所示。
#import "UIKitPrjButtonTap.h"
@implementation UIKitPrjButtonTap
- (void)viewDidLoad {
[super viewDidLoad];
UIButton* button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[button setTitle:@"危险!请勿触摸!" forState:UIControlStateNormal];
[button sizeToFit];
[button addTarget:self
action:@selector(buttonDidPush)
forControlEvents:UIControlEventTouchUpInside];
button.center = self.view.center;
button.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin |
UIViewAutoresizingFlexibleRightMargin |
UIViewAutoresizingFlexibleTopMargin |
UIViewAutoresizingFlexibleBottomMargin;
[self.view addSubview:button];
}
- (void)buttonDidPush {
UIAlertView* alert = [[[UIAlertView alloc] init] autorelease];
alert.message = @"哈哈,这是一个笑话!!";
[alert addButtonWithTitle:@"OK"];
[alert show];
}
@end
执行后的效果如图8-8所示。
图8-8 执行效果
8.2.3 实战演练——在屏幕中显示不同的按钮
在iOS应用中,使用Button控件可以实现不同样式的按钮效果。通过使用方法 ButtonWithType可以指定几种不同的UIButtonType的类型常量,使用不同的常量显示不同外观样式的按钮。在本实例中,在屏幕中演示了各种不同外观样式按钮。
实例文件 UIKitPrjButtonWithType.m的具体实现代码如下所示:
#import "UIKitPrjButtonWithType.h"
static const CGFloat kRowHeight = 80.0;
#pragma mark ----- Private Methods Definition -----
@interface UIKitPrjButtonWithType ()
- (UIButton*)buttonForThisSampleWithType:(UIButtonType)type;
@end
#pragma mark ----- Start Implementation For Methods -----
@implementation UIKitPrjButtonWithType
- (void)dealloc {
[dataSource_ release];
[buttons_ release];
[super dealloc];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.rowHeight = kRowHeight;
dataSource_ = [[NSArray alloc] initWithObjects:
@"Custom",
@"RoundedRect",
@"DetailDisclosure",
@"InfoLight",
@"InfoDark",
@"ContactAdd",
nil ];
UIButton* customButton = [self buttonForThisSampleWithType:UIButtonTypeCustom];
UIImage* image = [UIImage imageNamed:@"frame.png"];
UIImage* stretchableImage = [image stretchableImageWithLeftCapWidth:20 topCapHeight:20];
[customButton setBackgroundImage:stretchableImage forState:UIControlStateNormal];
customButton.frame = CGRectMake( 0, 0, 200, 60 );
//self.tableView.backgroundColor = [UIColor lightGrayColor];
buttons_ = [[NSArray alloc] initWithObjects:
customButton,
[self buttonForThisSampleWithType:UIButtonTypeRoundedRect],
[self buttonForThisSampleWithType:UIButtonTypeDetailDisclosure],
[self buttonForThisSampleWithType:UIButtonTypeInfoLight],
[self buttonForThisSampleWithType:UIButtonTypeInfoDark],
[self buttonForThisSampleWithType:UIButtonTypeContactAdd],
nil ];
}
- (void)viewDidUnload {
[dataSource_ release];
[super viewDidUnload];
}
- (NSInteger)tableView:(UITableView*)tableView
numberOfRowsInSection:(NSInteger)section
{
return [dataSource_count];
}
- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
static NSString*CellIdentifier=@"CellStyleDefault";
UITableViewCell* cell = [tableView
dequeueReusableCellWithIdentifier:CellIdentifier];
if ( nil == cell ) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier] autorelease];
}
cell.textLabel.text = [dataSource_ objectAtIndex:indexPath.row];
UIButton* button = [buttons_ objectAtIndex:indexPath.row];
button.frame = CGRectMake( cell.contentView.bounds.size.width - button.bounds.size.
width - 20,
( kRowHeight - button.bounds.
size.height ) / 2,
button.bounds.size.width,
button.bounds.size.height );
[cell.contentView addSubview:button];
return cell;
}
#pragma mark ----- Private Methods -----
- (UIButton*)buttonForThisSampleWithType:(UIButtonType) type {
UIButton* button = [UIButton buttonWithType:type];
[button setTitle:@"UIButton" forState:UIControlState
Normal];
[button setTitleColor:[UIColor blackColor]
forState:UIControlStateNormal];
[button sizeToFit];
return button;
}
@end
执行后的效果如图8-9所示。
图8-9 执行效果
8.3 实战演练——联合使用文本框、文本视图和按钮
在本节将通过一个具体实例的实现过程,来说明联合使用文本框、文本视图和按钮的流程。在这个实例中将创建一个故事生成器,可以让用户通过3个文本框(UITextField)输入一个名词(地点)、一个动词和一个数字。用户还可输入或修改一个模板,该模板包含将生成的故事概要。由于模板可能有多行,因此将使用一个文本视图(UITextView)来显示这些信息。当用户按下按钮(UIButton)时将触发一个操作,该操作生成故事并将其输出到另一个文本视图中。
8.3.1 创建项目
和本书前面的实例一样,本项目也是使用模板Single View Application。创建流程如下所示。
(1)从文件夹Developer/Applications或Launchpad的Developer编组中启动Xcode。
(2)启动后在左侧导航选择第一项“Create a new Xcode project”,如图8-10所示。
图8-10 新建一个 Xcode工程
(3)在弹出的新界面中选择项目类型和模板。在New Project窗口的左侧,确保选择了项目类型iOS中的Application,在右边的列表中选择Single View Application,再单击“Next”按钮,如图8-11所示。
图8-11 选择Single View Application
(4)在Product Name文本框中输入“lianhe”。对于公司标识符,可以随意设置,在此笔者设置的是com.guan。将保留文本框Class Prefix设置为空,并确保从下拉列表Device Family中选择了iPhone或iPad,笔者在此选择的是iPhone,然后确保选中了复选框Use Storyboard和Use Automatic Reference Counting,但没有选中复选框Include Unit Tests,如图8-12所示。然后,单击“Next”按钮。
图8-12 指定应用程序的名称和目标设备
(5)在Xcode提示时指定存储位置,再单击Create按钮创建项目。这将创建一个简单的应用程序结构,它包含一个应用程序委托、一个窗口、一个视图(在故事板场景中定义的)和一个视图控制器。打开项目窗口后将显示视图(已包含在MainStoryboard.storyboard中)和视图控制器类ViewController界面,如图8-13所示。
图8-13 新建的工程
本实例一共包含了6个输入区域,必须通过输出口将它们连接到代码。这里将使用3个文本框分别收集地点、动词和数字,它们分别对应于实例“变量/属性”thePlace、theVerb和theNumber。本实例还需要如下两个文本视图。
一个用于显示可编辑的故事模板:theTemplate。
另一个用于显示输出:theStory。
8.3.2 设计界面
启动Interface Builder后,确保文档大纲区域可见(选择菜单Editor>Show Document Outline)。如果觉得屏幕空间不够,可以隐藏导航区域,再打开对象库(选择菜单View>Utilityes>Show Object Library)。打开后的界面效果如图8-14所示。
图8-14 MainStoryboard.storyboard初始界面
1.添加文本框
在本项目中,首先在视图顶部添加3个文本框。要添加文本框,需要在对象库中找到文本框对象(UITextField)并将其拖放到视图中。重复操作两次该过程,再添加两个文本框;然后将这些文本框在顶端依次排列,并在它们之间留下足够的空间,让用户能够轻松地轻按任何文本框而不会碰到其他文本框。
为了帮助用户区分这3个文本框,还需在视图中添加标签,这需要单击对象库中的标签对象(UILabel)并将其拖放到视图中。在视图中,双击标签以设置其文本。按从上到下的顺序将标签的文本依次设置为“位置”、“数字”和“动作”,如图8-15所示。
图8-15 添加文本框和标签
(1)编辑文本框的属性
接下来需要调整它们的外观和行为以提供更好的用户体验。要查看文本框的属性,需要先单击一个文本框,然后按Option+Command+4(或选择菜单View> Utilities>Show Attributes Inspector)打开Attributes Inspector,如图8-16所示。
图8-16 编辑文本框的属性
这个时候,可以使用属性Placeholder(占位符)指定在用户编辑前出现在文本框背景中的文本,这一功能可用作提示或进一步阐述用户应输入的信息。另外还有可能需要激活清除按钮(Clear Button),清除按钮是一个加入到文本框中的“X”图标,用户可通过轻按它快速清除文本框的内容。要想在项目中添加清除按钮,需要从Clear Button下拉列表中选择一个可视选项,Xcode会自动把这种功能添加到应用程序中。另外,当用户轻按文本框以便进行编辑时会自动清除里面的内容,这一功能只需选中复选框Clear When Editing Begins。
为本实例中视图中的3个文本框添加上述功能后,此时执行后的效果如图8-17所示。
图8-17 执行效果
(2)定制键盘显示方式
对于输入文本框来说,可以设置的最重要的属性可能是文本输入特征(text input traits),即设置键盘将在屏幕上如何显示。对于文本框,Attributes Inspector底部有如下7个特征。
Capitalize(首字母大写):指定iOS自动将单词的第一个字母大写、句子的第一个字母大写还是将输入到文本框中的所有字符都大写。
Correction(修正):如果将其设置为on或off,输入文本框将更正或忽略常见的拼写错误。如果保留为默认设置,文本框将继承iOS设置的行为。
Keyboard(键盘):设置一个预定义键盘来提供输入。默认情况下,输入键盘让用户能够输入字母、数字和符号。如果将其设置为Number Pad(数字键盘),将只能输入数字;同样,如果将其设置为Email Address,将只能输入类似于电子邮件地址的字符串。总共有7种不同的键盘。
Appearance(外观):修改键盘外观使其更像警告视图。
Return Key(回车键):如果键盘有回车键,其名称为Return Key的设置,可用的选项包括Done、Search、Next、Go等。
Auto-Enable Return Key(自动启用回车键):除非用户在文本框中至少输入了一个字符,否则禁用回车键。
Secure(安全):将文本框内容视为密码,并隐藏每个字符。
在我们添加到视图中的3个文本框中,文本框“数字”将受益于一种输入特征设置。在已经打开Attributes Inspector的情况下,选择视图中的“数字”文本框,再从下拉列表 Keyboard中选择Number Pad,如图8-18所示。
图8-18 选择键盘类型
同理,也可以修改其他两个文本框的Capitalize和Correction设置,并将Return Key设置为Done。在此先将这些文本框的Return Key都设置为Done,并开始添加文本视图。另外,文本输入区域自动支持复制和粘贴功能,而无需开发人员对代码做任何修改。对于高级应用程序,可以覆盖UIResponderStandard EditActions定义的协议方法以实现复制、粘贴和选取功能。
2.添加文本视图
接下来添加本实例中的两个文本视图(UITextView)。其实文本视图的用法与文本框类似,我们可以用完全相同的方式来访问它们的内容,它们还支持很多与文本框一样的属性,其中包含文本输入特征。
要添加文本视图,需要先找到文本视图对象(UITextView),并将其拖曳到视图中。这样会在视图中添加一个矩形,其中包含表示输入区域的希腊语文本(Lorem ipsum...)。使用矩形上的手柄增大或缩小输入区域,使其适合视图。由于这个项目需要两个文本视图,因此在视图中添加两个文本视图,并调整其大小使其适合现有3个文本框下面的区域。
与文本框一样,文本视图本身同样不能向用户传递太多有关其用途的信息。为了指出它们的用途,需要在每个文本视图上方都添加一个标签,并将这两个标签的文本分别设置为Template和Story,此时视图效果如图8-19所示。
图8-19 在视图中添加两个文本视图和相应的标签
(1)编辑文本视图的属性。
通过文本视图中的属性,可以实现和文本框相同的外观控制。在此选择一个文本视图,再打开“Attributes Inspector”(快捷键是“Option+ Command+ 4”)以查看可用的属性,如图8-20所示。
图8-20 编辑每个文本视图的属性
在此需要修改Text属性,目的是删除默认的希腊语文本并提供我们自己的内容。对于上面那个用作模板的文本视图,在Attributes Inspector中选择属性Text的内容并将其清除,然后再输入下面的文本,它将在应用程序中用作默认模板:
大海 <place>小海 <verb> 海里<number> 太平洋 <place> 大西洋
当我们实现该界面后面的逻辑时,将把占位符(<place>、<verb>和<numb er>)替换为用户的输入,如图8-21所示。
图8-21 设置文本
然后选择文本视图Story,并再次使用Attributes Inspector以清除其所有内容。因为此文本视图会自动生成内容,所以可以将Text属性设置为空。这个文本视图也是只读的,因此取消选中复选框Editable。
在本实例中,为了让这两个文本视图看起来不同,特意将Template文本视图的背景色设置成淡红色,并将Story文本视图的背景色设置成淡绿色。要在这个项目中完成这项任务,只需选择要设置其背景色的文本视图,然后在Attributes Inspector的View部分单击属性Background,这样可以打开拾色器。
要对文本视图启用数据检测器,可以选择它并返回到Attributes Inspector(Command+1)。在Text View部分,选中复选框Detection(检测)下方的如下复选框。
复选框Phone Numbers(电话号码):可以识别表示电话号码的一系列数字。
复选框Address(地址):可以识别邮寄地址。
复选框Events(事件):可以识别包含日期和时间的文本。
复选框Links(链接):将网址或电子邮件地址转换为可单击的链接。
另外,数据检测器对用户来说非常方便,但是也可能被滥用。如果在项目中启用了数据检测器,请务必确保其有意义。例如对数字进行计算并将结果显示给用户,这很可能不希望这些数字被视为电话号码来使用并被处理。
(2)设置滚动选项。
在编辑文本视图的属性时,会看到一系列与其滚动特征相关的选项,如图8-22所示。使用这些属性可设置滚动指示器的颜色(黑色或白色)、指定是否启用垂直和水平滚动以及到达可滚动内容末尾时滚动区域是否有橡皮条“反弹”效果。
图8-22 Scroll View面板可以调整滚动行为的属性
3.添加风格独特的按钮
在本项目中只需要一个按钮,因此从对象库中将一个圆角矩形按钮(UIButton)实例拖放到视图底部,并将其标题设置为“Generate Story”,图8-23显示了包含默认按钮的最终视图和文档大纲。
图8-23 默认的按钮样式
在iOS项目中,虽然可以使用标准地按钮样式,但是为了进一步探索在Interface Builder中可以执行哪一些修改外观方面的操作,并最终通过代码进行修改。
(1)编辑按钮的属性。
要调整按钮的外观,同样可以使用Attributes Inspector (Option+Command+4)实现。通过使用AttributesInspector可以对按钮的外观做重大修改,通过如图8-24所示的下拉列表Type(类型)来选择常见的按钮类型。
图8-24 Attributes Inspector中的按钮类型
常见的按钮类型如下所示。
Rounded Rect(圆角矩形):默认的iOS按钮样式。
Detail Disclosure(显示细节):使用按钮箭头表示可显示其他信息。
Info Light(亮信息按钮):通常使用i图标显示有关应用程序或元素的额外信息。“亮”版本用于背景较暗的情形。
Infor Dark(暗信息按钮):暗版本的信息按钮,用于背景较亮的情形。
Add Contact(添加联系人):一个+按钮,常用于将联系人加入通讯录。
Custom(自定义):没有默认外观的按钮,通常与按钮图像结合使用。
除了选择按钮类型外,还可以让按钮响应用户的触摸操作,这通常被称为改变状态。例如在默认情况下,按钮在视图中不呈高亮显示,当用户触摸时将呈高亮显示,指出它被用户触摸。
在Attributes Inspector中,可以使用下拉列表State Config来修改按钮的标签、背景色甚至添加图像。
(2)设置自定义按钮图像。
要创建自定义iOS按钮,需要制作自定义图像,这包括呈高亮显示的版本以及默认不呈高亮显示的版本。这些图像的形状和大小无关紧要,但鉴于PNG格式的压缩和透明度特征,建议使用这种格式。
通过Xcode将这些图像加入项目后,便可以在Interface Builder中打开按钮的Attributes Inspector,并通过下拉列表Image或Background选择图像。使用下拉列表Image设置的图像将与按钮标题一起出现在按钮内,这让您能够使用图标美化按钮。
使用下拉列表Background设置的图像将拉伸以填满按钮的整个背景,这样可以使用自定义图像覆盖整个按钮,但是需要调整按钮的大小使其与图像匹配,否则图像将因拉伸而失真。另一种使用大小合适的自定义按钮图像的方法是通过代码。
8.3.3 创建并连接输出口和操作
到目前为止,整个项目的UI界面设计工作基本完毕。在设计好的界面中,需要通过视图控制器代码访问其中的6个“输入/输出”区域。另外还需要为按钮分别创建输出口和操作,其中输出口让我们能够在代码中访问按钮并设置其样式,而操作将使用模板和文本框的内容生成故事。总之,需要创建并连接如下7个输出口和一个操作。
地点文本框(UITextField):thePlace。
动词文本框(UITextField):theVerb。
数字文本框(UITextField):theNumber。
模板文本视图(UITextView):theTemplate。
故事文本视图(UITextView):theStory。
故事生成按钮(UIButton):theButton。
故事生成按钮触发的操作:createStory。
在此需要确保在Interface Builder编辑器中打开了文件MainStoryboard.storyboard,并使用工具栏按钮切换到助手模式。此时会看到UI设计和ViewController.h并排地显示,让您能够在它们之间建立连接。
1.添加输出口
首先按住Control键,并从文本框“位置”拖曳到文件ViewController.h中编译指令@interface下方。在Xcode询问时将连接设置为Outlet,名称设置为thePlace,并保留其他设置为默认值,默认值的类型为UITextField,Storage为Strong,如图8-25所示。
图8-25 为每个“输入/输出”界面元素创建并连接输出口
然后对文本框Verb和Number重复进行上述操作,将它们分别连接到输出口theVerb和theNumber。这次拖曳到前一次生成的编译指令@property下方。以同样的方式将两个文本视图分别连接到输出口theStory和theTemplate,但将Type设置为UITextView。最后,对Generate Story按钮做同样的处理,并将连接类型设置为Outlet,名称设置为theButton。
至此为止,便创建并连接好了输出口。
2.添加操作
在这个项目中创建了一个名为createStory的操作方法,该操作在用户单击“Generate Story”按钮时被触发。要创建该操作并生成一个方法以便后面可以实现它,按住Control键,并从按钮“Generate Story”拖放到文件ViewController.h中最后一个编译指令@property下方。在Xcode提示时,将该操作命名为createStory,如图8-26所示。
图8-26 创建用于生成故事的操作
至此,基本的接口文件就完成了。此时的接口文件ViewController.h的实现代码如下所示:
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property (strong, nonatomic) IBOutlet UITextField *thePlace;
@property (strong, nonatomic) IBOutlet UITextField *theVerb;
@property (strong, nonatomic) IBOutlet UITextField *theNumber;
@property (strong, nonatomic) IBOutlet UITextView *theTemplate;
@property (strong, nonatomic) IBOutlet UITextView *theStory;
@property (strong, nonatomic) IBOutlet UIButton *theButton;
- (IBAction)createStory:(id)sender;
当然,所有这些代码都是自动生成的,您无需手工进行编辑。
但是到目前为止,按钮的样式仍是平淡的。我们的第一个编码任务是,编写必要的代码,以实现样式独特的按钮。切换到Xcode标准编辑器,并确保能够看到项目导航器(Command+l)。
8.3.4 实现按钮模板
Xcode Interface Builder编辑器适合需要完成很多任务的场景,但是不包括创建样式独特的按钮。要想在不为每个按钮提供一幅图像的情况下创建一个吸引人的按钮,可以使用按钮模板来实现,但是这必须通过代码来实现。在本章的Projects文件夹中,有一个Images文件夹,其中包含两个Apple创建的按钮模板:whiteButton.png和blueButton.png。如图8-27所示。
图8-27 按钮素材
将文件夹Images拖放到该项目的项目代码编组中,在必要时选择复制资源并创建编组,如图8-28所示。
图8-28 将文件夹Images拖放到Xcode中的项目代码编组中
然后打开文件ViewController.m,找到方法ViewDidLoad,编写如下所示的对应代码:
- (void)viewDidLoad
{
UIImage*normalImage=[[UIImage imageNamed:@"whiteButton.png"]
stretchableImageWithLeftCapWidth:12.0
topCapHeight:0.0];
UIImage *pressedImage = [[UIImage imageNamed:@"blueButton.png"]
stretchableImageWithLeftCapWidth:12.0
topCapHeight:0.0];
[self.theButton setBackgroundImage:normalImage
forState:UIControlStateNormal];
[self.theButton setBackgroundImage:pressedImage
forState:UIControlStateHighlighted];
[super viewDidLoad];
}
在上述代码中实现了多项任务,这旨在向按钮(theButton)提供一个知道如何拉伸自己的图像对象(UIImage)。上述代码的实现流程如下所示:
(1)根据前面加入到项目资源中的图像文件创建了图像实例;
(2)将图像实例定义为可拉伸的。
为了根据指定的资源创建图像实例,使用类UIImage的方法imageNamed和一个包含图像资源文件名的字符串。例如在下面的代码中,根据图像whiteButton.png创建了一个图像实例:
[UIImage imageNamed:@”whiteButton.png"]
(3)使用实例方法stretchableImageWithLeftCapWidth:topCapHeight返回一个新的图像实例,使用属性定义了可如何拉伸它。这些属性是左端帽宽度(left cap width)和上端帽宽度(top cap width),它们指定了拉伸时应忽略图像左端或上端多宽的区域,然后到达可拉伸的1像素宽条带。在本实例中,使用stretchableImageWithLefiCapWidth:12.0 topCapHeight:0.0设置水平拉伸第13列像素,并且禁止垂直拉伸。然后将返回的UIImage实例赋值给变量normalImage和pressedImage,它们分别对应于默认按钮状态和呈高亮显示的按钮状态。
( 4 ) UIButton对象( theButton )的实例方法setBackgroundImage:forState能够将可拉伸图像normalImage和pressedImage分别指定为预定义按钮状态UIControlState Normal(默认状态)和UIControlStateHighlighted(呈高亮显示状态)的背景。
最后为了使整个实例的风格统一,将按钮的文本改为中文“构造”,然后在Xcode工具栏中,单击按钮Run编译并运行该应用程序,如图8-29所示,此时底部按钮的外观将显得十分整齐。
图8-29 按钮的最终效果
在iOS模拟器中的效果如图8-30所示。
图8-30 在iOS模拟器中的效果
虽然我们创建了一个高亮的按钮,但还没有编写它触发的操作(createStory)。但编写该操作前,还需完成一项与界面相关的工作:确保键盘按预期的那样消失。
8.3.5 隐藏键盘
当iOS应用程序启动并运行后,如果在一个文本框中单击会显示键盘。再单击另一个文本框,键盘将变成与该文本框的文本输入特征匹配,但仍显示在屏幕上。按Done键,什么也没有发生。但即使键盘消失了,应该如何处理没有Done键的数字键盘呢?假如正在尝试使用这个应用程序,就会发现键盘不会消失,并且还盖住了“Generate Story”按钮,这导致我们无法充分利用用户界面。这是怎么回事呢?因为响应者是处理输入的对象,而第一响应者是当前处理用户输入的对象。
对于文本框和文本视图来说,当它们成为第一响应者时,键盘将出现并一直显示在屏幕上,直到文本框或文本视图退出第一响应者状态。对于文本框 thePlace来说,可以使用如下代码行退出第一响应者状态,这样可以让键盘消失。
[self.thePlace resignFirstResponder];
调用resignFirstResponder让输入对象放弃其获取输入的权利,因此键盘将消失。
1.使用Done键隐藏键盘
在iOS应用程序中,触发键盘隐藏的最常用事件是文本框的Did End on Exit,它在用户按键盘中的Done键时发生。找到文件MainStory.storyboard并打开助手编辑器,按住Control键,并从文本框“位置”拖曳到文件ViewController.h中的操作createStory下方。在Xcode提示时,为事件Did End on Exit配置一个新操作(hideKeyboard),保留其他设置为默认值,如图8-31所示。
图8-31 添加一个隐藏键盘的新操作方法
接下来,必须将文本框“动作”连接到新定义的操作hideKeyboard。连接到已有操作的方式有很多,但只有几个可以让我们能够指定事件,此处将使用Connections Inspector方式。首先切换到标准编辑器,并确保能够看到文档大纲区域(选择菜单Editor>Show Document Outline)。选择文本框“动作”,再按Option+Command+6组合键(或选择菜单View>Utilities>Connections Inspector)打开Connections Inspector。从事件Did End on Exit旁边的圆圈拖曳到文档大纲区域中的View Controller图标,并在提示时选择操作hideKeyboard,如图8-32所示。
图8-32 将文本框Verb连接到操作hideKeyboard
但是用于输入数字的文本框打开的键盘并没有Done键,并且文本视图不支持Did End on Exit事件,那么此时我们如何为这些控件隐藏键盘呢?
2.通过触摸背景来隐藏键盘
有一种流行的iOS界面约定:在打开了键盘的情况下,如果用户触摸背景(任何控件外面)则键盘将自动消失。对于用于输入数字的文本框以及文本视图,也可以采用这种方法。为了确保一致性,需要给其他所有文本框添加这种功能。要想检测控件外面的事件,只需创建一个大型的不可见按钮并将其放在所有控件后面,再将其连接到前面编写的hideKeyboard方法。
在Interface Builder编辑器中,依次选择菜单View>Utilities> Object Library打开对象库,并拖曳一个新按钮(UIButton)到视图中。由于需要该按钮不可见,因此需要确保选择了它,然后再打开AttributesInspector(Option+Command+4)并将Type(类型)设置为Custom,这将让按钮变成透明的。使用手柄调整按钮的大小使其填满整个视图。在选择了按钮的情况下,选择菜单Editor>Arrange>Send to Back,将按钮放在其他所有控件的后面。
要将对象放在最后面,也可以在文档大纲区域将其拖放到视图层次结构的最顶端。对象按从上(后)到下(前)的顺序堆叠。为了将按钮连接到hideKeyboard方法,最简单的方式是使用Interface Builder文档大纲。选择刚创建的自定义按钮(它应位于视图层次结构的最顶端),再按住Control键并从该按钮拖曳到View Controller图标。提示时选择方法hideKeyboard。很好。现在可以实现hideKeyboard,以便位于文本框Place和Verb中时,用户可通过触摸“Done”按钮来隐藏键盘,还可在任何情况下通过触摸背景来隐藏键盘。
3.添加隐藏键盘的代码
要隐藏键盘,只需让显示键盘的对象放弃第一响应者状态。当用户在文本框“位置”(可通过属性thePlace访问它)中输入文本时,可使用下面的代码行来隐藏键盘:
[self.thePlace resignFirstResponder];
由于用户可能在如下4个地方进行修改:
thePlace;
theVerb;
theNumber;
theTemplate。
因此必须确定当前用户修改的对象或让所有这些对象都放弃第一响应者状态。实践表明,如果让不是第一响应者的对象放弃第一响应者状态不会有任何影响,这使得hideKeyboard实现起来很简单,只需将每个可编辑的UI元素对应的属性发送消息resignFirstResponder即可。
滚动到文件ViewController.m末尾,并找到我们创建操作时Xcode插入的方法hideKeyboard的存根。按照如下代码编辑该方法:
- (IBAction)hideKeyboard:(id)sender {
[self.thePlace resignFirstResponder];
[self.theVerb resignFirstResponder];
[self.theNumber resignFirstResponder];
[self.theTemplate resignFirstResponder];
}
如果此时单击文本框和文本视图外面或按Done键,键盘都将会消失。
8.3.6 实现应用程序逻辑
为了完成本章的演示项目“lianhe”,还需给视图控制器(ViewController.m)的方法createStory 添加处理代码。这个方法在模板中搜索占位符<place>、<verb>和<number>,将其替换为用户的输入,并将结果存储到文本视图中。我们将使用NSString的实例变量stringByReplacing OccurrencesOfString:WithString来完成这项繁重的工作,这个方法搜索指定的字符串并使用另一个指定的字符串替换它。
例如,如果变量myString包含Hello town,想将town替换为world,并将结果存储到变量myNewString中,则可使用如下代码:
myNewString=fmyString stringByReplacingOccurrencesOfString:@ "Hellotown"
withString:@ "world"];
在这个应用程序中,我们的字符串是文本框和文本视图的text属性(self .thePlace.text、self theVerb.text、self theNumber.text、self theTemplate.text和selftheStory.text).
在ViewController.m中,在Xcode生成的方法createStory的代码如下所示:
- (IBAction)createStory:(id)sender {
self.theStory.text=[self.theTemplate.text
stringByReplacingOccurrencesOfString:@"<place>"
withString:self.thePlace.text];
self.theStory.text=[self.theStory.text
stringByReplacingOccurrencesOfString:@"<verb>"
withString:self.theVerb.text];
self.theStory.text=[self.theStory.text
stringByReplacingOccurrencesOfString:@"<number>"
withString:self.theNumber.text];
}
上述代码的具体实现流程如下所示。
(1)使用文本库thePlace的内容替换模板中的占位符<place>,并将结果存储到文本视图Story中;
(2)使用合适的用户输入替换占位符<verb>以更新文本视图Story;
(3)使用合适的用户输入替换<number>重复该操作。最终的结果是在文本视图theStory中输出完成后的故事。
8.3.7 总结执行
到此为止,这个演示项目全部完成。在接下来的内容中,将首先对项目文件的代码进行总结。
(1)文件 ViewController.h的实现代码如下所示:
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property (strong, nonatomic) IBOutlet UITextField *thePlace;
@property (strong, nonatomic) IBOutlet UITextField *theVerb;
@property (strong, nonatomic) IBOutlet UITextField *theNumber;
@property (strong, nonatomic) IBOutlet UITextView *theTemplate;
@property (strong, nonatomic) IBOutlet UITextView *theStory;
@property (strong, nonatomic) IBOutlet UIButton *theButton;
- (IBAction)createStory:(id)sender;
- (IBAction)hideKeyboard:(id)sender;
@end
(2)文件 ViewController.m的实现代码如下所示:
#import "ViewController.h"
@implementation ViewController
@synthesize thePlace;
@synthesize theVerb;
@synthesize theNumber;
@synthesize theTemplate;
@synthesize theStory;
@synthesize theButton;
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Release any cached data, images, etc that aren't in use.
}
#pragma mark - View lifecycle
- (void)viewDidLoad
{
UIImage*normalImage=[[UIImage imageNamed:@"whiteButton.png"]
stretchableImageWithLeftCapWidth:12.0
topCapHeight:0.0];
UIImage *pressedImage = [[UIImage imageNamed:@"blueButton.png"]
stretchableImageWithLeftCapWidth:12.0
topCapHeight:0.0];
[self.theButton setBackgroundImage:normalImage
forState:UIControlStateNormal];
[self.theButton setBackgroundImage:pressedImage
forState:UIControlStateHighlighted];
[super viewDidLoad];
}
- (void)viewDidUnload
{
[self setThePlace:nil];
[self setTheVerb:nil];
[self setTheNumber:nil];
[self setTheTemplate:nil];
[self setTheStory:nil];
[self setTheButton:nil];
[super viewDidUnload];
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
}
-
(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrien
tation
{
//Return YES for supported orientations
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
/* 1:*/- (IBAction)createStory:(id)sender {
/* 2:*/ self.theStory.text=[self.theTemplate.text
/* 3:*/ stringByReplacingOccurrencesOfString:@"<place>"
/* 4:*/ withString:self.thePlace.text];
/* 5:*/ self.theStory.text=[self.theStory.text
/* 6:*/ stringByReplacingOccurrencesOfString:@"<verb>"
/* 7:*/ withString:self.theVerb.text];
/* 8:*/ self.theStory.text=[self.theStory.text
/* 9:*/ stringByReplacingOccurrencesOfString:@"<number>"
/* 10:*/ withString:self.theNumber.text];
/* 11:*/}
- (IBAction)hideKeyboard:(id)sender {
[self.thePlace resignFirstResponder];
[self.theVerb resignFirstResponder];
[self.theNumber resignFirstResponder];
[self.theTemplate resignFirstResponder];
}
@end
(3)文件main.m的实现代码如下所示:
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char *argv[])
{
@autoreleasepool{
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate
class]));
}
}
为查看并测试FieldButtonFun,单击Xcode工具栏中的Run按钮。最终的执行效果如图8-33所示。在文本框中输入信息,单击“构造”按钮后的效果如图8-34所示。
图8-33 初始执行效果
图8-34 单击按钮后的效果
8.4 使用UILabel控件和UIButton控件(基于Swift实现)
在本节的内容中,将通过一个具体实例的实现过程,详细讲解联合使用UILabel控件和UIButton控件的过程。和本章前面的实例有所不同,本实例是基于Swift语言实现的。
(1)打开Xcode 6,然后新建一个名为“UI001”的工程,工程的最终目录结构如图8-35所示。
图8-35 工程的目录结构
(2)文件RootViewController.swift的功能是,在TableView控件中分别显示两行文本:UILabel和UIButton。文件RootViewController.swift的具体实现代码如下所示。
import UIKit
class RootViewController : UIViewController,
UITableViewDelegate, UITableViewDataSource
{
var tableView:UITableView?
var items: NSArray?
override func viewDidLoad()
{
self.title="Swift"
self.items = ["UILabel", "UIButton"]
self.tableView = UITableView(frame:self.view.frame, style:UITableViewStyle.
Plain)
self.tableView!.delegate = self
self.tableView!.dataSource = self
self.tableView!.registerClass(UITableViewCell.self, forCellReuseIdentifier:
"Cell")
self.view?.addSubview(self.tableView)
}
// UITableViewDataSource函数
func numberOfSectionsInTableView(tableView: UITableView!) -> Int
{
return 1
}
//列表函数返回行数
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int
{
return self.items!.count
}
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath:
NSIndexPath!) -> UITableViewCell!
{
letcell=tableView.dequeueReusableCellWithIdentifier("Cell",forIndexPath:
indexPath) as UITableViewCell!
cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator
cell.textLabel.text = self.items?.objectAtIndex(indexPath.row) as String
return cell
}
// UITableViewDelegate 函数
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath:
NSIndexPath!)
{
self.tableView!.deselectRowAtIndexPath(indexPath, animated:true)
var detailViewController = DetailViewController()
detailViewController.title = self.items?.objectAtIndex(indexPath.row) as
String
self.navigationController.pushViewController(detailViewController,
animated:true)
}
//
override func didReceiveMemoryWarning()
{}
}
此时工程UI主界面的执行效果如图8-36所示。
图8-36 工程UI主界面的执行效果
(3)文件DetailViewController.swift的功能是,根据用户单击的列表选项弹出一个新的界面,并在新界面展示这个控件的功能。文件DetailViewController.swift的具体实现代码如下所示。
import UIKit
class DetailViewController : UIViewController,
UIPickerViewDataSource, UIPickerViewDelegate
{
override func viewDidLoad()
{
self.view!.backgroundColor=UIColor.whiteColor()
if self.title == "UILabel"
{
//Label
var label = UILabel(frame: self.view.bounds)
label.backgroundColor = UIColor.clearColor()
label.textAlignment = NSTextAlignment.Center
label.font = UIFont.systemFontOfSize(36)
label.text = "Hello, Swift"
self.view.addSubview(label)
}
else if self.title == "UIButton"
{
//Button
var button = UIButton.buttonWithType(UIButtonType.System) as? UIButton
button!.frame = CGRectMake(110.0, 120.0, 100.0, 50.0)
button!.backgroundColor = UIColor.grayColor()
button?.setTitleColor(UIColor.redColor(),forState: UIControlState.Normal)
button!.setTitleColor(UIColor.whiteColor(),forState: UIControlState.
Highlighted)
button?.setTitle("Touch Me", forState: UIControlState.Normal)
button?.setTitle("Touch Me", forState: UIControlState.Highlighted)
button?.addTarget(self, action: "buttonAction:", forControlEvents:
UIControlEvents.TouchUpInside)
button!.tag = 100
self.view.addSubview(button)
}
else
{}
}
override func viewWillAppear(animated: Bool) {}
override func viewDidAppear(animated: Bool) {}
override func viewWillDisappear(animated: Bool) {}
override func viewDidDisappear(animated: Bool) {}
// Button Action
func buttonAction(sender: UIButton)
{
var mathSum=MathSum()
var sum = mathSum.sum(11, number2: 22)
var alert = UIAlertController(title: "Title", message: String(format: "Result
= %i", sum), preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default,
handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
到此为止,整个实例介绍完毕。例如单击UI主界面中“UILabe”列表项后的执行效果如图8-37所示。
图8-37 单击UI主界面中“UILabe”列表项后的界面
单击主界面中“UIButton”列表项后的执行效果如图8-38所示。
图8-38 单击主界面中“UIButton”列表项后的执行效果
8.5 实战演练——开发一个方块游戏(基于Swift实现)
在本节的内容中,将通过一个具体实例的实现过程,详细讲解使用UIButton控件激活方块游戏的过程。本实例是基于Swift语言实现的,核心功能是监听用户的移动方块的操作和构建游戏视图。
(1)打开Xcode 6,然后新建一个名为“swift-game”的工程,工程的最终目录结构如图8-39所示。
图8-39 工程的目录结构
(2)在Xcode 6的Main.storyboard面板中设置UI界面,在里面插入一个“开始游戏”按钮。如图8-40所示。
图8-40 设计UI界面
(3)文件 ViewController.swift 的功能是监听用户单击按钮事件,单击按钮后将进入游戏视图界面。文件 ViewController.swift 的具体实现代码如下所示。
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func startGameButtonTapped(sender : UIButton) {
let game = NumberTileGameViewController(dimension: 4, threshold: 2048)
self.presentViewController(game, animated: true, completion: nil)
}
}
(4)文件 GameModel.swift 的功能是构建游戏模型,设置移动方块的方向和每一种步骤的具体走法操作,系统会检测每一个走法的可能性,并实时检测下达的走法指令是否正确。文件 GameModel.swift的具体实现代码如下所示。
import UIKit
protocol GameModelProtocol {
func scoreChanged(score: Int)
func moveOneTile(from: (Int, Int), to: (Int, Int), value: Int)
func moveTwoTiles(from: ((Int, Int), (Int, Int)), to: (Int, Int), value: Int)
func insertTile(location: (Int, Int), value: Int)
}
// 移动支持方向
enum MoveDirection {
case Up
case Down
case Left
case Right
}
// 移动命令
struct MoveCommand {
var direction: MoveDirection
var completion: (Bool) -> ()
init(d: MoveDirection, c: (Bool) -> ()) {
direction = d
completion = c
}
}
// 表示移动顺序的命令
enum MoveOrder {
case SingleMoveOrder(source: Int, destination: Int, value: Int, wasMerge: Bool)
case DoubleMoveOrder(firstSource: Int, secondSource: Int, destination: Int, value:
Int)
}
// 代表网格中的对象
enum TileObject {
case Empty
case Tile(value: Int)
}
// 对方块的动作,通过移动命令实现
enum ActionToken {
case NoAction(source: Int, value: Int)
case Move(source: Int, value: Int)
case SingleCombine(source: Int, value: Int)
case DoubleCombine(source: Int, second: Int, value: Int)
// 不管具体类型,得到一个值
func getValue() -> Int {
switch self {
case let .NoAction(_, v): return v
case let .Move(_, v): return v
case let .SingleCombine(_, v): return v
case let .DoubleCombine(_, _, v): return v
}
}
// 得到数据源,不管具体类型
func getSource() -> Int {
switch self {
case let .NoAction(s, _): return s
case let .Move(s, _): return s
case let .SingleCombine(s, _): return s
case let .DoubleCombine(s, _, _): return s
}
}
}
class GameModel: NSObject {
let dimension: Int
let threshold: Int
var score: Int = 0 {
didSet {
self.delegate.scoreChanged(score)
}
}
// var gameboard: TileObject[][] = TileObject[][]()
var gameboard_temp: TileObject[]
let delegate: GameModelProtocol
var queue: MoveCommand[]
var timer: NSTimer
let maxCommands = 100
let queueDelay = 0.3
init(dimension d: Int, threshold t: Int, delegate: GameModelProtocol) {
self.dimension = d
self.threshold = t
self.delegate = delegate
self.queue = MoveCommand[]()
self.timer = NSTimer()
self.gameboard_temp = TileObject[](count: (d*d), repeatedValue:TileObject.Empty)
NSLog("DEBUG: gameboard_temp has a count of \(self.gameboard_temp.count)")
super.init()
}
func reset() {
self.score = 0
self.queue.removeAll(keepCapacity: true)
self.timer.invalidate()
}
func queueMove(direction: MoveDirection, completion: (Bool) -> ()) {
if queue.count > maxCommands {
// Queue is wedged. This should actually never happen in practice.
return
}
let command = MoveCommand(d: direction, c: completion)
queue.append(command)
if (!timer.valid) {
// Timer isn't running, so fire the event immediately
timerFired(timer)
}
}
//---------------------------------------------------------------------------------
---------------------------------//
func timerFired(timer: NSTimer) {
if queue.count == 0 {
return
}
// 通过队列,直到一个有效的运行命令或队列为空
var changed = false
while queue.count > 0 {
let command = queue[0]
queue.removeAtIndex(0)
changed = performMove(command.direction)
command.completion(changed)
if changed {
// 如果命令没有任何改变,我们立即运行下一个
break
}
}
if changed {
self.timer = NSTimer.scheduledTimerWithTimeInterval(queueDelay,
target: self,
selector:
Selector("timerFired:"),
userInfo: nil,
repeats: false)
}
}
//---------------------------------------------------------------------------------
---------------------------------//
func temp_getFromGameboard(#x: Int, y: Int) -> TileObject {
let idx = x*self.dimension + y
return self.gameboard_temp[idx]
}
func temp_setOnGameboard(#x: Int, y: Int, obj: TileObject) {
self.gameboard_temp[x*self.dimension + y] = obj
}
//---------------------------------------------------------------------------------
---------------------------------//
func insertTile(pos: (Int, Int), value: Int) {
let (x, y) = pos
// TODO: hack
switch temp_getFromGameboard(x: x, y: y) {
// switch gameboard[x][y] {
case .Empty:
// TODO: hack
temp_setOnGameboard(x: x, y: y, obj: TileObject.Tile(value: value))
// gameboard[x][y] = TileObject.Tile(value: value)
self.delegate.insertTile(pos, value: value)
case .Tile:
break
}
}
func insertTileAtRandomLocation(value: Int) {
let openSpots = gameboardEmptySpots()
if openSpots.count == 0 {
// No more open spots; don't even bother
return
}
// Randomly select an open spot, and put a new tile there
let idx = Int(arc4random_uniform(UInt32(openSpots.count-1)))
let (x, y) = openSpots[idx]
insertTile((x, y), value: value)
}
func gameboardEmptySpots() -> (Int, Int)[] {
var buffer = Array<(Int, Int)>()
for i in 0..dimension {
for j in 0..dimension {
// TODO: hack
switch temp_getFromGameboard(x: i, y: j) {
// switch self.gameboard[i][j] {
case .Empty:
buffer += (i, j)
case .Tile:
break
}
}
}
return buffer
}
func gameboardFull() -> Bool {
return gameboardEmptySpots().count == 0
}
//---------------------------------------------------------------------------------
---------------------------------//
func userHasLost() -> Bool {
if !gameboardFull() {
// Player can't lose before filling up the board
return false
}
func tileBelowHasSameValue(loc: (Int, Int), value: Int) -> Bool {
let (x, y) = loc
if y == dimension-1 {
return false
}
// TODO: hack
switch temp_getFromGameboard(x: x, y: y+1) {
// switch gameboard[x][y+1] {
case let .Tile(v):
return v == value
default:
return false
}
}
func tileToRightHasSameValue(loc: (Int, Int), value: Int) -> Bool {
let (x, y) = loc
if x == dimension-1 {
return false
}
// TODO: hack
switch temp_getFromGameboard(x: x+1, y: y) {
// switch gameboard[x+1][y] {
case let .Tile(v):
return v == value
default:
return false
}
}
// 实现贯穿所有的方框的的移动可能性检查
for i in 0..dimension {
for j in 0..dimension {
// TODO: hack
switch temp_getFromGameboard(x: i, y: j) {
// switch gameboard[i][j] {
case .Empty:
assert(false, "Gameboard reported itself as full, but we still found an empty
tile. This is a logic error.")
case let .Tile(v):
if tileBelowHasSameValue((i, j), v) || tileToRightHasSameValue((i, j), v) {
return false
}
}
}
}
return true
}
func userHasWon() -> (Bool, (Int, Int)?) {
for i in 0..dimension {
for j in 0..dimension {
// Look for a tile with the winning score or greater
// TODO: hack
switch temp_getFromGameboard(x: i, y: j) {
// switch gameboard[i][j] {
case let .Tile(v) where v >= threshold:
return (true, (i, j))
default:
continue
}
}
}
return (false, nil)
}
//---------------------------------------------------------------------------------
---------------------------------//
// Perform move
func performMove(direction: MoveDirection) -> Bool {
// Prepare the generator closure
let coordinateGenerator: (Int) -> (Int, Int)[] = { (iteration: Int) -> (Int, Int)[]
in
let buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0))
for i in 0..self.dimension {
switch direction {
case .Up: buffer[i] = (i, iteration)
case .Down: buffer[i] = (self.dimension - i - 1, iteration)
case .Left: buffer[i] = (iteration, i)
case .Right: buffer[i] = (iteration, self.dimension - i - 1)
}
}
return buffer
}
var atLeastOneMove = false
for i in 0..dimension {
// Get the list of coords
let coords = coordinateGenerator(i)
// Get the corresponding list of tiles
let tiles = coords.map() { (c: (Int, Int)) -> TileObject in
let (x, y) = c
// TODO: hack
return self.temp_getFromGameboard(x: x, y: y)
// return self.gameboard[x][y]
}
// Perform the operation
let orders = merge(tiles)
atLeastOneMove = orders.count > 0 ? true : atLeastOneMove
// Write back the results
for object in orders {
switch object {
case let MoveOrder.SingleMoveOrder(s, d, v, wasMerge):
// Perform a single-tile move
let (sx, sy) = coords[s]
let (dx, dy) = coords[d]
if wasMerge {
score += v
}
// TODO: hack
temp_setOnGameboard(x: sx, y: sy, obj: TileObject.Empty)
temp_setOnGameboard(x: dx, y: dy, obj: TileObject.Tile(value: v))
// gameboard[sx][sy] = TileObject.Empty
// gameboard[dx][dy] = TileObject.Tile(value: v)
delegate.moveOneTile(coords[s], to: coords[d], value: v)
case let MoveOrder.DoubleMoveOrder(s1, s2, d, v):
// Perform a simultaneous two-tile move
let (s1x, s1y) = coords[s1]
let (s2x, s2y) = coords[s2]
let (dx, dy) = coords[d]
score += v
// TODO: hack
temp_setOnGameboard(x: s1x, y: s1y, obj: TileObject.Empty)
temp_setOnGameboard(x: s2x, y: s2y, obj: TileObject.Empty)
temp_setOnGameboard(x: dx, y: dy, obj: TileObject.Tile(value: v))
// gameboard[s1x][s1y] = TileObject.Empty
// gameboard[s2x][s2y] = TileObject.Empty
// gameboard[dx][dy] = TileObject.Tile(value: v)
delegate.moveTwoTiles((coords[s1], coords[s2]), to: coords[d], value: v)
}
}
}
return atLeastOneMove
}
//---------------------------------------------------------------------------------
---------------------------------//
// Remove interstital space (e.g. |[2][-][-][4]| becomes |[2][4]|)
func condense(group: TileObject[]) -> ActionToken[] {
var tokenBuffer = ActionToken[]()
for (idx, tile) in enumerate(group) {
// Go through all the tiles in 'group'. When we see a tile 'out of place', create
// a corresponding ActionToken.
switch tile {
case let .Tile(value) where tokenBuffer.count == idx:
tokenBuffer.append(ActionToken.NoAction(source: idx, value: value))
case let .Tile(value):
tokenBuffer.append(ActionToken.Move(source: idx, value: value))
default:
break
}
}
return tokenBuffer;
}
// Collapse adjacent tiles of equal value
func collapse(group: ActionToken[]) -> ActionToken[] {
func quiescentTileStillQuiescent(inputPosition: Int, outputLength: Int,
originalPosition: Int) -> Bool {
// Return whether or not a 'NoAction' token still represents an unmoved tile
return (inputPosition == outputLength) && (originalPosition == inputPosition)
}
var tokenBuffer = ActionToken[]()
var skipNext = false
for (idx, token) in enumerate(group) {
if skipNext {
// Prior iteration handled a merge. So skip this iteration.
skipNext = false
continue
}
switch token {
case .SingleCombine:
assert(false, "Cannot have single combine token in input")
case .DoubleCombine:
assert(false, "Cannot have double combine token in input")
case let .NoAction(s, v)
where (idx < group.count-1
&& v == group[idx+1].getValue()
&& quiescentTileStillQuiescent(idx, tokenBuffer.count, s)):
// This tile hasn't moved yet, but matches the next tile. This is a single merge
// The last tile is *not* eligible for a merge
let next = group[idx+1]
let nv = v + group[idx+1].getValue()
skipNext = true
tokenBuffer.append(ActionToken.SingleCombine(source: next.getSource(), value: nv))
case let t where (idx < group.count-1 && t.getValue() == group[idx+1].getValue()):
// This tile has moved, and matches the next tile. This is a double merge
// (The tile may either have moved prevously, or the tile might have moved as
// a result of a previous merge)
// The last tile is *not* eligible for a merge
let next = group[idx+1]
let nv = t.getValue() + group[idx+1].getValue()
skipNext = true
tokenBuffer.append(ActionToken.DoubleCombine(source: t.getSource(), second:
next.getSource(), value: nv))
case let .NoAction(s, v) where !quiescentTileStillQuiescent(idx,
tokenBuffer.count, s):
// A tile that didn't move before has moved (first cond.), or there was a previous
// merge (second cond.)
tokenBuffer.append(ActionToken.Move(source: s, value: v))
case let .NoAction(s, v):
// A tile that didn't move before still hasn't moved
tokenBuffer.append(ActionToken.NoAction(source: s, value: v))
case let .Move(s, v):
// Propagate a move
tokenBuffer.append(ActionToken.Move(source: s, value: v))
default:
// Don't do anything
break
}
}
return tokenBuffer
}
// Convert all action tokens into move orders
func convert(group: ActionToken[]) -> MoveOrder[] {
var moveBuffer = MoveOrder[]()
for (idx, t) in enumerate(group) {
switch t {
case let .Move(s, v):
moveBuffer.append(MoveOrder.SingleMoveOrder(source: s, destination: idx,
value: v, wasMerge: false))
case let .SingleCombine(s, v):
moveBuffer.append(MoveOrder.SingleMoveOrder(source: s, destination: idx,
value: v, wasMerge: true))
case let .DoubleCombine(s1, s2, v):
moveBuffer.append(MoveOrder.DoubleMoveOrder(firstSource: s1, secondSource: s2,
destination: idx, value: v))
default:
// Don't do anything
break
}
}
return moveBuffer
}
// Given an array of TileObjects, perform a collapse and create an array of move orders
that can be fed to the view
func merge(group: TileObject[]) -> MoveOrder[] {
return convert(collapse(condense(group)))
}
}
系统首界面的执行后将显示一个“开始游戏”按钮,如图8-41所示。
图8-41 “开始游戏”按钮
(5)文件NumberTileGame.swift的功能是创建游戏面板视图和得分视图,设置了游戏面板中方块的数量和不同组件之间的距离。文件NumberTileGame.swift的具体实现代码如下所示:
import UIKit
class NumberTileGameViewController : UIViewController, GameModelProtocol {
// 设置游戏面板纵向和横向方块数目
var dimension: Int
var threshold: Int
var board: GameboardView?
var model: GameModel?
var scoreView: ScoreViewProtocol?
//游戏板的宽度
let boardWidth: CGFloat = 230.0
// How much padding to place between the tiles
let thinPadding: CGFloat = 3.0
let thickPadding: CGFloat = 6.0
// 不同组件视图之间的空间大小值 (gameboard, score view, etc)
let viewPadding: CGFloat = 10.0
// 垂直对齐数量
let verticalViewOffset: CGFloat = 0.0
init(dimension d: NSInteger, threshold t: NSInteger) {
self.dimension = d > 2 ? d : 2
self.threshold = t > 8 ? t : 8
super.init(nibName: nil, bundle: nil)
model = GameModel(dimension: dimension, threshold: threshold, delegate: self)
self.view.backgroundColor = UIColor.whiteColor()
setupSwipeControls()
}
func setupSwipeControls() {
let upSwipe = UISwipeGestureRecognizer(target: self, action:
Selector("upCommand"))
upSwipe.numberOfTouchesRequired = 1
upSwipe.direction = UISwipeGestureRecognizerDirection.Up
self.view.addGestureRecognizer(upSwipe)
let downSwipe = UISwipeGestureRecognizer(target: self, action:
Selector("downCommand"))
downSwipe.numberOfTouchesRequired = 1
downSwipe.direction = UISwipeGestureRecognizerDirection.Down
self.view.addGestureRecognizer(downSwipe)
let leftSwipe = UISwipeGestureRecognizer(target: self, action:
Selector("leftCommand"))
leftSwipe.numberOfTouchesRequired = 1
leftSwipe.direction = UISwipeGestureRecognizerDirection.Left
self.view.addGestureRecognizer(leftSwipe)
let rightSwipe = UISwipeGestureRecognizer(target: self, action:
Selector("rightCommand"))
rightSwipe.numberOfTouchesRequired = 1
rightSwipe.direction = UISwipeGestureRecognizerDirection.Right
self.view.addGestureRecognizer(rightSwipe)
}
// View Controller
override func viewDidLoad() {
super.viewDidLoad()
setupGame()
}
func reset() {
assert(board != nil && model != nil)
let b = board!
let m = model!
b.reset()
m.reset()
m.insertTileAtRandomLocation(2)
m.insertTileAtRandomLocation(2)
}
func setupGame() {
let vcHeight = self.view.bounds.size.height
let vcWidth = self.view.bounds.size.width
//此嵌套函数提供了一个组件X轴视图位置
func xPositionToCenterView(v: UIView) -> CGFloat {
let viewWidth = v.bounds.size.width
let tentativeX = 0.5*(vcWidth - viewWidth)
return tentativeX >= 0 ? tentativeX : 0
}
//此嵌套函数提供了一个组件Y轴视图位置
func yPositionForViewAtPosition(order: Int, views: UIView[]) -> CGFloat {
assert(views.count > 0)
assert(order >= 0 && order < views.count)
let viewHeight = views[order].bounds.size.height
let totalHeight = CGFloat(views.count - 1)*viewPadding +
views.map({ $0.bounds.size.height }).reduce(verticalViewOffset, { $0 + $1 })
let viewsTop = 0.5*(vcHeight - totalHeight) >= 0 ? 0.5*(vcHeight - totalHeight) : 0
// Not sure how to slice an array yet
var acc: CGFloat = 0
for i in 0..order {
acc += viewPadding + views[i].bounds.size.height
}
return viewsTop + acc
}
// 创建得分视图
let scoreView = ScoreView(backgroundColor: UIColor.blackColor(),
textColor: UIColor.whiteColor(),
font: UIFont(name: "HelveticaNeue-Bold", size: 16.0),
radius: 6)
scoreView.score = 0
// 创建游戏板视图
let padding: CGFloat = dimension > 5 ? thinPadding : thickPadding
let v1 = boardWidth - padding*(CGFloat(dimension + 1))
let width: CGFloat = CGFloat(floorf(CFloat(v1)))/CGFloat(dimension)
let gameboard = GameboardView(dimension: dimension,
tileWidth: width,
tilePadding: padding,
cornerRadius: 6,
backgroundColor: UIColor.blackColor(),
foregroundColor: UIColor.darkGrayColor())
// 设置框架
let views = [scoreView, gameboard]
var f = scoreView.frame
f.origin.x = xPositionToCenterView(scoreView)
f.origin.y = yPositionForViewAtPosition(0, views)
scoreView.frame = f
f = gameboard.frame
f.origin.x = xPositionToCenterView(gameboard)
f.origin.y = yPositionForViewAtPosition(1, views)
gameboard.frame = f
// 增加游戏模式
self.view.addSubview(gameboard)
self.board = gameboard
self.view.addSubview(scoreView)
self.scoreView = scoreView
assert(model != nil)
let m = model!
m.insertTileAtRandomLocation(2)
m.insertTileAtRandomLocation(2)
}
// Misc
func followUp() {
assert(model != nil)
let m = model!
let (userWon, winningCoords) = m.userHasWon()
if userWon {
// TODO: alert delegate we won
let alertView = UIAlertView()
alertView.title = "Victory"
alertView.message = "You won!"
alertView.addButtonWithTitle("Cancel")
alertView.show()
// TODO: At this point we should stall the game until the user taps 'New Game'
// (which hasn't been implemented yet)
return
}
// Now, insert more tiles
let randomVal = Int(arc4random_uniform(10))
m.insertTileAtRandomLocation(randomVal == 1 ? 4 : 2)
// At this point, the user may lose
if m.userHasLost() {
// TODO: alert delegate we lost
NSLog("You lost...")
let alertView = UIAlertView()
alertView.title = "Defeat"
alertView.message = "You lost..."
alertView.addButtonWithTitle("Cancel")
alertView.show()
}
}
// Commands
func upCommand() {
assert(model != nil)
let m = model!
m.queueMove(MoveDirection.Up,
completion: { (changed: Bool) -> () in
if changed {
self.followUp()
}
})
}
func downCommand() {
assert(model != nil)
let m = model!
m.queueMove(MoveDirection.Down,
completion: { (changed: Bool) -> () in
if changed {
self.followUp()
}
})
}
func leftCommand() {
assert(model != nil)
let m = model!
m.queueMove(MoveDirection.Left,
completion: { (changed: Bool) -> () in
if changed {
self.followUp()
}
})
}
func rightCommand() {
assert(model != nil)
let m = model!
m.queueMove(MoveDirection.Right,
completion: { (changed: Bool) -> () in
if changed {
self.followUp()
}
})
}
// Protocol
func scoreChanged(score: Int) {
if (!scoreView) {
return
}
let s = scoreView!
s.scoreChanged(newScore: score)
}
func moveOneTile(from: (Int, Int), to: (Int, Int), value: Int) {
assert(board != nil)
let b = board!
b.moveOneTile(from, to: to, value: value)
}
func moveTwoTiles(from: ((Int, Int), (Int, Int)), to: (Int, Int), value: Int) {
assert(board != nil)
let b = board!
b.moveTwoTiles(from, to: to, value: value)
}
func insertTile(location: (Int, Int), value: Int) {
assert(board != nil)
let b = board!
b.insertTile(location, value: value)
}
}
(6)文件AppearanceProvider.swift的功能是构建游戏视图中不同区域的颜色样式和记分牌的字体,具体实现代码如下所示。
import UIKit
protocol AppearanceProviderProtocol {
func tileColor(value: Int) -> UIColor
func numberColor(value: Int) -> UIColor
func fontForNumbers() -> UIFont
}
class AppearanceProvider: AppearanceProviderProtocol {
// 给方块提供给定的颜色值
func tileColor(value: Int) -> UIColor {
switch value {
case 2:
return UIColor(red: 238.0/255.0, green: 228.0/255.0, blue: 218.0/255.0, alpha:
1.0)
case 4:
return UIColor(red: 237.0/255.0, green: 224.0/255.0, blue: 200.0/255.0, alpha:
1.0)
case 8:
return UIColor(red: 242.0/255.0, green: 177.0/255.0, blue: 121.0/255.0, alpha:
1.0)
case 16:
return UIColor(red: 245.0/255.0, green: 149.0/255.0, blue: 99.0/255.0, alpha:
1.0)
case 32:
return UIColor(red: 246.0/255.0, green: 124.0/255.0, blue: 95.0/255.0, alpha:
1.0)
case 64:
return UIColor(red: 246.0/255.0, green: 94.0/255.0, blue: 59.0/255.0, alpha: 1.0)
case 128:
fallthrough
case 256:
fallthrough
case 512:
fallthrough
case 1024:
fallthrough
case 2048:
return UIColor(red: 237.0/255.0, green: 207.0/255.0, blue: 114.0/255.0, alpha:
1.0)
default:
return UIColor.whiteColor()
}
}
// Provide a numeral color for a given value
func numberColor(value: Int) -> UIColor {
switch value {
case 2:
fallthrough
case 4:
return UIColor(red: 119.0/255.0, green:
110.0/255.0, blue: 101.0/255.0, alpha: 1.0)
default:
return UIColor.whiteColor()
}
}
// 设置数字记分牌的字体
func fontForNumbers() -> UIFont {
return UIFont(name: "HelveticaNeue-Bold",
size: 20)
}
}
到此为止,整个实例介绍完毕,执行后的效果如图8-42所示。
图8-42 执行效果