iOS城市三级联动选择器实战项目(支持Objective-C与Swift)

本文还有配套的精品资源,点击获取

简介:在iOS开发中,城市三级选择器广泛应用于需要精确地理位置的场景,如导航、外卖和求职类应用。本项目"CityChooseDemo"提供了一个完整的城市三级联动实现方案,涵盖Objective-C与Swift两种语言版本,帮助开发者快速集成省、市、区县级联选择功能。项目示例包括UITableView与UIPickerView的灵活运用、动态数据绑定、跨层级联动逻辑处理,并集成了网络请求获取实时城市数据、JSON解析、本地缓存与内存优化等核心功能。通过本项目实践,开发者可掌握高效构建流畅、响应式城市选择界面的关键技术,提升实际开发效率与用户体验。

1. iOS城市三级联动选择器概述

在移动应用开发中,用户常需进行地理位置的选择操作,如填写地址、定位城市等场景。为此,城市三级联动选择器作为一种高效、直观的交互组件被广泛应用于各类App中。本章将系统介绍iOS平台下城市三级联动选择器的核心功能与应用场景,阐述其在用户体验优化中的重要价值。从省、市到区县的逐级筛选机制不仅提升了输入效率,也减少了误操作的可能性。

swift 复制代码
// 示例:城市数据模型简要结构(Swift)
struct District {
    let name: String
    let areaCodes: [String]
}

结合当前主流开发语言Objective-C与Swift的技术生态,实现该控件需综合运用数据建模、UI组件集成、异步处理与内存管理策略。项目【ios-城市3级选择.zip】旨在构建一个高复用、易扩展的城市选择解决方案,支持UITableView与UIPickerView双模式交互,为后续章节的深入实现奠定基础。

2. Objective-C中UITableView与UIPickerView集成实现

在iOS应用开发中,城市三级联动选择器的实现方式多种多样,其中基于 UITableViewUIPickerView 的两种主流控件构建方案因其灵活性和原生支持度而被广泛采用。本章将深入探讨如何在 Objective-C 环境下结合这两个核心 UI 组件来实现一个高效、可维护的城市选择界面。通过对比它们的数据驱动机制、布局策略及交互逻辑,分析各自优势与适用场景,并最终完成一个可扩展的多级联动架构设计。

2.1 UITableView与UIPickerView的基本用法对比

UITableViewUIPickerView 虽然都属于列表型控件,但其设计理念和使用模式存在显著差异。理解这些差异有助于开发者根据实际业务需求做出合理的技术选型。

2.1.1 表视图的数据源与代理模式

UITableView 是 iOS 开发中最常用的滚动列表控件之一,它依赖于 数据源(DataSource)代理(Delegate) 模式进行内容展示和交互控制。这种设计遵循了 MVC 架构原则,实现了视图与数据的解耦。

objective-c 复制代码
@interface CitySelectionViewController () <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSArray *provinces;
@property (nonatomic, strong) NSDictionary *cityData; // 存储省->市映射
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    [self.view addSubview:self.tableView];
}

上述代码展示了基本的表视图初始化流程。关键在于设置 dataSourcedelegate 属性,使控制器能够响应数据请求和用户事件。

接下来是实现数据源方法:

objective-c 复制代码
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.provinces.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellIdentifier = @"CityCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
    }
    cell.textLabel.text = self.provinces[indexPath.row];
    return cell;
}

逻辑分析:

  • numberOfRowsInSection: 返回当前分区中的行数,这里直接返回省份数量。
  • cellForRowAtIndexPath: 创建或复用单元格,填充对应索引处的省份名称。
  • 使用静态标识符 @"CityCell" 实现 Cell 复用机制,避免频繁创建对象,提升性能。

此外, UITableViewDataSource 还包含如 numberOfSectionsInTableView:titleForHeaderInSection: 等可选方法,可用于组织更复杂的层级结构。

方法 作用 是否必需
numberOfSectionsInTableView: 定义表格的分区数量
numberOfRowsInSection: 每个分区的行数
cellForRowAtIndexPath: 提供每行显示的内容
titleForHeaderInSection: 设置分区标题

该模式的优势在于高度灵活,支持自定义 Cell、动态刷新、动画更新等高级特性。然而,这也带来了较高的代码复杂度,尤其是在处理嵌套层级时需手动管理展开状态和数据依赖。

classDiagram class UITableViewDataSource { <> +numberOfSectionsInTableView(UITableView) -> NSInteger +tableView:numberOfRowsInSection(NSInteger) -> NSInteger +tableView:cellForRowAtIndexPath:(NSIndexPath) -> UITableViewCell* } class UITableViewDelegate { <> +tableView:didSelectRowAtIndexPath:(NSIndexPath) +tableView:heightForRowAtIndexPath:(NSIndexPath) -> CGFloat } UITableViewDataSource <|.. CitySelectionViewController UITableViewDelegate <|.. CitySelectionViewController

上图展示了 UITableViewDataSourceUITableViewDelegate 协议的基本结构及其与视图控制器的关系。通过实现这些协议方法,开发者可以完全掌控表格的行为表现。

2.1.2 选择器视图的层级展示特性

UIPickerView 是一种旋转式滚轮选择器,适用于有限选项的精确选取场景。与 UITableView 不同, UIPickerView 支持多列并行显示,天然适合实现"省-市-区"三级联动。

其核心接口同样是基于 数据源代理

objective-c 复制代码
@interface CityPickerViewController () <UIPickerViewDataSource, UIPickerViewDelegate>
@property (nonatomic, strong) UIPickerView *pickerView;
@property (nonatomic, strong) NSArray *provinceList;
@property (nonatomic, strong) NSArray *cityList;
@property (nonatomic, strong) NSArray *districtList;
@end

初始化并绑定数据源:

objective-c 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];

    self.pickerView = [[UIPickerView alloc] initWithFrame:CGRectMake(0, 100, self.view.bounds.size.width, 200)];
    self.pickerView.dataSource = self;
    self.pickerView.delegate = self;
    [self.view addSubview:self.pickerView];
}

实现数据源方法以指定列数和每列行数:

objective-c 复制代码
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
    return 3; // 三列:省、市、区
}

- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
    switch (component) {
        case 0:
            return self.provinceList.count;
        case 1:
            return self.cityList.count;
        case 2:
            return self.districtList.count;
        default:
            return 0;
    }
}

参数说明:

  • numberOfComponentsInPickerView: 返回滚轮组件的数量,此处设为 3,分别代表省、市、区。
  • pickerView:numberOfRowsInComponent: 动态返回每一列的有效选项数,需根据当前选中状态实时更新。

代理方法用于提供显示文本:

objective-c 复制代码
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
    NSArray *data;
    switch (component) {
        case 0: data = self.provinceList; break;
        case 1: data = self.cityList; break;
        case 2: data = self.districtList; break;
        default: return @"";
    }
    return data[row];
}

当用户滚动某一列时,应监听选中变化并联动更新后续列:

objective-c 复制代码
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
    if (component == 0) {
        NSString *selectedProvince = self.provinceList[row];
        self.cityList = [self getCitiesByProvince:selectedProvince];
        self.districtList = @[];
        [self.pickerView reloadComponent:1]; // 重新加载市级
        [self.pickerView reloadComponent:2]; // 清空区级
        [self.pickerView selectRow:0 inComponent:1 animated:YES];
    } else if (component == 1) {
        NSString *selectedCity = self.cityList[row];
        self.districtList = [self getDistrictsByCity:selectedCity];
        [self.pickerView reloadComponent:2];
    }
}

此段代码体现了联动的核心逻辑: 前一级选择触发后一级数据重载 。通过调用 reloadComponent: 可局部刷新某列内容,避免整体重建,提高响应速度。

特性 UITableView UIPickerView
显示形式 垂直列表 滚轮式多列
数据容量 支持大量数据 建议 ≤ 100 项/列
内存占用 较低(Cell 复用) 中等(全部行预渲染)
用户操作习惯 点击为主 滑动滚轮
自定义程度

综上所述, UIPickerView 更适合小范围、高精度的选择任务,尤其在空间受限的弹窗或底部浮层中表现出色;而 UITableView 更擅长展示大规模、结构化数据,适合需要搜索、分组或长列表浏览的场景。

2.1.3 使用场景分析与选型依据

在构建城市三级联动选择器时,技术选型需综合考虑用户体验、性能开销、开发成本以及设备适配能力。

