管理文本字段和文本视图 <- iOS文本编程指南

文本字段和文本视图有两个主要的功能:显示文本,以及允许输入和编辑文本。 很多编程任务和这些简单的目标相关联,包括配置文本对象、访问当前文本、检查用户的输入、以及显示覆盖视图(例如在文本字段中的书签按钮)。

UITextField或者UITextView对象的委托负责处理其中大部分的任务。这些委托必须采用UITextFieldDelegate或者UITextViewDelegate协议,并使其中一个或多个方法。所有协议方法都是可选实现的。为了能够调用这些方法,你必须用编码的方式或者Interface Builder设置文本字段和文本视图的delegate属性。

给委托发送的消息的顺序

在大多数情况下,UITextField或UITextView类的实例会在给定的文本对象的第一响应者状态改变的时候发送一系列名字类似的信息给它们的委托。当用户点击一个文本对象,它会自动变成第一响应者;所以系统回味该文本对象显示键盘并开始一个编辑会话。当用户点击另一个文本对象或点击按钮来结束编辑的时候,当前的文本对象注销第一响应者状态。如果没有其他文本对象被选择,系统会隐藏键盘;另一方面,如果用户选择了另一个文本对象,它会变成第一响应者,并且键盘会为该对象而存在。

对于一般的行为有两个例外。在iPad上,如果视图控制器使用“form sheet”样式模态呈现它的视图,显示的键盘不会消失,直到用户点击关闭按钮或者模态视图控制器以编程的方式关闭。这样设计的目的是避免过多的动画。另一个例外是自定义输入视图。输入视图可以替代分配给文本视图或自定义视图的inputView属性的系统键盘。当有输入视图的时候,即使文本对象是第一响应者,UIKit也可以切换键盘。

文本视图和文本字段给它们委托发送信息的顺序如下:

  1. 在文本对象刚刚成为第一响应者之前——textFieldShouldBeginEditing: (文本字段) 和textViewShouldBeginEditing: (文本视图)。委托可以通过返回YES(默认)或NO来校验是否将文本对象变成第一响应者。
  2. 在文本对象刚刚成为第一响应者之后——textFieldDidBeginEditing: (文本字段) 和 textViewDidBeginEditing: (文本视图)。委托可以通过诸如更新状态信息或在编辑会话期间展示覆盖视图对该消息作出响应。
  3. 在编辑会话期间——当用户输入以及编辑文本的时候,文本对象调用合适的委托方法(如果实现)。例如,文本视图可以在文本改变时候,它的委托可以接受textViewDidChange:消息。文本字段可以在用于点击它的清除按钮的时候接受textFieldShouldClear: 消息;委托返回布尔值来说明是否清除文本。
    实现这些方法是为了输入文本的有效性。例如,如果文本应该遵循给定的格式,那么在字符串未遵守格式的时候委托应该返回NO。
  4. 在文本对象刚刚注销第一响应者之前——textFieldShouldEndEditing: (文本字段) 和 textViewShouldEndEditing: (文本视图)。实现这些方法的主要原因是,验证输入的文本。例如,如果文本应该符合给定的模式,如果输入的字符串不符合给定的模式,该委托会返回NO。默认情况下返回的是YES。
    相关的文本字段的方法是textFieldShouldReturn:。当用户点击回车键的时候,文本字段会发送textFieldShouldReturn:方法给委托,来询问它是否应该注销第一响应者。
  5. 在文本对象刚刚注销第一响应着之后————textFieldDidEndEditing: (文本字段) 和 textViewDidEndEditing: (文本视图)。委托可以实现这些方法来获取用户刚刚输入或编辑的文本。

文本视图和文本字段的第一响应者状态的改变,可以通过观察通知来通知委托以外的对象。(但是,它们不能批准或拒绝新状态的过渡。)通知有名字,例如UITextFieldTextDidBeginEditingNotification, UITextViewTextDidEndEditingNotification, 和 UITextViewTextDidChangeNotification。和textFieldDidEndEditing: 和 textViewDidEndEditing:一样,观察并处理UITextFieldTextDidEndEditingNotification 和 UITextViewTextDidEndEditingNotification通知是为了访问相关的文本字段和文本视图。更多关于通过这些类发送的通知的信息,参见UITextField Class Reference 和 UITextView Class Reference 。

配置文本字段和文本视图

就像所有由UIKit框架提供的视图对象一样,你通常需要在文本字段和文本视图显示之前配置它们。你可以用编程方式配置它们,也可以使用在Interface Builder中的属性检查器来配置它们。这两种方式,你都是在设置文本对象的属性。

