第 10 章:协议化编程在 iOS 开发中的应用
10.3 实战案例:用协议重构一个复杂的视图控制器
在 iOS 开发中,视图控制器(View Controller)往往因为承担过多职责而变得臃肿不堪。这种“Massive View Controller”问题会导致代码难以维护和测试。本节通过一个实战案例,展示如何利用协议化编程(Protocol-Oriented Programming, POP)重构一个复杂的视图控制器,将其职责分解为清晰、可复用的模块。
案例背景:一个复杂的用户详情视图控制器
假设我们有一个 UserDetailViewController,负责显示用户详情,包括头像、用户名、简介,以及一个“关注”按钮的功能。初始实现如下:
class UserDetailViewController: UIViewController {
private let userId: String
private let avatarImageView = UIImageView()
private let nameLabel = UILabel()
private let bioLabel = UILabel()
private let followButton = UIButton()
init(userId: String) {
self.userId = userId
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
fetchUserData()
}
private func setupUI() {
view.backgroundColor = .white
// 配置 UI 布局(省略具体代码)
view.addSubview(avatarImageView)
view.addSubview(nameLabel)
view.addSubview(bioLabel)
view.addSubview(followButton)
followButton.addTarget(self, action: #selector(followButtonTapped), for: .touchUpInside)
}
private func fetchUserData() {
// 模拟网络请求
DispatchQueue.global().async {
let userData = self.mockFetchUser(userId: self.userId)
DispatchQueue.main.async {
self.avatarImageView.image = userData.avatar
self.nameLabel.text = userData.name
self.bioLabel.text = userData.bio
self.followButton.setTitle(userData.isFollowing ? "取消关注" : "关注", for: .normal)
}
}
}
private func mockFetchUser(userId: String) -> (avatar: UIImage, name: String, bio: String, isFollowing: Bool) {
// 模拟数据
return (UIImage(named: "avatar")!, "Alice", "iOS Developer", false)
}
@objc private func followButtonTapped() {
// 模拟关注/取消关注逻辑
print("Follow button tapped")
}
}
这个实现存在以下问题:
- 职责混杂:视图控制器同时负责 UI 配置、数据获取和用户交互逻辑。
- 耦合严重:数据获取和 UI 更新逻辑直接嵌入视图控制器,难以复用或测试。
- 扩展性差:如果需要支持不同的数据源或 UI 样式,必须修改视图控制器内部代码。
重构目标
我们将使用协议化编程,将职责分解为以下模块:
- 数据提供者(Data Provider):负责获取用户数据。
- 视图模型(View Model):处理数据并提供 UI 友好的格式。
- 视图更新协议(View Updating):定义 UI 更新行为。
重构步骤
1. 定义协议
首先,定义必要的协议来抽象职责:
// 数据提供协议
protocol UserDataProviding {
func fetchUserData(userId: String, completion: @escaping (UserData) -> Void)
}
// 视图模型输出协议
protocol UserDetailViewModelOutput {
var avatar: UIImage { get }
var name: String { get }
var bio: String { get }
var followButtonTitle: String { get }
func toggleFollow()
}
// 视图更新协议
protocol UserDetailViewUpdating {
func updateUserDetails(avatar: UIImage, name: String, bio: String, followButtonTitle: String)
}
2. 实现数据提供者
将数据获取逻辑抽离到独立模块:
struct UserDataProvider: UserDataProviding {
func fetchUserData(userId: String, completion: @escaping (UserData) -> Void) {
DispatchQueue.global().async {
// 模拟网络请求
let userData = UserData(
avatar: UIImage(named: "avatar")!,
name: "Alice",
bio: "iOS Developer",
isFollowing: false
)
DispatchQueue.main.async {
completion(userData)
}
}
}
}
// 用户数据模型
struct UserData {
let avatar: UIImage
let name: String
let bio: String
let isFollowing: Bool
}
3. 实现视图模型
视图模型负责协调数据并提供 UI 所需的格式:
class UserDetailViewModel: UserDetailViewModelOutput {
private let dataProvider: UserDataProviding
private var userData: UserData?
var avatar: UIImage { userData?.avatar ?? UIImage() }
var name: String { userData?.name ?? "" }
var bio: String { userData?.bio ?? "" }
var followButtonTitle: String { userData?.isFollowing == true ? "取消关注" : "关注" }
init(userId: String, dataProvider: UserDataProviding) {
self.dataProvider = dataProvider
loadUserData(userId: userId)
}
private func loadUserData(userId: String) {
dataProvider.fetchUserData(userId: userId) { [weak self] userData in
self?.userData = userData
}
}
func toggleFollow() {
guard let data = userData else { return }
userData = UserData(
avatar: data.avatar,
name: data.name,
bio: data.bio,
isFollowing: !data.isFollowing
)
}
}
4. 重构视图控制器
视图控制器只负责 UI 配置和协调:
class UserDetailViewController: UIViewController, UserDetailViewUpdating {
private let viewModel: UserDetailViewModelOutput
private let avatarImageView = UIImageView()
private let nameLabel = UILabel()
private let bioLabel = UILabel()
private let followButton = UIButton()
init(viewModel: UserDetailViewModelOutput) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
updateUserDetails(
avatar: viewModel.avatar,
name: viewModel.name,
bio: viewModel.bio,
followButtonTitle: viewModel.followButtonTitle
)
}
private func setupUI() {
view.backgroundColor = .white
// 配置 UI 布局(省略具体代码)
view.addSubview(avatarImageView)
view.addSubview(nameLabel)
view.addSubview(bioLabel)
view.addSubview(followButton)
followButton.addTarget(self, action: #selector(followButtonTapped), for: .touchUpInside)
}
func updateUserDetails(avatar: UIImage, name: String, bio: String, followButtonTitle: String) {
avatarImageView.image = avatar
nameLabel.text = name
bioLabel.text = bio
followButton.setTitle(followButtonTitle, for: .normal)
}
@objc private func followButtonTapped() {
viewModel.toggleFollow()
updateUserDetails(
avatar: viewModel.avatar,
name: viewModel.name,
bio: viewModel.bio,
followButtonTitle: viewModel.followButtonTitle
)
}
}
// 使用示例
let dataProvider = UserDataProvider()
let viewModel = UserDetailViewModel(userId: "123", dataProvider: dataProvider)
let viewController = UserDetailViewController(viewModel: viewModel)
重构后的优势
- 职责分离:
- 数据获取由
UserDataProvider负责。 - 数据处理和状态管理由
UserDetailViewModel负责。 - UI 更新和用户交互由
UserDetailViewController负责。
- 数据获取由
- 松耦合:视图控制器通过协议依赖
UserDetailViewModelOutput,无需关心具体实现。 - 可测试性:可以轻松 mock
UserDataProviding和UserDetailViewModelOutput进行单元测试。 - 可扩展性:需要替换数据源或添加新功能时,只需提供新的协议实现。
小结
通过协议化编程,我们成功将一个复杂的视图控制器重构为多个模块化的组件。每个模块职责清晰,代码更易于维护和扩展。这种方法在实际项目中尤其适用于大型应用,能够有效提升开发效率和代码质量。