场景一:全屏地址选择页(推荐使用 UITableView

当用户进入独立页面进行详细地址填写时,通常期望看到清晰的层级结构和完整的可选项列表。此时使用 UITableView 可以实现如下功能:

  • 分区展示:每个级别作为一个 section,视觉层次分明;
  • 展开/折叠动画:点击省份行后动态插入市级行,提供流畅过渡;
  • 支持搜索框集成:便于快速定位目标城市;
  • 易于调试与扩展:可通过日志输出 indexPath 变化轨迹。
场景二:表单内嵌选择器(推荐使用 UIPickerView

若选择器作为输入框的附属控件出现(例如点击 UITextField 弹出),则 UIPickerView 更加合适:

  • 占用空间小,常配合 UIDatePicker 共享同一弹出区域;
  • 手势操作直观,滑动即可切换选项;
  • 内置居中高亮效果,增强可读性;
  • 与键盘共存良好,不会遮挡主界面。
决策流程图
graph TD A[是否为全屏独立页面?] -->|是| B[使用 UITableView] A -->|否| C{是否有足够横向空间?} C -->|是| D[使用 UIPickerView] C -->|否| E[考虑组合方案: TableView + 导航栏确认按钮]

此外,还需评估数据量大小。若全国城市总数超过 3000 项,且分布不均(如直辖市下辖区少,边远省份城市稀疏),则 UITableView 的懒加载机制更具优势;反之,若仅涉及少数省份或定制化区域,则 UIPickerView 更简洁高效。

最终决策还应结合团队技术栈偏好。SwiftUI 项目可能倾向于使用 Picker + ForEach 实现声明式 UI,而在传统 Objective-C 工程中, UIPickerView 因其成熟稳定仍被广泛沿用。


2.2 多组件界面布局设计

为了实现美观且兼容性强的城市选择界面,合理的布局设计至关重要。无论是使用 Storyboard/XIB 还是纯代码方式,都需要确保组件排列有序、间距一致,并能适应不同屏幕尺寸。

2.2.1 界面元素的组织方式(Storyboard/XIB vs 手动编码)

在现代 iOS 开发中,界面构建主要有两种方式:可视化编辑器(Storyboard/XIB)和程序化布局(Manual Code)。两者各有优劣。

方式 优点 缺点
Storyboard/XIB 直观拖拽,实时预览,适合新手 文件易冲突,难以版本控制
Manual Code 精确控制,易于复用,适合复杂逻辑 初期学习曲线陡峭

对于城市选择器这类结构相对固定的界面,若团队协作频繁,建议采用 纯代码 + Auto Layout 方式,以提升可维护性。

示例:手动添加标题栏与表格视图

objective-c 复制代码
- (void)setupUI {
    UILabel *titleLabel = [[UILabel alloc] init];
    titleLabel.text = @"请选择所在地区";
    titleLabel.textAlignment = NSTextAlignmentCenter;
    titleLabel.font = [UIFont boldSystemFontOfSize:18];
    [self.view addSubview:titleLabel];
    self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
    self.tableView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:self.tableView];
    // 使用 NSLayoutConstraint 设置约束
    [NSLayoutConstraint activateConstraints:@[
        [titleLabel.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:20],
        [titleLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
        [titleLabel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
        [self.tableView.topAnchor constraintEqualToAnchor:titleLabel.bottomAnchor constant:10],
        [self.tableView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
        [self.tableView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
        [self.tableView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor]
    ]];
}

逐行解析:

  • 第 1--5 行:创建标题标签,设置文字、对齐方式和字体样式;
  • 第 6--7 行:初始化 tableView 并禁用 autoresizing mask 转换,这是使用 Auto Layout 的前提;
  • 第 9--18 行:通过 NSLayoutConstraint activateConstraints: 批量激活约束,确保各元素定位准确;
  • 使用 safeAreaLayoutGuide 兼容 iPhone X 及以上机型的刘海屏安全区。

该方法虽比 Storyboard 多写几行代码,但所有逻辑集中一处,便于调试和迁移。

2.2.2 自动布局约束设置与适配不同屏幕尺寸

自动布局(Auto Layout)是实现跨设备适配的核心技术。通过定义视图间的相对关系,系统可在运行时自动计算最终位置。

关键约束类型包括:

  • 边界对齐 :leading、trailing、top、bottom
  • 尺寸固定 :width、height
  • 居中对齐 :centerX、centerY
  • 比例约束 :multiplier(如宽高比)

以适配 iPad 为例,希望在大屏上左右留白:

objective-c 复制代码
UIView *container = [[UIView alloc] init];
container.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:container];

[NSLayoutConstraint activateConstraints:@[
    [container.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
    [container.widthAnchor constraintLessThanOrEqualToConstant:400], // 最大宽度 400pt
    [container.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:40],
    [container.leadingAnchor constraintGreaterThanOrEqualToAnchor:self.view.leadingAnchor constant:20],
    [container.trailingAnchor constraintLessThanOrEqualToAnchor:self.view.trailingAnchor constant:-20],
    [container.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-40]
]];

此容器会居中显示,最大宽度不超过 400pt,在小屏幕上贴边,在大屏幕上保持紧凑布局。

2.2.3 组件间间距控制与视觉一致性维护

良好的视觉节奏依赖于统一的间距规范。建议定义全局常量:

objective-c 复制代码
static const CGFloat kVerticalSpacing = 8.0f;
static const CGFloat kHorizontalSpacing = 16.0f;
static const CGFloat kCornerRadius = 8.0f;

并在 Cell 或自定义视图中统一应用:

objective-c 复制代码
- (void)layoutSubviews {
    [super layoutSubviews];
    self.contentView.layer.cornerRadius = kCornerRadius;
    self.contentView.clipsToBounds = YES;
    self.textLabel.frame = CGRectMake(kHorizontalSpacing,
                                     kVerticalSpacing,
                                     self.contentView.bounds.size.width - 2 * kHorizontalSpacing,
                                     self.contentView.bounds.size.height - 2 * kVerticalSpacing);
}

同时,使用 UIAppearance 统一主题风格:

objective-c 复制代码
[[UINavigationBar appearance] setTitleTextAttributes:@{NSFontAttributeName: [UIFont boldSystemFontOfSize:17]}];
[[UIButton appearanceWhenContainedInInstancesOfClasses:@[CitySelectionViewController.class]] setTitleColor:[UIColor systemBlueColor] forState:UIControlStateNormal];

确保整个应用内的城市选择器具有一致的视觉语言。


(后续章节将继续深入数据驱动、实践案例等内容,保持相同深度与格式)

3. Swift中结构体与枚举封装城市数据

在现代iOS开发实践中,Swift语言以其强大的类型系统、内存安全性以及表达力丰富的语法特性,成为构建高可维护性应用的首选。尤其在处理复杂层级数据结构如省市区三级联动时,合理利用结构体( struct )和枚举( enum )不仅能提升代码的清晰度,还能增强运行时的安全性与性能表现。本章深入探讨如何基于Swift的值类型机制设计高效的城市数据模型,并通过枚举实现状态驱动的UI逻辑控制,最终完成一个模块化、可复用的城市数据管理组件。

3.1 结构化数据建模原则

城市信息本质上是一个具有明确层级关系的树形结构:每个省份包含若干城市,每个城市又下辖区县。这种"一对多"的嵌套结构天然适合使用结构体进行建模。与Objective-C中的类不同,Swift的 struct 是值类型,具备自动内存管理、线程安全及不可变性保障等优势,特别适用于数据传输对象(DTO)的设计场景。

3.1.1 使用struct表示城市信息的优势(值类型安全)

在Swift中, struct 默认采用值语义传递,意味着每次赋值或函数传参都会创建副本而非引用共享。这一特性对于城市选择器尤为重要------当多个视图控制器同时访问城市数据时,可以避免因某一方修改导致全局状态污染的问题。

swift 复制代码
struct District {
    let id: Int
    let name: String
}

struct City {
    let id: Int
    let name: String
    let districts: [District]
}

struct Province {
    let id: Int
    let name: String
    let cities: [City]
}

代码逻辑逐行解读:

  • District 定义最底层行政区划,仅包含ID和名称。
  • City 包含自身基本信息及下属区县数组,体现一级嵌套。
  • Province 持有城市列表,形成完整三级结构。
  • 所有属性均为常量( let ),确保实例一旦创建不可更改,符合函数式编程理念。

该设计保证了数据的一致性和可预测性。例如,在搜索操作中对某个省份的数据进行筛选,不会影响原始数据源,极大降低了调试难度。

特性 struct(值类型) class(引用类型)
内存管理 栈上分配(小对象更高效) 堆上分配,需ARC管理
赋值行为 复制整个实例 共享同一实例引用
线程安全性 高(无共享状态) 低(需额外同步机制)
可变性控制 通过 mutating 关键字显式声明 默认可变
性能开销 小规模数据更快 大对象频繁拷贝可能慢

表: struct vs class 在城市数据建模中的对比分析

从表中可见,对于读多写少、结构固定的地理数据, struct 是更优选择。

此外,Swift编译器会对结构体进行优化,如内联存储、SIL级别去虚拟化等,进一步提升运行效率。结合JSON解析框架(如 Codable ),可轻松实现序列化支持:

swift 复制代码
struct City: Codable {
    let id: Int
    let name: String
    let districts: [District]
    private enum CodingKeys: String, CodingKey {
        case id = "city_id"
        case name = "city_name"
        case districts
    }
}

此处通过自定义 CodingKeys 映射服务器字段命名规则,提高兼容性。整个过程无需手动解析,大幅减少样板代码。

3.1.2 层级关系建模:Province -> City -> District

为了准确反映行政隶属关系,必须在数据模型中建立清晰的父子连接。虽然上述结构已具备基本层级,但在实际查询过程中仍需快速定位子集。为此引入辅助方法,增强遍历能力。

swift 复制代码
extension Province {
    func city(byId id: Int) -> City? {
        return cities.first { $0.id == id }
    }
    func cityIndex(byId id: Int) -> Int? {
        return cities.firstIndex { $0.id == id }
    }
}
graph TD A[Province] --> B[City] B --> C[District] D[查找 City by ID] --> E{是否存在匹配?} E -- 是 --> F[返回 City 实例] E -- 否 --> G[返回 nil]

图:基于ID的城市查找流程图(Mermaid格式)

上述扩展为 Province 添加按ID检索功能,便于联动更新时精准获取下一级数据。类似地,可在 City 上添加 district(byId:) 方法,形成统一接口风格。

更重要的是,这种嵌套结构支持递归遍历算法。例如,实现全量地址扁平化输出:

swift 复制代码
func flattenAddresses(provinces: [Province]) -> [(province: String, city: String, district: String)] {
    var result: [(String, String, String)] = []
    for province in provinces {
        for city in city.cities {
            for district in city.districts {
                result.append((province.name, city.name, district.name))
            }
        }
    }
    return result
}

此函数将三级结构展平为元组数组,可用于后续排序、过滤或导出CSV文件。由于所有数据均为不可变值类型,该操作完全安全且无副作用。

3.1.3 编码规范与命名约定

良好的命名不仅提升可读性,也利于团队协作与后期维护。遵循Apple官方《Swift API Design Guidelines》,建议采用以下规范:

  • 类型名使用大驼峰(PascalCase): Province , CityDataManager
  • 属性与方法使用小驼峰(camelCase): selectedProvince , loadCities()
  • 布尔属性以形容词开头: hasChildren , isExpanded
  • 方法动词前置: fetchData() , selectRow(at:)

此外,为避免歧义,推荐使用明确语义的复合名称。例如:

swift 复制代码
// ❌ 不够清晰
var selProv: Province?

// ✅ 推荐写法
var selectedProvince: Province?

若项目涉及国际化,还应考虑本地化键的命名一致性:

swift 复制代码
extension Province {
    var localizedDisplayName: String {
        return NSLocalizedString(name, comment: "省份显示名称")
    }
}

综上所述,合理的结构体设计配合严格的编码规范,能够显著提升代码质量与协作效率。

3.2 枚举在多级状态管理中的应用

在城市选择器交互中,用户会经历"选择省 → 选择市 → 选择区"三个阶段,每一阶段对应不同的UI呈现与数据加载逻辑。传统的做法是使用整数标记当前层级(如 level = 1 表示省级),但这种方式缺乏类型安全性且易出错。Swift的枚举提供了更优雅的解决方案。

3.2.1 定义SelectionLevel枚举区分当前选择层级

swift 复制代码
enum SelectionLevel {
    case province
    case city(Province)
    case district(City)
}

此枚举示例展示了如何通过关联值携带上下文信息:

  • province :初始状态,尚未选择任何省份
  • city(Province) :已选省份,需加载其下属城市
  • district(City) :已选城市,准备展示区县

相比简单的整数标记,该枚举具备更强的语义表达能力。更重要的是,它强制编译器检查所有可能的状态分支,防止遗漏处理。

swift 复制代码
func updateUI(for level: SelectionLevel) {
    switch level {
    case .province:
        pickerView.reloadComponent(0)
        pickerView.selectRow(0, inComponent: 0, animated: true)
    case .city(let selectedProvince):
        cities = selectedProvince.cities
        pickerView.reloadComponent(1)
    case .district(let selectedCity):
        districts = selectedCity.districts
        pickerView.reloadComponent(2)
    }
}

每当选择层级变化时,调用 updateUI(for:) 即可自动刷新对应列内容。由于 switch 必须穷尽所有情况,开发者无法忽略任一状态,有效规避了运行时崩溃风险。

3.2.2 关联值传递选中数据

传统代理模式中常需定义多个回调方法(如 didSelectProvince(_:) , didSelectCity(_:) ),而使用带有关联值的枚举,可将所有事件统一为单一类型:

swift 复制代码
enum AddressSelectionEvent {
    case levelChanged(to: SelectionLevel)
    case completed(province: Province, city: City, district: District)
    case cancelled
}

随后可通过闭包统一接收:

swift 复制代码
var onSelectionChange: ((AddressSelectionEvent) -> Void)?

// 触发事件示例
onSelectionChange?(.completed(
    province: currentProvince,
    city: currentCity,
    district: currentDistrict

这使得外部调用者可以用一个处理器响应多种状态变更,简化集成逻辑。

3.2.3 switch语句驱动UI行为切换

结合 SelectionLevel 枚举,可构建动态UI渲染逻辑。以下为 UIPickerViewDataSource 的部分实现:

swift 复制代码
extension CitySelectorViewController: UIPickerViewDataSource {
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 3
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        switch selectionLevel {
        case .province:
            return provinces.count
        case .city(let prov):
            return prov.cities.count
        case .district(let city):
            return city.districts.count
        }
    }
}
stateDiagram-v2 [*] --> ProvinceSelected ProvinceSelected --> CitySelected: 用户选择省份 CitySelected --> DistrictSelected: 用户选择城市 DistrictSelected --> Completed: 确认最终地址 Completed --> [*] note right of CitySelected 此时selectionLevel为.city(_) 数据源仅加载该省下的城市 end note

图:基于枚举的状态流转图(Mermaid格式)

该状态机清晰表达了用户导航路径,并自然引导代码分治。每种状态只关心自己的数据供给,职责分明。

3.3 数据预加载与静态初始化

城市数据通常体量较大(全国约有34个省级单位、300+地级市、2800+县级区划),若每次启动都实时请求网络,将严重影响用户体验。因此应在App启动时预加载本地缓存数据。

3.3.1 内置JSON文件读取城市数据

将城市数据以JSON格式嵌入Bundle资源中:

json 复制代码
// cities.json
[
  {
    "id": 11,
    "name": "北京市",
    "cities": [
      {
        "id": 1101,
        "name": "北京市",
        "districts": [
          { "id": 110101, "name": "东城区" },
          { "id": 110102, "name": "西城区" }
        ]
      }
    ]
  }
]

使用Swift内置 Codable 协议解析:

swift 复制代码
class CityDataManager {
    private var provinces: [Province] = []
    init() {
        loadFromJSON()
    }
    private func loadFromJSON() {
        guard let url = Bundle.main.url(forResource: "cities", withExtension: "json"),
              let data = try? Data(contentsOf: url),
              let decoded = try? JSONDecoder().decode([Province].self, from: data)
        else {
            fatalError("Failed to load city data")
        }
        provinces = decoded
    }
}

参数说明:

  • Bundle.main.url(...) :查找主Bundle中的资源路径
  • Data(contentsOf:) :同步读取文件内容(适用于小文件)
  • JSONDecoder().decode() :反序列化为Swift对象

注意:同步IO仅适用于小型数据集;若文件过大,应改为异步加载并提供占位UI。

3.3.2 Bundle资源访问路径构造

为提高灵活性,可抽象资源路径获取逻辑:

swift 复制代码
extension Bundle {
    func jsonFile(_ name: String) -> URL? {
        return self.url(forResource: name, withExtension: "json")
    }
}

这样可在测试环境中替换Bundle,便于单元测试mock数据。

3.3.3 懒加载属性延迟解析大数据集

对于极大数据集,可使用 lazy var 延迟初始化:

swift 复制代码
lazy var allFlattenedDistricts: [(String, String, String)] = {
    print("正在解析全部区县...")
    return flattenAddresses(provinces: self.provinces)
}()

首次访问 allFlattenedDistricts 时才执行耗时操作,避免阻塞主线程。结合GCD调度,可进一步优化:

swift 复制代码
DispatchQueue.global(qos: .background).async { [weak self] in
    _ = self?.allFlattenedDistricts
}

提前预热数据,使后续搜索响应更快。

3.4 实践案例:Swift版本城市模型完整封装

3.4.1 创建CityDataManager单例管理全局数据

swift 复制代码
final class CityDataManager {
    static let shared = CityDataManager()
    private init() { loadFromJSON() }
    private(set) var provinces: [Province] = []
}

使用单例模式确保数据全局唯一,避免重复加载。

3.4.2 提供按名称/ID查找接口

swift 复制代码
extension CityDataManager {
    func findProvince(byId id: Int) -> Province? {
        provinces.first { $0.id == id }
    }
    func findCity(byId id: Int) -> (province: Province, city: City)? {
        for province in provinces {
            if let city = province.city(byId: id) {
                return (province, city)
            }
        }
        return nil
    }
}

返回元组形式便于同时获取父级信息,满足联动需求。

3.4.3 支持模糊搜索与拼音首字母排序

集成第三方库(如 CFCorrectionLocalelibpinyin )实现拼音转换后,可添加高级查询:

swift 复制代码
func searchDistricts(query: String) -> [District] {
    let lowercased = query.lowercased()
    return allFlattenedDistricts
        .filter { tuple in
            tuple.0.contains(lowercased) ||
            tuple.1.contains(lowercased) ||
            tuple.2.contains(lowercased)
        }
        .map { District(id: 0, name: $0.2) } // 简化示例
}

未来可扩展支持拼音首字母索引栏(A-Z侧边导航),提升大型列表浏览体验。

综上,本章通过结构体与枚举的深度整合,构建了一个类型安全、易于维护的城市数据模型体系,为后续UI绑定与联动逻辑打下坚实基础。

4. UIPickerView数据源绑定与选中事件监听

在iOS开发中, UIPickerView 是实现三级联动选择器的核心组件之一。其灵活的数据源和代理机制使得开发者能够构建出高度定制化的滚轮式选择界面。相较于 UITableView 的列表展开模式, UIPickerView 提供了更直观的空间布局------通过并列的三列分别展示省、市、区,用户只需滑动即可完成逐级筛选。这种交互方式尤其适用于表单填写、地址录入等高频操作场景,具备良好的用户体验一致性。本章将深入剖析 UIPickerView 的数据绑定逻辑与事件响应流程,重点讲解如何基于协议方法实现动态数据加载、状态联动更新以及视觉样式优化,并最终完成一个平滑流畅的城市三级联动选择器。

4.1 UIPickerView的数据源协议实现

UIPickerView 的核心行为由两个协议控制: UIPickerViewDataSourceUIPickerViewDelegate 。其中, 数据源协议(DataSource)负责定义列数与每列行数 ,是联动逻辑的基础支撑层。要实现三级联动,必须正确实现以下三个关键方法:

  • numberOfComponents(in:)
  • pickerView(_:numberOfRowsInComponent:)
  • (可选) pickerView(_:titleForRow:forComponent:)

这些方法共同决定了滚轮的结构与内容呈现。

4.1.1 numberOfComponentsInPickerView:返回三级列数

该方法用于指定 UIPickerView 显示多少"列"(components),对于城市三级联动而言,通常固定为三列:省份、城市、区县。

swift 复制代码
func numberOfComponents(in pickerView: UIPickerView) -> Int {
    return 3 // 固定三列:省、市、区
}

此方法在整个生命周期中仅调用一次,除非手动调用 reloadAllComponents() 或改变视图结构。因此,若未来需支持动态列数(如某些地区仅有两级行政区划),可在该方法中根据业务规则返回不同值。目前我们采用静态设计,确保所有用户面对统一的交互结构。

参数说明

  • pickerView : 当前调用该方法的 UIPickerView 实例,可用于多实例判断。

  • 返回值类型为 Int ,表示组件数量,取值范围 ≥ 1。

扩展性思考:是否应允许动态列数?

虽然中国大陆绝大多数地区都具备"省-市-区"三级结构,但存在特殊情况,例如直辖市(北京、上海)下辖区直接隶属于市级单位,中间无地级市层级;或某些县级市属于省直辖。此时若强行填充"市"列为占位符,可能引发语义混淆。一种解决方案是在数据模型中标记层级深度,然后在此方法中动态返回 23 。例如:

swift 复制代码
var selectedProvince: Province?
func numberOfComponents(in pickerView: UIPickerView) -> Int {
    guard let province = selectedProvince else { return 3 }
    return province.hasDirectDistricts ? 2 : 3
}

这样可以根据当前选中的省份特性调整列数,提升数据准确性。然而这也带来了UI跳变的问题------当从普通省切换到直辖市时,第三列突然消失,影响视觉连贯性。因此实践中更多采用统一三列结构,对无下级区域使用"---"或"请选择"作为提示项。

4.1.2 pickerView:numberOfRowsInComponent:动态计算每列行数

这是数据源中最关键的方法,决定每一列有多少个可选项:

swift 复制代码
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
    switch component {
    case 0:
        return provinces.count
    case 1:
        let selectedProvinceIndex = pickerView.selectedRow(inComponent: 0)
        guard selectedProvinceIndex < provinces.count else { return 0 }
        return provinces[selectedProvinceIndex].cities.count
    case 2:
        let selectedProvinceIndex = pickerView.selectedRow(inComponent: 0)
        let selectedCityIndex = pickerView.selectedRow(inComponent: 1)
        guard selectedProvinceIndex < provinces.count,
              selectedCityIndex < provinces[selectedProvinceIndex].cities.count else { return 0 }
        return provinces[selectedProvinceIndex].cities[selectedCityIndex].districts.count
    default:
        return 0
    }
}
代码逻辑逐行分析:
行号 代码片段 解释
1 func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int 方法签名,接收 pickerView 实例和组件索引
2 switch component 根据列索引分情况处理
3-5 case 0: return provinces.count 第一列显示所有省份总数
6-9 case 1: 开始 获取第一列当前选中行,取出对应省份的城市数量
10-14 case 2: 开始 嵌套获取省市索引,查出区县数量
15+ default: return 0 安全兜底

⚠️ 注意:不能直接使用 selectedProvince 属性,因为 UIPickerView 在初始化或刷新时尚未触发 didSelectRow 事件,必须通过 pickerView.selectedRow(inComponent:) 主动查询当前滚动位置。

该方法会在每次 reloadComponent(_:) 被调用时重新执行,也常被系统频繁调用以测量内容高度。因此内部不应包含复杂运算或网络请求,避免卡顿。

4.1.3 根据选中状态实时更新后续列内容

联动的核心在于"前一列变化 → 后续列重载"。虽然 numberOfRowsInComponent 能动态计算行数,但真正驱动刷新的是外部主动调用:

swift 复制代码
@objc private func pickerViewValueChanged(_ sender: UIPickerView) {
    let selectedProvinceIndex = sender.selectedRow(inComponent: 0)
    // 清空第二列选中状态
    if sender.selectedRow(inComponent: 1) != 0 {
        sender.selectRow(0, inComponent: 1, animated: true)
    }
    // 重载第二列(城市)
    sender.reloadComponent(1)
    // 同步重载第三列(区县)
    sender.reloadComponent(2)
}

我们将此方法绑定至 UIControl.Event.valueChanged 事件:

swift 复制代码
pickerView.addTarget(self, action: #selector(pickerViewValueChanged), for: .valueChanged)

📌 使用 KVO 或 addTarget 监听 valueChanged 是实现联动的关键技巧。

流程图展示联动过程:
graph TD A[用户滑动第一列] --> B{触发 valueChanged 事件} B --> C[调用 pickerViewValueChanged:] C --> D[读取当前省份索引] D --> E[重载第二列数据] E --> F[自动调用 numberOfRowsInComponent] F --> G[更新城市列表] G --> H[同步清空并重载第三列] H --> I[完成联动刷新]
数据依赖关系表格:
列编号 依赖数据源 更新触发条件 是否可为空
0(省) provinces 数组 初始化加载
1(市) provinces[index].cities 第一列变更 是(初始为空)
2(区) cities[index].districts 第二列变更 是(部分城市无细分区)

通过上述机制,实现了真正的"按需加载",既节省内存又保证了数据准确。同时由于 reloadComponent 只刷新指定列,不会引起整个 picker 重绘,性能表现优异。

4.2 代理方法处理滚动停止事件

除了数据源提供内容外, UIPickerViewDelegate 负责处理用户的交互行为,尤其是 pickerView(_:didSelectRow:inComponent:) 方法,它是捕捉用户最终选择的核心入口。

4.2.1 pickerView:didSelectRow:inComponent:响应用户选择

每当用户停止滚动某一列时,系统会立即调用该代理方法:

swift 复制代码
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
    switch component {
    case 0: // 省份变化
        handleProvinceSelection(at: row, pickerView: pickerView)
    case 1: // 城市变化
        handleCitySelection(at: row, pickerView: pickerView)
    case 2: // 区县变化
        handleDistrictSelection(at: row, pickerView: pickerView)
    default:
        break
    }
}

每个处理函数负责更新本地状态并通知下游刷新:

swift 复制代码
private func handleProvinceSelection(at index: Int, pickerView: UIPickerView) {
    guard index < provinces.count else { return }
    selectedProvince = provinces[index]
    // 强制重置后两列
    pickerView.selectRow(0, inComponent: 1, animated: true)
    pickerView.selectRow(0, inComponent: 2, animated: true)
    // 触发 reload,使 numberOfRowsInComponent 生效
    pickerView.reloadComponent(1)
    pickerView.reloadComponent(2)
}

✅ 这里不仅更新了数据模型,还显式设置了下一级的默认选中行为,确保 UI 与数据一致。

4.2.2 联动更新其他列的数据源与显示内容

联动的本质是 数据依赖 + 主动刷新 。当某一级发生变化时,必须强制刷新其子级列的内容:

swift 复制代码
private func handleCitySelection(at cityIndex: Int, pickerView: UIPickerView) {
    let provinceIndex = pickerView.selectedRow(inComponent: 0)
    guard provinceIndex < provinces.count,
          cityIndex < provinces[provinceIndex].cities.count else { return }
    selectedCity = provinces[provinceIndex].cities[cityIndex]
    // 重载第三列(区县)
    pickerView.reloadComponent(2)
    // 若新城市无区县,则自动选中"无"
    if selectedCity.districts.isEmpty {
        pickerView.selectRow(0, inComponent: 2, animated: true)
    }
}

💡 技巧:即使某城市没有区县划分(如儋州市),仍可在界面上保留第三列,仅显示一条"全市"或"不区分"条目,保持界面一致性。

4.2.3 防止无效回调引发的数据错乱

在实际运行中, didSelectRow 可能因程序化调用 selectRow() 而被意外触发,导致无限循环或错误状态更新。为此需加入防抖机制:

swift 复制代码
private var isProgrammaticUpdate = false

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
    guard !isProgrammaticUpdate else { return } // 忽略程序触发
    isProgrammaticUpdate = true
    defer { isProgrammaticUpdate = false } // 作用域结束恢复
    // 正常处理用户选择...
}

此外,在调用 selectRow(..., animated: true) 之前也应设置标记:

swift 复制代码
isProgrammaticUpdate = true
pickerView.selectRow(0, inComponent: 1, animated: true)
isProgrammaticUpdate = false

否则会导致 didSelectRow 被误判为用户操作,从而错误地再次触发联动。

示例场景还原:
操作步骤 是否触发 didSelectRow 是否应处理
用户滑动选择江苏
代码调用 selectRow 切换回北京 否(设 isProgrammaticUpdate = true)
用户继续选择朝阳区

通过该机制,有效隔离了"人为操作"与"系统响应"的边界,防止状态雪崩。

4.3 自定义标题与外观样式

为了提升可读性和品牌一致性,可对 UIPickerView 的文本样式进行深度定制。

4.3.1 pickerView:titleForRow:forComponent:提供文本显示

最基础的方式是返回字符串:

swift 复制代码
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
    switch component {
    case 0:
        return provinces[row].name
    case 1:
        let pIdx = pickerView.selectedRow(inComponent: 0)
        return provinces[pIdx].cities[row].name
    case 2:
        let pIdx = pickerView.selectedRow(inComponent: 0)
        let cIdx = pickerView.selectedRow(inComponent: 1)
        return provinces[pIdx].cities[cIdx].districts[row].name
    default:
        return nil
    }
}

⚠️ 注意数组越界风险,建议封装安全访问扩展。

4.3.2 使用attributedTitle提高可读性

支持富文本格式,可用于突出当前选中项或添加辅助信息:

swift 复制代码
func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
    let plainString = /* 获取名称 */
    let style: UIFont.TextStyle = component == pickerView.selectedRow(inComponent: component) ? .headline : .body
    let font = UIFont.preferredFont(forTextStyle: style)
    let color = component == 0 ? UIColor.systemBlue : UIColor.label
    return NSAttributedString(string: plainString, attributes: [
        .font: font,
        .foregroundColor: color,
        .kern: 0.8
    ])
}

✅ 支持 .preferredFont 实现动态字体适配,符合无障碍设计规范。

4.3.3 设置字体颜色与对齐方式增强视觉体验

结合 viewForRow 可进一步自定义视图:

swift 复制代码
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
    var label = view as? UILabel
    if label == nil {
        label = UILabel()
        label?.textAlignment = .center
        label?.adjustsFontSizeToFitWidth = true
    }
    label?.text = getTitleForRow(row, component: component)
    label?.textColor = getTextColor(for: component, at: row)
    label?.font = getFont(for: component)

    return label!
}
样式配置对照表:
属性 推荐值 说明
字体大小 UIFont.preferredFont(forTextStyle: .body) 支持动态字体
文字颜色 主列深色,辅列灰色 强化视觉层次
对齐方式 居中对齐 适配滚轮居中聚焦
字间距 .kern: 0.5~1.0 提升辨识度

4.4 实践案例:实现平滑联动的滚轮选择器

4.4.1 初始化三列分别对应省市区

viewDidLoad 中完成基本配置:

swift 复制代码
override func viewDidLoad() {
    super.viewDidLoad()
    pickerView.dataSource = self
    pickerView.delegate = self
    // 加载初始数据
    loadCityData()
    // 默认选中第一项
    pickerView.selectRow(0, inComponent: 0, animated: false)
    pickerView.selectRow(0, inComponent: 1, animated: false)
    pickerView.selectRow(0, inComponent: 2, animated: false)
    // 绑定值变化事件
    pickerView.addTarget(self, action: #selector(pickerViewValueChanged), for: .valueChanged)
}

4.4.2 第一列变化后清空后两列并重新加载

已在 handleProvinceSelection 中实现:

swift 复制代码
private func handleProvinceSelection(at index: Int, pickerView: UIPickerView) {
    selectedProvince = provinces[index]
    // 清空子级选择
    pickerView.selectRow(0, inComponent: 1, animated: true)
    pickerView.selectRow(0, inComponent: 2, animated: true)
    // 通知 reload
    pickerView.reloadComponent(1)
    pickerView.reloadComponent(2)
}

4.4.3 记录选中路径并在确认时输出完整地址

添加确认按钮回调:

swift 复制代码
@IBAction func confirmSelection(_ sender: UIButton) {
    let pIdx = pickerView.selectedRow(inComponent: 0)
    let cIdx = pickerView.selectedRow(inComponent: 1)
    let dIdx = pickerView.selectedRow(inComponent: 2)
    let province = provinces[pIdx].name
    let city = provinces[pIdx].cities[cIdx].name
    let district = provinces[pIdx].cities[cIdx].districts[dIdx].name
    let fullAddress = "\(province)\(city)\(district)"
    delegate?.citySelector(didSelect: fullAddress)
}

至此,一个功能完整、交互流畅的三级联动 UIPickerView 已成功集成。

5. 省市区三级联动逻辑设计与状态更新

城市三级联动选择器的用户体验核心在于其"联动"机制------即用户在选择某一行政区划层级(如省份)后,后续层级(城市、区县)能够自动更新为对应的子级数据。这种动态依赖关系看似简单,实则涉及复杂的状态管理、数据映射、边界处理和UI同步策略。本章将深入剖析实现这一功能所需的完整逻辑架构,重点围绕 状态追踪、数据依赖更新、索引映射机制、跨层级跳转一致性保障 等关键点展开,构建一个可复用、高内聚、低耦合的联动引擎。

5.1 联动状态机模型设计

5.1.1 状态机驱动的联动流程抽象

在实现省市区三级联动时,若直接通过事件触发多个 if-else 判断来控制数据刷新,极易导致代码混乱、难以维护。为此,引入 有限状态机(Finite State Machine, FSM) 模型是一种行之有效的工程实践。该模型将整个选择过程划分为若干明确状态,并定义状态间的转移规则,从而提升逻辑清晰度与可测试性。

例如,在三级联动中可以定义如下状态:

状态名称 描述
.provinceSelected 用户已选省份,等待加载城市列表
.citySelected 用户已选城市,等待加载区县列表
.districtSelected 用户已完成最终选择
.idle 初始或重置状态

通过 Swift 枚举结合关联值的方式,可进一步增强状态表达能力:

swift 复制代码
enum SelectionState {
    case idle
    case provinceSelected(province: Province)
    case citySelected(province: Province, city: City)
    case districtSelected(province: Province, city: City, district: District)
}

上述定义不仅记录当前所处阶段,还携带了上下文数据,便于后续操作直接使用。

状态转移图(Mermaid)
stateDiagram-v2 [*] --> idle idle --> provinceSelected : 选择省份 provinceSelected --> citySelected : 选择城市 citySelected --> districtSelected : 选择区县 districtSelected --> idle : 提交/重置 provinceSelected --> idle : 取消

此图清晰展示了用户在不同交互行为下的路径流转,有助于开发者从整体视角理解系统行为。

5.1.2 状态变更触发的数据联动机制

当状态发生改变时,必须触发相应的 UI 更新动作。以 UIPickerView 为例,每列对应一个组件(component),其数据源来源于当前状态下的可选项集合。以下是典型的联动响应逻辑:

swift 复制代码
func updateComponents(for state: SelectionState) {
    switch state {
    case .idle:
        provinces = loadAllProvinces()
        cities = []
        districts = []
    case .provinceSelected(let selectedProvince):
        provinces = loadAllProvinces()
        cities = selectedProvince.cities
        districts = []
    case .citySelected(let _, let selectedCity):
        districts = selectedCity.districts
    case .districtSelected:
        break // 已完成选择
    }
    pickerView.reloadAllComponents()
}

代码逻辑逐行解读:

  • 第3行:根据传入的新状态进行模式匹配。
  • 第5~8行:进入 .idle 状态时,仅加载所有省份,清空城市与区县列表,确保无残留数据干扰。
  • 第10~13行:当省份被选中时,保留省份列表不变,加载其所辖城市,同时清空区县(防错联动)。 参数说明: selectedProvince.cities 是结构体 Province 的属性,类型为 [City] ,表示子城市数组。
  • 第15~17行:城市选定后,立即加载其下属区县。
  • 第19行:最终状态无需更新,但可用于启用"确认"按钮或高亮显示完整地址。
  • 最后调用 reloadAllComponents() 强制 UIPickerView 重新读取各列数据源,反映最新状态。

该机制的关键优势在于: 将 UI 更新完全封装在状态变化之后,实现了视图与逻辑解耦 。任何外部模块只需修改状态,即可自动触发正确的界面刷新。

5.1.3 防止无效状态跃迁的安全校验

由于用户可能快速滑动滚轮或点击取消,存在短时间内多次触发选择事件的风险。若不加以控制,可能导致中间状态缺失(如跳过城市直接设置区县),造成数据错乱。

为此,需加入前置验证逻辑:

swift 复制代码
func didSelectCity(_ city: City, in province: Province) -> Bool {
    guard case .provinceSelected(let selectedProvince) = currentState,
          selectedProvince.id == province.id else {
        print("非法状态跃迁:当前未处于省份选择状态")
        return false
    }
    currentState = .citySelected(province: province, city: city)
    return true
}

参数说明:

  • city : 当前选中的城市对象。
  • province : 所属省份,用于上下文比对。

逻辑分析:

使用 guard 对当前状态进行双重校验:

  1. 是否为 .provinceSelected 类型;

  2. 当前选中省份是否与传入一致。

若任一条件不满足,则拒绝状态更新并返回 false ,防止非法操作破坏数据链完整性。

此类防护措施对于构建健壮的联动系统至关重要,尤其在异步数据加载场景下更应加强状态一致性检查。

5.2 数据依赖关系建模与索引映射

5.2.1 城市数据层级结构设计

为了支持高效的父子查询,城市数据必须采用树形结构组织。常见的做法是使用嵌套结构体,形成明确的层级依赖:

swift 复制代码
struct Province {
    let id: Int
    let name: String
    let cities: [City]
}

struct City {
    let id: Int
    let name: String
    let districts: [District]
}

struct District {
    let id: Int
    let name: String
}

该模型体现了清晰的"一对多"关系,便于遍历和筛选。但在实际运行中,频繁查找某省下所有城市会带来性能开销,因此需要建立缓存索引。

5.2.2 建立哈希表加速子级检索

为避免每次都要线性搜索整个省份数组,可通过字典预构建索引:

swift 复制代码
private var provinceIDToCities: [Int: [City]] = [:]

func preloadIndex() {
    for province in allProvinces {
        provinceIDToCities[province.id] = province.cities
    }
}

参数说明:

  • allProvinces : 全量省份数据数组,通常由 JSON 解析而来。
  • provinceIDToCities : 键为省份 ID,值为对应城市列表,时间复杂度 O(1) 查询。

结合此索引,可在用户选择省份时快速获取城市列表:

swift 复制代码
func cities(for provinceID: Int) -> [City] {
    return provinceIDToCities[provinceID] ?? []
}

扩展讨论:

若数据量极大(如包含乡镇四级联动),还可引入 Trie 树或倒排索引支持模糊搜索。但对于标准三级联动,哈希表已足够高效。

5.2.3 行号与数据对象的双向绑定机制

UIPickerView 中,组件显示的是"行号",而业务逻辑处理的是"数据对象"。因此必须建立两者之间的映射关系:

UIPickerView Component 数据来源 显示内容
第0列(省份) provinces 数组 各省份名称
第1列(城市) cities 数组 当前省份的城市名
第2列(区县) districts 数组 当前城市的区县名

每次滚动结束时,需将行号转换为具体对象:

swift 复制代码
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
    switch component {
    case 0:
        let selectedProvince = provinces[row]
        currentState = .provinceSelected(province: selectedProvince)
        updateComponents(for: currentState)
    case 1:
        guard case .provinceSelected(let province) = currentState else { return }
        let selectedCity = cities[row]
        currentState = .citySelected(province: province, city: selectedCity)
        updateComponents(for: currentState)
    default:
        break
    }
}

逻辑分析:

  • row 参数表示用户选中的行索引。
  • 通过数组下标访问对应数据对象(如 provinces[row] )。
  • 更新状态后调用统一刷新方法,保证联动一致性。

注意:此处假设 cities 已随省份切换而更新,否则会出现越界错误。

5.3 跨层级状态一致性维护

5.3.1 清除后继列的历史残留问题

常见 Bug:用户先选择"广东省",再切换到"海南省",此时第二列仍显示"广州、深圳"等旧城市。这是因未主动清除后续列所致。

正确做法是在每一级变更时清空其后的所有列:

swift 复制代码
func selectProvince(at index: Int) {
    guard index < provinces.count else { return }
    let newProvince = provinces[index]
    cities = newProvince.cities
    districts = [] // 关键:清除区县
    pickerView.reloadComponent(1) // 重新加载城市列
    pickerView.reloadComponent(2) // 区县列变为空白
    pickerView.select(row: 0, inComponent: 1, animated: true) // 可选:重置城市选择
}

参数说明:

  • reloadComponent(_:) : 通知 UIPickerView 重新请求指定列的数据。
  • select(row:inComponent:animated:) : 可用于默认选中第一项,提升体验。

5.3.2 记录选中路径以支持回填与校验

为支持"编辑已有地址"功能,需保存每一级的选中行号。定义路径容器如下:

swift 复制代码
struct SelectionPath {
    var provinceRow: Int?
    var cityRow: Int?
    var districtRow: Int?
    func isValid() -> Bool {
        return provinceRow != nil && cityRow != nil && districtRow != nil
    }
}

当加载已有地址时,可逆向定位:

swift 复制代码
func restoreSelection(from address: Address) {
    if let pIndex = provinces.firstIndex(where: { $0.name == address.province }) {
        provinceRow = pIndex
        pickerView.select(row: pIndex, inComponent: 0, animated: false)
        let selectedProvince = provinces[pIndex]
        cities = selectedProvince.cities
        if let cIndex = cities.firstIndex(where: { $0.name == address.city }) {
            cityRow = cIndex
            pickerView.select(row: cIndex, inComponent: 1, animated: false)
            let selectedCity = cities[cIndex]
            districts = selectedCity.districts
            if let dIndex = districts.firstIndex(where: { $0.name == address.district }) {
                districtRow = dIndex
                pickerView.select(row: dIndex, inComponent: 2, animated: false)
            }
        }
    }
}

执行逻辑说明:

逐层查找匹配项,依次更新数据源并设置选中位置。 animated: false 避免视觉跳动,适合初始化场景。

5.3.3 处理空子级情况的UI反馈

某些行政区可能没有下一级(如直辖市下辖区为空)。此时应提供合理提示:

swift 复制代码
func handleEmptyChildren(for city: City) {
    if city.districts.isEmpty {
        let alert = UIAlertController(title: "提示", message: "\(city.name)暂无下辖区划", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "确定", style: .default))
        present(alert, animated: true)
    }
}