有一些属性对于文本视图和文本对象是常见的,但是也有一些属性对于每个类型的对象都是特定的,包括以下几个:

  • 文本特性——文本颜色、对齐方式、字体族、字体、以及字体尺寸。
  • 键盘——键盘类型、返回键名字、安全文本输入、以及自动启用的返回键,所有这些都通过UITextInputTraits协议声明。(注意,与文本视图相关联的自动启用会车键,在点击时,它的行为和回车键一样。)更多信息,参见“为文本对象配置键盘”。
  • 文本字段特有——边框、背景图片、禁用图片、清除按钮、以及展位文本。作为一个UIControl对象,文本字段也可以有高亮、选择、启用、和其他属性。
  • 文本视图特有——可编辑状态、数据探测器(针对电话号码和URL连接)。因为文本视图继承自UIScrollView,你可以通过设置合适的属性个来管理滚动视图的行为。

跟踪多个文本字段或文本视图

所有的UITextFieldDelegate或UITextViewDelegate协议的方法,都有一个属性来识别文本字段或文本视图第一响应者状态的改变、值的改变、或者其他委托信息原因的改变。如果只有一个文本对象在当前的视图中显示,该文本对象通过上面的属性进行验证。但是,如果当前视图上面有多个文本字段或者文本视图,委托必须找到某种方式来指示该文本对象就是委托信息的对象。

你可以使用以下两种方法来确定:outlet或tag。对于outlet方法,声明一个outlet实例变量 (使用IBOutlet关键字),并建立一个outlet连接。在你的委托方法中,使用指针对照,测试传入的文本对象是否与outlet指示的对象相同。例如,假设你声明并连接了一个名为SSN的outlet。你的代码应该和代码清单3-1类似。

代码清单 3-1 使用outlet识别传入的文本对象

- (BOOL)textFieldShouldEndEditing:(UITextField *)textField {

    if (textField == SSN) {

            // .....

            return NO;

        }

    return YES;

}

当你需要将字符串值写入到文本对象中,而不仅仅是获取它们,那么为在视图中的这些文本对象定义outlet连接特别有用,甚至是必要的。

而对于tag方式,声明一组枚举常量,每个tag对应一个常量。

enum {

    NameFieldTag = 0,

    EmailFieldTag,

    DOBFieldTag,

    SSNFieldTag

};

然后通过编程方式或者Interface Builder方式把整型值分配给文本对象的tag属性。(tag属性在UIView中声明了。)在委托方法中,你可以使用seitch语句来评估传入的文本对象的tag值,从而继续(如代码清单3-2所示)。

文本清单 3-2 使用tag识别传入的文本对象

- (void)textFieldDidEndEditing:(UITextField *)textField {

 

    switch (textField.tag) {

        case NameFieldTag:

            // do something with this text field

            break;

        case EmailFieldTag:

             // do something with this text field

            break;

        // remainder of switch statement....

    }

}

获取输入的文本并设置文本

在用户在文本字段或文本视图中输入或者编辑文本且编辑会话结束之后,委托将获得文本,并把它存储在应用的数据模型中。对于访问输入文本的最好的委托方法是textFieldDidEndEditing: (文本字段) 和 textViewDidEndEditing: (文本视图).

代码清单 3-3 演示了如何获取用户在文本字段中(使用tag值来在视图中区别多个文本字段)输入的文本。UITextField或者UITextView的text属性通过文本对象持后当前显示的字符串。委托从该属性中获取字符串,并把它存储在一个字典对象中。如果该文本字段换没有字符串值(就是该字段持有一个空字符串),该委托就简单的返回。

代码清单 3-3 在文本字段中获取输入的文本

- (void)textFieldDidEndEditing:(UITextField *)textField {

    if ([textField.text isEqualToString:@""])

        return;

 

    switch (textField.tag) {

        case NameFieldTag:

            [thePerson setObject:textField.text forKey:MyAppPersonNameKey];

            break;

        case EmailFieldTag:

            [thePerson setObject:textField.text forKey:MyAppPersonEmailKey];

            break;

        case SSNFieldTag:

            [thePerson setObject:textField.text forKey:MyAppPersonSSNKey];

            break;

        default:

            break;

    }

}

代码清单 3-4 显示了textViewDidEndEditing: 方法的实现,它从文本视图中获取显示的字符串,并把它存储在字典中。这个方法不要求文本视图注册为第一响应者。(resignFirstResponder方法在动作方法被调用之前被调用,该动作方法在用户在视图的用户界面上点击完成按钮的时候被调用。)

代码清单 3-4 在文本视图中获取输入的文本

- (void)textViewDidEndEditing:(UITextView *)textView {

    NSString *theText = textView.text;

    if (![theText isEqualToString:@""]) {

        [thePerson setObject:theText forKey:MyAppPersonNotesKey];

    }

    doneButton.enabled = NO;

}

