iOS开发基础24-UIPickerView、UITextField、KVC、UIDatePicker、控制器及导航控制器
UIKit 进阶:UIPickerView、UITextField 自定义键盘、控制器加载与导航控制器
本文涵盖 UIPickerView 选择器、UITextField 代理与自定义键盘、KVC 字典转模型底层、Storyboard/XIB 加载控制器、控制器 view 生命周期、UINavigationController 导航控制器等核心知识点。
一、UIPickerView 的使用
UIPickerView 是选择器控件,用于从多列多行中选择数据,常见于地址选择、日期选择(旧样式)等场景。
1. DataSource(必须实现)
@interface ViewController () <UIPickerViewDataSource, UIPickerViewDelegate>
@property (nonatomic, strong) UIPickerView *pickerView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.pickerView = [[UIPickerView alloc] initWithFrame:CGRectMake(0, 100, 320, 200)];
self.pickerView.dataSource = self;
self.pickerView.delegate = self;
[self.view addSubview:self.pickerView];
}
// 返回列数(必须实现)
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 2; // 两列,如省、市
}
// 返回每列的行数(必须实现)
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return component == 0 ? 5 : 10;
}
@end
2. Delegate(常用方法)
#pragma mark - UIPickerViewDelegate
// 每列宽度
- (CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component {
return component == 0 ? 120 : 200;
}
// 每行高度
- (CGFloat)pickerView:(UIPickerView *)pickerView rowHeightForComponent:(NSInteger)component {
return 44;
}
// 普通文本标题
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
return [NSString stringWithFormat:@"第 %ld 行", (long)row];
}
// 富文本标题(可设置颜色、字体)
- (NSAttributedString *)pickerView:(UIPickerView *)pickerView attributedTitleForRow:(NSInteger)row forComponent:(NSInteger)component {
NSString *title = [NSString stringWithFormat:@"第 %ld 行", (long)row];
return [[NSAttributedString alloc] initWithString:title attributes:@{
NSForegroundColorAttributeName: [UIColor systemBlueColor],
NSFontAttributeName: [UIFont boldSystemFontOfSize:16]
}];
}
// 自定义视图(可完全自定义每行外观,支持重用)
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
UILabel *label = (UILabel *)view;
if (!label) {
label = [[UILabel alloc] init];
label.textAlignment = NSTextAlignmentCenter;
}
label.text = [NSString stringWithFormat:@"第 %ld 行", (long)row];
return label;
}
// 选中某行时回调
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
NSLog(@"选中第 %ld 列第 %ld 行", (long)component, (long)row);
}
3. 常用操作方法
// 刷新所有列
[self.pickerView reloadAllComponents];
// 刷新指定列
[self.pickerView reloadComponent:0];
// 选中某行(代码触发,不触发 didSelectRow 回调)
[self.pickerView selectRow:2 inComponent:0 animated:YES];
// 获取当前选中的行
NSInteger selectedRow = [self.pickerView selectedRowInComponent:0];
titleForRow、attributedTitleForRow、viewForRow三者实现一个即可,优先级viewForRow>attributedTitleForRow>titleForRow。
二、UITextField 代理与自定义键盘
1. UITextFieldDelegate 常用方法
@interface ViewController () <UITextFieldDelegate>
@end
@implementation ViewController
// 是否允许开始编辑
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
return YES;
}
// 开始编辑
- (void)textFieldDidBeginEditing:(UITextField *)textField {
NSLog(@"开始编辑: %@", textField.text);
}
// 是否允许结束编辑(如输入不合法可阻止结束)
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField {
return YES;
}
// 结束编辑
- (void)textFieldDidEndEditing:(UITextField *)textField {
NSLog(@"结束编辑: %@", textField.text);
}
// 拦截输入内容(返回 NO 禁止修改,常用于只读选择框)
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
// 禁止手动输入,内容只能通过自定义键盘选择
return NO;
}
// 点击键盘 return 键
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder]; // 收起键盘
return YES;
}
// 是否允许清除(点击输入框右侧的清除按钮)
- (BOOL)textFieldShouldClear:(UITextField *)textField {
return YES;
}
@end
2. 自定义键盘(inputView)
通过 inputView 属性替换系统键盘,常见于日期选择、城市选择等场景:
@interface BirthdayTextField : UITextField
@end
@implementation BirthdayTextField
- (void)awakeFromNib {
[super awakeFromNib];
[self setup];
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setup];
}
return self;
}
- (void)setup {
// 禁止手动输入
self.delegate = self;
// 自定义键盘:UIDatePicker
UIDatePicker *datePicker = [[UIDatePicker alloc] init];
datePicker.datePickerMode = UIDatePickerModeDate;
datePicker.locale = [NSLocale localeWithLocaleIdentifier:@"zh_CN"];
// iOS 13.4+ 可设置样式:轮子样式 / 内嵌日历样式
if (@available(iOS 13.4, *)) {
datePicker.preferredDatePickerStyle = UIDatePickerStyleWheels;
}
[datePicker addTarget:self action:@selector(dateChanged:) forControlEvents:UIControlEventValueChanged];
self.inputView = datePicker;
// 键盘上方的辅助视图(完成按钮)
UIToolbar *toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
UIBarButtonItem *flex = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
UIBarButtonItem *done = [[UIBarButtonItem alloc] initWithTitle:@"完成" style:UIBarButtonItemStyleDone target:self action:@selector(doneClicked)];
toolbar.items = @[flex, done];
self.inputAccessoryView = toolbar;
}
- (void)dateChanged:(UIDatePicker *)picker {
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"yyyy-MM-dd";
self.text = [fmt stringFromDate:picker.date];
}
- (void)doneClicked {
[self resignFirstResponder];
}
// 禁止手动输入
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
return NO;
}
@end
inputView替换键盘主体,inputAccessoryView是键盘上方的辅助视图(如"完成""取消"按钮栏),两者可同时使用。
三、KVC 与字典转模型底层
1. setValuesForKeysWithDictionary:
字典转模型时,setValuesForKeysWithDictionary: 遍历字典的所有键值对,对每个 key 调用 setValue:forKey::
[model setValuesForKeysWithDictionary:dict];
2. setValue:forKey: 查找顺序
setValue:forKey: 的底层查找顺序:
- 查找 setter 方法:是否存在
set<Key>:方法(首字母大写,如setName:),存在则调用。 - 查找实例变量:如果未找到 setter,且
accessInstanceVariablesDirectly返回YES(默认 YES),依次查找以下实例变量:_<key>、_is<Key>、<key>、is<Key>,找到则直接赋值。 - 未定义处理:以上都未找到,调用
setValue:forUndefinedKey:,默认实现抛出NSUnknownKeyException异常。
// 简化的底层逻辑
- (void)setValue:(id)value forKey:(NSString *)key {
NSString *setter = [NSString stringWithFormat:@"set%@:", [key capitalizedString]];
// 1. 查找 setter 方法
if ([self respondsToSelector:NSSelectorFromString(setter)]) {
[self performSelector:NSSelectorFromString(setter) withObject:value];
return;
}
// 2. 查找实例变量(_key, _isKey, key, isKey)
if ([self accessInstanceVariablesDirectly]) {
NSArray *ivarNames = @[
[NSString stringWithFormat:@"_%@", key],
[NSString stringWithFormat:@"_is%@", [key capitalizedString]],
key,
[NSString stringWithFormat:@"is%@", [key capitalizedString]]
];
for (NSString *ivarName in ivarNames) {
Ivar ivar = class_getInstanceVariable([self class], ivarName.UTF8String);
if (ivar) {
object_setIvar(self, ivar, value);
return;
}
}
}
// 3. 未定义 key,调用 forUndefinedKey(默认抛异常)
[self setValue:value forUndefinedKey:key];
}
3. 忽略服务器多余字段
服务器返回的 JSON 可能包含模型中未定义的字段,直接调用 setValuesForKeysWithDictionary: 会崩溃。重写 setValue:forUndefinedKey: 可安全忽略:
@implementation BaseModel
// 重写后,未定义的 key 直接忽略,不崩溃
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
// 空实现,忽略未定义的 key
}
@end
这是 MJExtension 等字典转模型框架的底层基础之一。
四、通过 Storyboard 加载控制器
// 加载 Storyboard 文件
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
// 创建初始控制器(Storyboard 中标记为 Is Initial View Controller 的控制器)
UIViewController *vc = [storyboard instantiateInitialViewController];
// 通过 Storyboard ID 创建指定控制器(需在 Storyboard 中设置 Identity → Storyboard ID)
UIViewController *detailVC = [storyboard instantiateViewControllerWithIdentifier:@"DetailViewController"];
使用
instantiateViewControllerWithIdentifier:前,必须在 Storyboard 中选中对应控制器,在 Identity Inspector 中设置 Storyboard ID,否则会抛出运行时异常。
五、通过 XIB 创建控制器
1. 创建与使用
- 新建 XIB 文件,将 File's Owner 的 Custom Class 设置为对应的控制器类。
- 右键 File's Owner,将
viewoutlet 连线到 XIB 中的顶层视图。 - 代码中通过
initWithNibName:bundle:创建:
XMGViewController *vc = [[XMGViewController alloc] initWithNibName:nil bundle:nil];
self.window.rootViewController = vc;
[self.window makeKeyAndVisible];
2. 底层查找逻辑
initWithNibName:bundle: 加载 view 时的查找顺序:
- 如果
nibName不为 nil,加载指定名称的 XIB。 - 如果
nibName为 nil,按以下顺序查找:- 与控制器类名同名的 XIB(如
XMGViewController.xib)。 - 类名去掉
Controller后缀的 XIB(如XMGView.xib)。 - 设备特定后缀(
~iphone/~ipad)。
- 与控制器类名同名的 XIB(如
- 都未找到时,创建一个空白的
UIView作为控制器的 view。
六、控制器 view 的创建与生命周期
1. loadView
loadView 在第一次访问控制器的 view 属性时调用,负责创建控制器的 view。重写时必须手动创建 view,且不要调用 [super loadView](会覆盖自定义实现):
- (void)loadView {
// 手动创建 view(不要调用 [super loadView])
self.view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.view.backgroundColor = [UIColor whiteColor];
}
如果通过 Storyboard/XIB 创建控制器,系统会在
loadView中从 XIB 加载 view,无需手动重写。
2. view 的懒加载
控制器的 view 是懒加载的,只有第一次访问 view 属性时才创建:
// 简化的 view getter 底层逻辑
- (UIView *)view {
if (!_view) {
[self loadView]; // 创建 view
[self viewDidLoad]; // view 加载完成,最常用的初始化入口
}
return _view;
}
viewDidLoad在 view 加载完成后调用,是控制器最常用的初始化入口(添加子视图、加载数据等)。此时 view 的 frame 可能还未最终确定(如 Auto Layout 约束未布局完成),涉及 frame 计算的操作建议放在viewDidLayoutSubviews中。
3. view 默认背景色
纯代码创建的控制器 view,默认 backgroundColor 为 nil(完全透明),如果不设置背景色,会看到下层视图。通常在 viewDidLoad 中设置:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
}
Storyboard/XIB 中创建的控制器 view 默认背景色为白色(由 XIB 配置决定)。
七、导航控制器 UINavigationController
1. 设置根控制器
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootVC = [[OneViewController alloc] init];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:rootVC];
self.window.rootViewController = nav;
[self.window makeKeyAndVisible];
return YES;
}
2. 页面跳转(push / pop)
// push 到下一个页面
- (IBAction)pushToNext:(id)sender {
TwoViewController *two = [[TwoViewController alloc] init];
[self.navigationController pushViewController:two animated:YES];
}
// 返回上一个页面
- (IBAction)popBack:(id)sender {
[self.navigationController popViewControllerAnimated:YES];
}
// 返回根控制器
- (IBAction)popToRoot:(id)sender {
[self.navigationController popToRootViewControllerAnimated:YES];
}
// 返回到指定控制器
- (IBAction)popToSpecific:(id)sender {
for (UIViewController *vc in self.navigationController.viewControllers) {
if ([vc isKindOfClass:[OneViewController class]]) {
[self.navigationController popToViewController:vc animated:YES];
break;
}
}
}
3. 导航栏设置(navigationItem)
每个控制器通过自己的 navigationItem 控制导航栏内容:
- (void)viewDidLoad {
[super viewDidLoad];
// 标题
self.title = @"首页";
self.navigationItem.title = @"首页"; // 等价
// 左按钮
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"菜单" style:UIBarButtonItemStylePlain target:self action:@selector(menuClicked)];
// 右按钮
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addClicked)];
// 多个右按钮
self.navigationItem.rightBarButtonItems = @[
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:nil action:nil],
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit target:nil action:nil]
];
// 返回按钮标题(影响下一个页面的返回按钮)
self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"返回" style:UIBarButtonItemStylePlain target:nil action:nil];
}
4. initWithRootViewController: 底层逻辑
initWithRootViewController: 内部不是调用 pushViewController:animated:,而是直接设置导航控制器的 viewControllers 数组,将根控制器作为导航栈的第一个元素:
// 简化的底层逻辑
- (instancetype)initWithRootViewController:(UIViewController *)rootViewController {
if (self = [super init]) {
_viewControllers = [NSMutableArray arrayWithObject:rootViewController];
rootViewController.navigationController = self;
}
return self;
}
与
pushViewController:animated:不同,initWithRootViewController:不会触发 push 动画,也不会调用 push 相关的代理方法,只是初始化导航栈。
5. 导航控制器的子控制器管理
// 获取导航栈中的所有控制器(数组顺序:底部 → 顶部)
NSArray<UIViewController *> *stack = self.navigationController.viewControllers;
// 获取栈顶控制器(当前显示的控制器)
UIViewController *topVC = self.navigationController.topViewController;
// 获取可见控制器(可能是 topViewController,也可能是模态弹出的控制器)
UIViewController *visibleVC = self.navigationController.visibleViewController;
八、Swift 版本对照
UIPickerView
class ViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate {
let pickerView = UIPickerView()
override func viewDidLoad() {
super.viewDidLoad()
pickerView.dataSource = self
pickerView.delegate = self
pickerView.frame = CGRect(x: 0, y: 100, width: 320, height: 200)
view.addSubview(pickerView)
}
func numberOfComponents(in pickerView: UIPickerView) -> Int { 2 }
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
component == 0 ? 5 : 10
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
"第 \(row) 行"
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
print("选中: \(component) - \(row)")
}
}
自定义键盘
class BirthdayTextField: UITextField, UITextFieldDelegate {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
delegate = self
let datePicker = UIDatePicker()
datePicker.datePickerMode = .date
datePicker.locale = Locale(identifier: "zh_CN")
if #available(iOS 13.4, *) {
datePicker.preferredDatePickerStyle = .wheels
}
datePicker.addTarget(self, action: #selector(dateChanged(_:)), for: .valueChanged)
inputView = datePicker
}
@objc private func dateChanged(_ picker: UIDatePicker) {
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd"
text = fmt.string(from: picker.date)
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
false // 禁止手动输入
}
}
导航控制器
let rootVC = OneViewController()
let nav = UINavigationController(rootViewController: rootVC)
window?.rootViewController = nav
window?.makeKeyAndVisible()
// push
let two = TwoViewController()
navigationController?.pushViewController(two, animated: true)
// pop
navigationController?.popViewController(animated: true)
navigationController?.popToRootViewController(animated: true)
// 导航栏
title = "首页"
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addClicked))
九、总结
- UIPickerView:需实现 DataSource 两个必选方法,Delegate 提供标题/富文本/自定义视图三种展示方式(优先级递减),通过
reloadComponent:刷新、selectRow:inComponent:animated:选中。 - UITextField:
shouldChangeCharactersInRange返回 NO 可禁止手动输入;inputView替换键盘,inputAccessoryView添加辅助视图栏;textFieldShouldReturn:处理 return 键。 - KVC:
setValue:forKey:查找顺序为 setter 方法 → 实例变量(_key/_isKey/key/isKey)→setValue:forUndefinedKey:;重写forUndefinedKey:可安全忽略服务器多余字段。 - 控制器加载:Storyboard 通过
instantiateViewControllerWithIdentifier:(需设 Storyboard ID);XIB 通过initWithNibName:bundle:,nibName 为 nil 时按类名自动查找。 - view 生命周期:view 懒加载,首次访问触发
loadView→viewDidLoad;重写loadView必须手动创建 view 且不调用 super;纯代码 view 默认透明,需设置背景色。 - UINavigationController:
initWithRootViewController:内部设置viewControllers数组(非 push);通过 push/pop 管理导航栈;每个控制器用navigationItem控制标题和左右按钮。

浙公网安备 33010602011771号