也可在 UI 上禁用第三列或显示灰色占位符文本,提升可用性。

5.4 联动引擎的模块化封装

5.4.1 抽象联动控制器协议

为实现多 UI 组件兼容(Table View / Picker View),应定义通用接口:

swift 复制代码
protocol CityLinkageEngineDelegate: AnyObject {
    func linkageEngine(_ engine: CityLinkageEngine, didUpdateSelection selection: SelectionState)
}

class CityLinkageEngine {
    weak var delegate: CityLinkageEngineDelegate?
    private(set) var currentState: SelectionState = .idle
    func selectProvince(_ province: Province) {
        currentState = .provinceSelected(province: province)
        delegate?.linkageEngine(self, didUpdateSelection: currentState)
    }
    func selectCity(_ city: City) {
        guard case .provinceSelected(let province) = currentState else { return }
        currentState = .citySelected(province: province, city: city)
        delegate?.linkageEngine(self, didUpdateSelection: currentState)
    }
}

优势分析:

  • 将业务逻辑集中于 CityLinkageEngine ,与具体 UI 解耦。
  • 通过委托模式通知外界状态变更,支持多种视图适配。

5.4.2 支持异步数据加载的扩展设计

若城市数据来自网络,需考虑延迟加载:

swift 复制代码
func loadCities(for provinceID: Int, completion: @escaping ([City]) -> Void) {
    NetworkService.shared.fetchCities(provinceID: provinceID) { result in
        switch result {
        case .success(let cities):
            completion(cities)
        case .failure(let error):
            print("加载城市失败: $error)")
            completion([])
        }
    }
}

