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: 的底层查找顺序:

  1. 查找 setter 方法:是否存在 set<Key>: 方法(首字母大写,如 setName:),存在则调用。
  2. 查找实例变量:如果未找到 setter,且 accessInstanceVariablesDirectly 返回 YES(默认 YES),依次查找以下实例变量:_<key>、_is<Key>、<key>、is<Key>,找到则直接赋值。
  3. 未定义处理:以上都未找到,调用 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. 创建与使用

  1. 新建 XIB 文件,将 File's Owner 的 Custom Class 设置为对应的控制器类。
  2. 右键 File's Owner,将 view outlet 连线到 XIB 中的顶层视图。
  3. 代码中通过 initWithNibName:bundle: 创建:
XMGViewController *vc = [[XMGViewController alloc] initWithNibName:nil bundle:nil];
self.window.rootViewController = vc;
[self.window makeKeyAndVisible];

2. 底层查找逻辑

initWithNibName:bundle: 加载 view 时的查找顺序:

  1. 如果 nibName 不为 nil,加载指定名称的 XIB。
  2. 如果 nibName 为 nil,按以下顺序查找:
    • 与控制器类名同名的 XIB(如 XMGViewController.xib)。
    • 类名去掉 Controller 后缀的 XIB(如 XMGView.xib)。
    • 设备特定后缀(~iphone / ~ipad)。
  3. 都未找到时,创建一个空白的 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 控制标题和左右按钮。

posted @ 2015-07-28 22:25  Mr.陳  阅读(1909)  评论(0)    收藏  举报