如果你需要把字符串值写入文本对象(通常在从app的数据模型检索它们之后),只需要简单的把字符串分配给文本对象的text属性即可。例如:

NSString *storedValue = [thePerson objectForKey:MyAppPersonEmailKey];
emailField.text = storedValue;

为此,给想要写入字符串的文本字段换或者文本视图(在这个例子中时emailField)定义outlet是有用的。

在文本字段使用格式化器

格式化器对象自动解析在特定格式中的字符串,并且将该字符串转换成代表数字、日期、或其他值的对象;它们还可以反操作,把NSData、NSNumber、以及类似的对象转换为代表这些对象值的格式化字符串。Foundation框架提供抽象基础类NSFormatter和两个具体子类NSDateFormatter和NSNumberFormatter。始终这些类,用户可以在文本字段输入类似下面的值:

11/15/2010

-1,348.09

然后app可以使用formatter对象将字符串分别转换为NSData对象和NSNumber对象。

下面的代码清单,使用了日期格式化器对象来说明格式化器的使用。(当然,你可以使用UIDatePicker对象来代替文本字段输入日期,而附加日期格式化器的文本字段是另一个选项。)代码清单3-5中的代码,创建了一个NSDateFormatter对象,并且给它分配了一个实例变量。它配置这个日期格式化器来使用日期的“短样式”,但是在某种程度上可以响应日历、区域、以及时区的改变。它还把今天的日期分配给给定的格式作为占位字符串,以便用户在输入日期的时候有一个可以遵照的模型。

代码清单 3-5 配置日期格式化器

- (void)viewDidLoad {
[super viewDidLoad];
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setGeneratesCalendarDates:YES];
[dateFormatter setLocale:[NSLocale currentLocale]];
[dateFormatter setCalendar:[NSCalendar autoupdatingCurrentCalendar]];
[dateFormatter setTimeZone:[NSTimeZone defaultTimeZone]];
[dateFormatter setDateStyle:NSDateFormatterShortStyle]; // example: 4/13/10
DOB.placeholder = [NSString stringWithFormat:@"Example: %@", [dateFormatter stringFromDate:[NSDate date]]];
// code continues....
}

在你配置了日期格式化器之后,该委托可以调用格式化器的dateFromString:方法,来把输入的日期字符串转换为NSData对象。代码清单3-6显示了此过程。

代码清单3-6 使用NSDateFormatter对象,把日期字符串转化为数据对象

- (void)textFieldDidEndEditing:(UITextField *)textField {
[textField resignFirstResponder];
if ([textField.text isEqualToString:@""])
return;
switch (textField.tag) {
case DOBField:
NSDate *theDate = [dateFormatter dateFromString:textField.text];;
if (theDate)
[inputData setObject:theDate forKey:MyAppPersonDOBKey];
break;
// more switch case code here...
default:
break;
}
}

使用了格式化器并不能保证输入的字符串必定包含了有效值(例如,用户可以输入13作为格里高利日历的月份值)。为了确保用户输入正确的值,委托必须验证字符串,这部分在验证输入文本中已经解释了。因为验证经常要求已知的格式和有效值的范围,如果你想代码清单3-5那样配置日期格式化器,以便它对于不同的日历和区域敏感,则无法肯定的知道该格式。想要指定一个已知的日期格式,通过调用setDateFormat:来配置日期格式化器,给它传递一个通过Unicode标准定义的格式模型。

你还可以将上面演示的过程反过来:把数据对象转换成通过调用NSDateFormatter的方法stringFromDate:生成的字符串,然后把该字符串分配给文本字段、文本视图或者标签的text属性。

更多关于NSDateFormatter和NSNumberFormatter的信息,参见Data Formatting Guide。

验证输入的文本

App有时候不能接收在文本字段和文本视图中输入的没有验证的字符串。或许该字符串必须是某种格式,也或者该值必须在特定范围之内。验证输入字符串最好的委托方法,对于文本字段来说是textFieldShouldEndEditing:,对于文本视图来说是textViewShouldEndEditing:。这些方法在文本字段或文本视图注册为第一响应者之前调用。返回NO来防止这种情况的发生,因此文本对象依然是编辑的重点。如果输入的字符串是无效的,你还应该显示警告来提醒用户这个错误。

代码清单3-7使用正则表达式来验证在“社会保障号码”字段中输入的字符串是否符合特定的格式。

代码清单 3-7 实用正则表达式验证文本字段的字符串格式

- (BOOL)textFieldShouldEndEditing:(UITextField *)textField {
if (textField == SSN) { // SSN is an outlet
NSString *regEx = @"[0-9]{3}-[0-9]{2}-[0-9]{4}";
NSRange r = [textField.text rangeOfString:regEx options:NSRegularExpressionSearch];
if (r.location == NSNotFound) {
UIAlertView *av = [[[UIAlertView alloc] initWithTitle:@"Entry Error"
message:@"Enter social security number in 'NNN-NN-NNNN' format"
delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil] autorelease];
[av show];
return NO;
}
}
return YES;
}