@escaping 闭包说明:

因网络回调发生在函数返回之后,故需标记为逃逸闭包,确保内存安全。

此时联动逻辑需调整为异步等待:

swift 复制代码
func didSelectProvince(_ province: Province) {
    showLoadingIndicator()
    loadCities(for: province.id) { [weak self] cities in
        self?.cities = cities
        self?.updateComponents(for: .provinceSelected(province: province))
        self?.hideLoadingIndicator()
    }
}

综上所述,省市区三级联动并非简单的数据展示,而是融合了状态管理、数据建模、UI同步与异常处理的综合性系统工程。通过引入状态机、索引优化、路径追踪与模块化封装,可显著提升代码质量与用户体验稳定性,为复杂表单场景提供坚实支撑。

6. KVO与代理模式在OC中的应用

在Objective-C开发中,组件间的通信机制是构建可维护、高内聚架构的核心。尤其是在实现城市三级联动选择器这类涉及多层级数据状态同步的场景下,如何高效地传递状态变更信息、解耦视图逻辑与业务逻辑,成为决定系统健壮性与扩展性的关键因素。本章将深入探讨两种经典的设计模式------键值观察(KVO)与代理(Delegate),分析其底层原理、使用方式及最佳实践,并结合实际案例展示如何在 ios-城市3级选择.zip 项目中整合这两种机制,以实现省市区数据的动态响应式更新和跨组件通信。

6.1 KVO(键值观察)机制原理

KVO(Key-Value Observing)是Foundation框架提供的一种基于观察者模式的属性监听机制,允许对象注册为另一个对象特定属性的观察者,在该属性发生改变时自动接收通知。这一特性非常适合用于实现"当某一级行政区划被选中后,自动触发下一级列表刷新"的联动需求。

6.1.1 注册观察者与接收属性变更通知

要启用KVO,首先需要通过 addObserver:forKeyPath:options:context: 方法注册一个观察者。假设我们在 CitySelectionManager 类中定义了一个表示当前选中省份的属性:

objective-c 复制代码
@interface CitySelectionManager : NSObject

@property (nonatomic, strong) Province *selectedProvince;
@property (nonatomic, strong) NSArray *cities; // 根据selectedProvince动态加载

@end

当外部控制器希望监听 selectedProvince 的变化以便更新市级列表时,可以这样注册观察者:

objective-c 复制代码
// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    self.selectionManager = [[CitySelectionManager alloc] init];
    [self.selectionManager addObserver:self
                           forKeyPath:@"selectedProvince"
                              options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                              context:&SelectedProvinceContext];
}

其中, SelectedProvinceContext 是一个唯一的上下文指针,用于区分不同观察路径或避免父类KVO冲突:

c 复制代码
static void *SelectedProvinceContext = &SelectedProvinceContext;

随后重写 observeValueForKeyPath:ofObject:change:context: 方法来处理回调:

objective-c 复制代码
- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change 
                       context:(void *)context {
    if (context == &SelectedProvinceContext) {
        if ([keyPath isEqualToString:@"selectedProvince"]) {
            Province *newProvince = change[NSKeyValueChangeNewKey];
            [self loadCitiesForProvince:newProvince];
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
代码逻辑逐行解读:
  • 第4行 :判断上下文是否匹配预设的 SelectedProvinceContext ,这是防止多个KVO共用同一回调时产生误判的安全做法。
  • 第5行 :确认监听的是 selectedProvince 属性变更。
  • 第6行 :从 change 字典中提取新值(即新选中的省份)。
  • 第7行 :调用本地方法加载对应的城市列表,完成联动逻辑。
参数 类型 说明
keyPath NSString* 被观察的属性路径,支持嵌套如 user.address.city
object id 发生变化的对象实例
change NSDictionary* 包含旧值、新值、变化类型等元数据
context void* 用户自定义上下文,用于精确匹配
graph TD A[用户选择省份] --> B[CitySelectionManager.selectedProvince赋值] B --> C{KVO触发} C --> D[ViewController收到observeValue回调] D --> E[调用loadCitiesForProvince:] E --> F[刷新UI显示城市列表]

此流程展示了KVO如何实现松耦合的状态传播,无需主动调用刷新接口即可完成数据驱动的界面更新。

6.1.2 动态触发数据同步更新

KVO之所以能"自动"感知属性变化,依赖于Objective-C运行时对符合KVC(Key-Value Coding)规范的属性进行的动态方法调用拦截。当一个属性被声明为 @dynamic 或由编译器合成( @synthesize )时,其setter方法会被自动包装成支持KVO通知的形式。

例如, self.selectedProvince = province; 实际上会执行类似以下逻辑:

objc 复制代码
- (void)setSelectedProvince:(Province *)selectedProvince {
    [self willChangeValueForKey:@"selectedProvince"];
    _selectedProvince = selectedProvince;
    [self didChangeValueForKey:@"selectedProvince"];
}

这两个通知方法由系统插入,用于广播变更事件给所有注册的观察者。这意味着只有通过标准访问器修改属性才能触发KVO;若直接操作实例变量(如 _selectedProvince = ... ),则不会生效。

此外,对于集合类属性(如数组),还可使用 mutableArrayValueForKey: 获取可变代理,从而监听内部元素增删:

objective-c 复制代码
NSMutableArray *proxiedArray = [self mutableArrayValueForKey:@"cities"];
[proxiedArray addObject:city]; // 自动触发KVO通知

这使得KVO不仅适用于标量属性,也能有效监控复杂结构的数据变动。

6.1.3 移除观察者防止内存泄漏

由于KVO持有观察者的强引用,若未显式移除观察者,极易导致野指针访问或内存泄漏。因此必须在适当时机调用 removeObserver:forKeyPath:context:

通常在 deallocviewWillDisappear: 中执行清理:

objective-c 复制代码
- (void)dealloc {
    [self.selectionManager removeObserver:self forKeyPath:@"selectedProvince" context:&SelectedProvinceContext];
}

// 或者在ARC环境下更安全的方式:
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    if ([self.selectionManager observationInfo]) {
        [self.selectionManager removeObserver:self forKeyPath:@"selectedProvince"];
    }
}

⚠️ 注意:重复移除或对已释放对象发送消息会导致崩溃。推荐使用 try/catch 包裹或检查 observationInfo 是否存在。

6.2 代理模式定义通信契约

代理(Delegate)是一种行为设计模式,常用于定义两个对象之间的"一对一"通信协议。它通过预先声明的方法集,让一个对象(委托方)在特定事件发生时通知另一个对象(代理方)。相比KVO,代理更适合主动事件通知,如"用户完成选择"、"取消操作"等语义明确的动作。

6.2.1 声明CitySelectorDelegate协议

我们可以在 CitySelectorView.h 中定义如下协议:

objective-c 复制代码
@protocol CitySelectorDelegate <NSObject>

- (void)didSelectFullAddress:(nonnull NSString *)provinceName
                        city:(nonnull NSString *)cityName
                    district:(nullable NSString *)districtName;

- (void)didCancelCitySelection;

@end

@interface CitySelectorView : UIView

@property (nonatomic, weak) id<CitySelectorDelegate> delegate;

@end

该协议规定了两种回调:

  • didSelectFullAddress:city:district: :当选中完整地址后通知宿主视图控制器;

  • didCancelCitySelection :用户点击取消按钮时触发。

参数说明:
方法 参数名 类型 是否必传 含义
didSelectFullAddress provinceName NSString* 省份名称
cityName NSString* 城市名称
districtName NSString* 区县名称(可能为空)
didCancelCitySelection ------ ------ ------ 无参数

这种设计清晰表达了组件意图,提升了API可读性。

6.2.2 didSelectFullAddress:方法传递结果

当用户在UIPickerView中完成三级选择并点击"确定"按钮时,触发代理回调:

objective-c 复制代码
// CitySelectorView.m
- (IBAction)confirmButtonTapped:(id)sender {
    NSString *provinceName = self.provinces[self.selectedProvinceIndex];
    NSString *cityName = self.cities[self.selectedCityIndex];
    NSString *districtName = self.districts.count > 0 ? self.districts[self.selectedDistrictIndex] : nil;

    if ([self.delegate respondsToSelector:@selector(didSelectFullAddress:city:district:)]) {
        [self.delegate didSelectFullAddress:provinceName city:cityName district:districtName];
    }
}

宿主VC只需遵循协议并实现方法即可接收数据:

objective-c 复制代码
// ViewController.m
- (void)didSelectFullAddress:(NSString *)provinceName city:(NSString *)cityName district:(NSString *)districtName {
    self.addressLabel.text = [NSString stringWithFormat:@"%@ %@ %@", provinceName, cityName, districtName ?: @""];
    [self dismissViewControllerAnimated:YES completion:nil];
}

这种方式避免了直接暴露内部组件细节,实现了良好的封装性。

6.2.3 解耦视图控制器与数据管理器

代理模式的核心价值在于 职责分离CitySelectorView 仅负责UI交互,不关心结果如何处理;而 ViewController 作为代理,专注于业务响应,无需了解选择器内部实现。

这种松耦合结构极大增强了组件复用能力。同一个 CitySelectorView 可被多个VC使用,每个VC根据自身需求定制回调逻辑,而无需修改选择器源码。

6.3 模式对比与适用场景

虽然KVO与代理都能实现对象间通信,但它们的设计哲学和适用场景存在显著差异。

6.3.1 KVO适合监听内部状态变化

特性 描述
被动监听 KVO关注"属性何时变了",属于反应式编程范畴
细粒度控制 可监听任意KVC兼容属性,包括私有字段
自动触发 无需手动调用,只要setter被执行即通知
性能开销 运行时动态派发有一定成本,不宜频繁监听

✅ 推荐场景:

  • 监听模型层属性变化自动刷新UI

  • 多个组件依赖同一状态源

  • 需要实时响应内部状态迁移

❌ 不推荐:

  • 替代事件通知(如按钮点击)

  • 监听大量属性造成回调泛滥

6.3.2 代理更适合主动事件通知

特性 描述
主动通知 代理表达"发生了什么事",强调行为语义
接口契约 协议明确定义可用方法,增强类型安全
单向通信 通常是一对一,避免广播风暴
易于调试 调用栈清晰,便于追踪事件流向

✅ 推荐场景:

  • 控件完成操作后的结果反馈

  • 定制化交互行为(如编辑、删除)

  • 构建可插拔UI组件

❌ 不推荐:

  • 高频状态同步(如滑块实时位置)

  • 多播事件(应使用NSNotification)

6.3.3 在复杂组件中组合使用两种模式

在真实项目中,往往需要协同使用KVO与代理。以城市选择器为例:

flowchart LR subgraph DataLayer [数据管理层] direction TB A[CityDataManager] A -- KVO --> B((selectedLevel)) A -- KVO --> C((filteredCities)) end subgraph UILayer [UI层] D[CitySelectorViewController] D -- Delegate --> E[HostViewController] D -- Observe --> A end style A fill:#f9f,stroke:#333 style D fill:#bbf,stroke:#333 style E fill:#dfd,stroke:#333
  • KVO :用于监听 CityDataManager 内部状态(如当前层级、筛选结果),实现UI自动刷新;
  • Delegate :用于向上层VC报告最终选择结果或取消动作。

两者分工明确:KVO维持"状态一致性",代理负责"事件传达"。

6.4 实践案例:在Objective-C项目中整合KVO与代理

我们将在一个完整的Objective-C项目中演示如何融合KVO与代理,构建一个稳定的城市三级选择器。

6.4.1 观察selectedProvince属性触发城市加载

CitySelectorViewController.m 中:

objective-c 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];

    _dataManager = [CityDataManager sharedInstance];

    // 使用KVO监听省份变化
    [self.dataManager addObserver:self
                       forKeyPath:@"selectedProvince"
                          options:NSKeyValueObservingOptionNew
                          context:&kSelectedProvinceContext];

    [self loadData];
}

- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary *)change 
                       context:(void *)context {
    if (context == &kSelectedProvinceContext) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self reloadCityList];
        });
    }
}

- (void)reloadCityList {
    NSArray *cities = self.dataManager.citiesForSelectedProvince;
    self.pickerView.reloadComponent(1); // 重新加载城市列
}

此处利用KVO实现了"选省→自动加载市"的无缝衔接。

6.4.2 通过代理将最终结果回调给宿主VC

定义并调用代理:

objective-c 复制代码
// CitySelectorViewController.h
@property (nonatomic, weak) id<CitySelectorDelegate> delegate;

// 确认按钮点击
- (IBAction)doneAction:(id)sender {
    NSString *fullAddress = [self generateFullAddressString];
    if (self.delegate && [self.delegate respondsToSelector:@selector(didSelectFullAddress:)]) {
        [self.delegate didSelectFullAddress:fullAddress];
    }
    [self dismissViewControllerAnimated:YES completion:nil];
}

宿主VC实现代理:

objective-c 复制代码
// HostViewController.m
- (void)presentCitySelector {
    CitySelectorViewController *vc = [[CitySelectorViewController alloc] init];
    vc.delegate = self;
    [self presentViewController:vc animated:YES completion:nil];
}

- (void)didSelectFullAddress:(NSString *)address {
    self.userLocationLabel.text = address;
}

6.4.3 异常情况下发送cancelSelection消息

添加取消处理:

objective-c 复制代码
- (IBAction)cancelAction:(id)sender {
    if ([self.delegate respondsToSelector:@selector(didCancelCitySelection)]) {
        [self.delegate didCancelCitySelection];
    }
    [self dismissViewControllerAnimated:YES completion:nil];
}

并在VC中统一处理:

objective-c 复制代码
- (void)didCancelCitySelection {
    NSLog(@"User canceled city selection.");
    // 可记录埋点或恢复默认状态
}

整个架构体现出清晰的分层思想:

  • 数据层通过KVO对外暴露状态变更;

  • UI层通过代理向上汇报关键事件;

  • 两者结合形成闭环,兼顾响应性与可控性。