在代码清单3-8中,实现了textViewShouldEndEditing:方法,强制限制了在文本视图中输入的文本的字符数。

代码清单 3-8 验证文本视图允许的字符串长度

- (BOOL)textViewShouldEndEditing:(UITextView *)textView {
if (textView.text.length > 50) {
UIAlertView *av = [[[UIAlertView alloc] initWithTitle:@"Entry Error"
message:@"You must enter less than 50 characters." delegate:self cancelButtonTitle:@"OK"
otherButtonTitles:@"Clear", nil] autorelease];
[av show];
return NO;
}
return YES;
}

委托还可以通过实现textField:shouldChangeCharactersInRange:replacementString:方法,来验证在文本字段中输入的每个字符。代码清单3-9中的代码,验证了每个输入的字符(字符串)代表一个数字。(你可以通过给文本字段指定UIKeyboardTypeNumberPad键盘来实现相同的目标。)

代码清单 3-9 验证每个输入的字符

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string {
if ([string isEqualToString:@""]) return YES;
if (textField.tag == SalaryFieldTag) {
unichar c = [string characterAtIndex:0];
if ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:c]) {
return YES;
} else {
return NO;
}
}
return YES;
}

你可以实现textField:shouldChangeCharactersInRange:replacementString:方法,在用户输入文本的时候提供单词的可能完整性和更正功能。

在文本字段中使用覆盖视图

覆盖视图是一个插入到文本字段左侧或右侧的小视图。作为控件,当用户点击它们的时候,可以操作文本字段当前的内容。搜索和书签是覆盖视图的两种常见任务,但是其他也是可能的。覆盖视图可以使用文本字段中(部分)URL加载web浏览器:

想要实现覆盖视图,创建一个和文本字段高度尺寸匹配的视图,并给该视图一个合适尺寸的图片。如果该视图是一个按钮或其他空间,请指定目标对象、动作选择器、和触发控件事件。通常来说,你希望在文本字段聚焦于编辑的时候出现覆盖视图,所以把它分配给textFieldDidBeginEditing: 委托方法中的leftView 或者rightView属性。在编辑会话期间(例如在用户输入文本之前,或者用户刚刚开始输入文本之后),在覆盖视图出现的时候,你可以通过将UITextFieldViewMode常量分配给leftViewMode或rightViewMode属性进行控制。代码清单3-10演示了如何实现覆盖视图。

代码清单 3-10在文本字段中显示覆盖视图。

- (void)textFieldDidBeginEditing:(UITextField *)textField {
if (textField.tag == NameField && self.overlayButton) {
textField.leftView = self.overlayButton;
textField.leftViewMode = UITextFieldViewModeAlways;
}
}
@dynamic overlayButton;
- (UIButton *)overlayButton {
if (!overlayButton) {
overlayButton = [[UIButton buttonWithType:UIButtonTypeCustom] retain];
UIImage *overlayImage = [UIImage imageNamed:@"bookmark.png"];
if (overlayImage) {
[overlayButton setImage:overlayImage forState:UIControlStateNormal];
[overlayButton addTarget:self action:@selector(bookmarkTapped:)
forControlEvents:UIControlEventTouchUpInside];
}
}
return overlayButton;
}

如果你想使用覆盖视图的空间,请确保实现了该动作方法。

要想移除掉覆盖视图,只要在textFieldDidEndEditing:委托方法中,将leftView或者rightView属性简单设置为nil即可。参照代码清单3-11。

代码清单 3-11移除覆盖视图

- (void)textFieldDidEndEditing:(UITextField *)textField {
if (textField.tag == NameFieldTag) {
textField.leftView = nil;
}
// remainder of implementation....
}

跟踪文本视图中的所选部分

UITextViewDelegate中的方法textViewDidChangeSelection:让你可以跟踪用户在文本视图中所选择部分的修改。你可以实现方法来获取选择部分的子字符串,并对它进行操作。代码清单3-12 是一个怪诞的例子,它让所选子字符串的字符都变成大写。

代码清单 3-12 获取选中的子字符串并更改它

- (void)textViewDidChangeSelection:(UITextView *)textView {
NSRange r = textView.selectedRange;
if (r.length == 0) {
return;
}
NSString *selText = [textView.text substringWithRange:r];
NSString *upString = [selText uppercaseString];
NSString *newString = [textView.text stringByReplacingCharactersInRange:r withString:upString];
textView.text = newString;
}

推荐阅读更多精彩内容