组件 通信方式 方向 典型用途
DataManager → SelectorVC KVO 下行 状态同步
SelectorVC → HostVC Delegate 上行 事件通知
HostVC → SelectorVC Property Set 上行 初始化配置

综上所述,合理运用KVO与代理模式,不仅能提升代码组织效率,还能显著增强系统的可测试性与可扩展性,是iOS开发中不可或缺的工程实践工具。

7. Swift闭包实现组件间通信

在Swift语言中,闭包(Closure)作为一种轻量、灵活的函数式编程特性,已成为组件间通信的重要范式。相较于Objective-C中的代理模式,闭包能够以更简洁的语法实现在城市选择器这类UI组件中结果的回调传递,尤其适用于单次交互、生命周期短暂的场景。

7.1 闭包的基本语法与尾随闭包优化

Swift中的闭包类似于Objective-C中的Block,但具备更强的类型推断能力和语法糖支持。定义一个用于接收省市区选择结果的闭包如下:

swift 复制代码
typealias CitySelectionHandler = (String?, String?, String?) -> Void

该类型表示一个接受三个可选字符串参数(省、市、区),无返回值的函数类型。在城市选择器类中,可以将其作为属性声明:

swift 复制代码
class CityPickerViewController: UIViewController {
    var onCitySelected: CitySelectionHandler?
    // 模拟用户完成选择时触发回调
    private func didFinishPicking() {
        let province = "广东省"
        let city = "深圳市"
        let district = "南山区"
        onCitySelected?(province, city, district)
    }
}

使用尾随闭包(Trailing Closure)调用方式,使调用代码更加清晰:

swift 复制代码
let pickerVC = CityPickerViewController()
pickerVC.onCitySelected = { [weak self] province, city, district in
    guard let self = self else { return }
    print("选中地址:\(province ?? "")-\(city ?? "")-\(district ?? "")")
    self.updateAddressLabel(province, city, district)
}
present(pickerVC, animated: true)

7.2 逃逸闭包与异步数据加载

当城市数据需要从网络异步加载时,闭包必须标记为 @escaping ,表示其执行时间超出函数作用域:

swift 复制代码
func loadCityData(completion: @escaping (Bool) -> Void) {
    DispatchQueue.global().async {
        // 模拟网络请求延迟
        usleep(500_000)
        let success = true
        DispatchQueue.main.async {
            completion(success)
        }
    }
}

此处 completion 是逃逸闭包,必须用 @escaping 标记,否则编译报错。这是Swift内存安全机制的一部分,防止非逃逸闭包被错误地存储或延迟调用。

7.3 弱引用管理避免循环引用

由于闭包会自动捕获上下文中的变量和 self ,若不加以控制,极易造成强引用循环(retain cycle)。推荐使用 [weak self][unowned self] 显式解引用:

swift 复制代码
pickerVC.onCitySelected = { [weak self] province, city, district in
    guard let strongSelf = self else { return }
    strongSelf.addressModel.fullAddress = "\(province ?? "")\(city ?? "")\(district ?? "")"
}
捕获方式 适用场景 风险
[weak self] 大多数情况,对象可能已释放 需要解包 optional
[unowned self] 确保生命周期长于闭包 可能访问已释放内存
[strong self] 明确需要延长生命周期 容易导致循环引用

7.4 闭包与代理模式对比分析

下表展示了闭包与代理在城市选择器场景下的关键差异:

特性 闭包(Closure) 代理(Delegate)
实现复杂度 简单,内联定义 需定义协议和方法
上下文捕获 自动捕获变量 需通过参数传递
多回调支持 多个闭包属性 单一代理对象
内存管理 易产生循环引用 解耦良好,不易泄漏
可读性 调用处逻辑集中 分离清晰,适合复杂交互
扩展性 不便于多监听者 支持多个观察者(需组合)

7.5 综合实践:构建支持闭包回调的城市选择器

以下是一个完整的 CitySelector 类,集成闭包通信机制:

swift 复制代码
class CitySelector: UIPickerViewDelegate, UIPickerViewDataSource {
    private let pickerView: UIPickerView
    private var provinces: [String] = []
    private var cities: [String] = []
    private var districts: [String] = []
    var onCitySelected: ((String, String, String)) -> Void = { _, _, _ in }
    var onCancel: () -> Void = {}
    init(pickerView: UIPickerView) {
        self.pickerView = pickerView
        loadData()
    }
    private func loadData() {
        provinces = ["北京市", "上海市", "广东省", "浙江省"]
        cities = ["广州市", "深圳市", "东莞市"] // 默认广东城市
        districts = ["天河区", "南山区", "福田区"]
    }
    // MARK: - UIPickerViewDataSource
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 3
    }
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        switch component {
        case 0: return provinces.count
        case 1: return cities.count
        case 2: return districts.count
        default: return 0
        }
    }
    // MARK: - UIPickerViewDelegate
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        switch component {
        case 0: return provinces[row]
        case 1: return cities[row]
        case 2: return districts[row]
        default: return nil
        }
    }
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        if component == 0 {
            // 切换省份时重置城市和区
            cities = simulatedCities(for: provinces[row])
            districts = simulatedDistricts(for: cities.first ?? "")
            pickerView.reloadComponent(1)
            pickerView.reloadComponent(2)
            pickerView.selectRow(0, inComponent: 1, animated: true)
            pickerView.selectRow(0, inComponent: 2, animated: true)
        } else if component == 1 {
            districts = simulatedDistricts(for: cities[row])
            pickerView.reloadComponent(2)
            pickerView.selectRow(0, inComponent: 2, animated: true)
        }
    }
    private func simulatedCities(for province: String) -> [String] {
        switch province {
        case "北京市": return ["北京市"]
        case "上海市": return ["上海市"]
        case "广东省": return ["广州市", "深圳市", "东莞市"]
        case "浙江省": return ["杭州市", "宁波市", "温州市"]
        default: return []
        }
    }
    private func simulatedDistricts(for city: String) -> [String] {
        switch city {
        case "广州市": return ["天河区", "越秀区", "海珠区"]
        case "深圳市": return ["南山区", "福田区", "宝安区"]
        case "杭州市": return ["西湖区", "上城区", "滨江区"]
        default: return ["中心区", "新区", "老城区"]
        }
    }
    // 提供确认接口,触发闭包回调
    func confirmSelection() {
        let selectedProvince = provinces[pickerView.selectedRow(inComponent: 0)]
        let selectedCity = cities[pickerView.selectedRow(inComponent: 1)]
        let selectedDistrict = districts[pickerView.selectedRow(inComponent: 2)]
        onCitySelected(selectedProvince, selectedCity, selectedDistrict)
    }
    func cancel() {
        onCancel()
    }
}

7.6 使用示例与UI集成

在视图控制器中集成该选择器:

swift 复制代码
class AddressEditViewController: UIViewController {
    @IBOutlet weak var addressButton: UIButton!
    private var citySelector: CitySelector!
    override func viewDidLoad() {
        super.viewDidLoad()
        let pickerView = UIPickerView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 200))
        citySelector = CitySelector(pickerView: pickerView)
        // 设置闭包回调
        citySelector.onCitySelected = { [weak self] province, city, district in
            self?.addressButton.setTitle("\(province)\(city)\(district)", for: .normal)
        }
        citySelector.onCancel = { [weak self] in
            self?.dismiss(animated: true)
        }
        // 添加工具栏和滚轮
        let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 44))
        let doneItem = UIBarButtonItem(barButtonSystemItem: .done, target: citySelector, action: #selector(citySelector.confirmSelection))
        let cancelItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: citySelector, action: #selector(citySelector.cancel))
        toolbar.items = [cancelItem, UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), doneItem]
        let container = UIStackView(arrangedSubviews: [toolbar, pickerView])
        container.axis = .vertical
        container.frame = CGRect(x: 0, y: view.frame.height - 244, width: view.frame.width, height: 244)
        view.addSubview(container)
    }
}

7.7 闭包在响应链中的扩展应用

借助函数式思想,还可以将闭包用于事件链式处理:

swift 复制代码
func chainHandlers() {
    let validator: (String?, String?, String?) -> Bool = { $0 != nil && !$0!.isEmpty }
    let logger = onCitySelected
    let uploader: CitySelectionHandler = { [weak self] p, c, d in
        self?.uploadToServer(p, c, d)
    }
    onCitySelected = { [weak self] p, c, d in
        guard validator(p, c, d) else { return }
        logger?(p, c, d)
        uploader(p, c, d)
    }
}

这种模式允许在不修改原有逻辑的前提下动态增强行为,体现高阶函数的灵活性。

graph TD A[用户选择省份] --> B{是否改变?} B -->|是| C[更新城市列表] C --> D[重置区县] D --> E[刷新UIPickerView] E --> F[等待用户选择城市] F --> G{是否改变?} G -->|是| H[更新区县列表] H --> I[刷新第三列] I --> J[用户点击确认] J --> K[触发onCitySelected闭包] K --> L[执行业务逻辑]

本文还有配套的精品资源,点击获取

简介:在iOS开发中,城市三级选择器广泛应用于需要精确地理位置的场景,如导航、外卖和求职类应用。本项目"CityChooseDemo"提供了一个完整的城市三级联动实现方案,涵盖Objective-C与Swift两种语言版本,帮助开发者快速集成省、市、区县级联选择功能。项目示例包括UITableView与UIPickerView的灵活运用、动态数据绑定、跨层级联动逻辑处理,并集成了网络请求获取实时城市数据、JSON解析、本地缓存与内存优化等核心功能。通过本项目实践,开发者可掌握高效构建流畅、响应式城市选择界面的关键技术,提升实际开发效率与用户体验。

本文还有配套的精品资源,点击获取