OC 和 Swift 开发中的 UI 数据绑定:MVC 与 MVVM 模式详解
1. 基本概念介绍
1.1 UIKit 与 SwiftUI 对比
UIKit (Objective-C/Swift):
- 命令式 UI 框架
- 基于视图控制器 (UIViewController) 的中心架构
- 需要手动管理视图层次结构和状态
- 使用委托模式和数据源模式
- 成熟的生态系统,支持 iOS 8+
SwiftUI (仅 Swift):
- 声明式 UI 框架
- 基于值类型的视图结构
- 自动状态管理和视图更新
- 内置数据绑定支持
- 需要 iOS 13+,功能仍在不断丰富中
1.2 MVC 与 MVVM 模式
MVC (Model-View-Controller):
- 传统苹果推荐模式
- Controller 作为 Model 和 View 的中介
- 容易导致 Massive View Controller 问题
- 数据流向: View → Controller → Model → Controller → View
MVVM (Model-View-ViewModel):
- 更现代的架构模式
- ViewModel 封装展示逻辑和状态
- View 通过绑定自动更新
- 更好的可测试性和关注点分离
- 数据流向: View ↔ ViewModel ↔ Model
2. UIKit 中的数据绑定 (Objective-C)
2.1 MVC 模式实现
网络请求与数据处理
objectivec
// UserModel.h
@interface UserModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *avatarUrl;
- (instancetype)initWithDictionary:(NSDictionary *)dict;
@end
// UserModel.m
@implementation UserModel
- (instancetype)initWithDictionary:(NSDictionary *)dict {
self = [super init];
if (self) {
_name = dict[@"name"];
_age = [dict[@"age"] integerValue];
_avatarUrl = dict[@"avatar"];
}
return self;
}
@end
控制器实现
objectivec
// UserViewController.h
#import <UIKit/UIKit.h>
@interface UserViewController : UIViewController
@property (weak, nonatomic) IBOutlet UILabel *nameLabel;
@property (weak, nonatomic) IBOutlet UILabel *ageLabel;
@property (weak, nonatomic) IBOutlet UIImageView *avatarImageView;
@end
// UserViewController.m
#import "UserViewController.h"
#import "UserModel.h"
@interface UserViewController ()
@property (nonatomic, strong) UserModel *user;
@property (nonatomic, strong) NSURLSessionDataTask *dataTask;
@end
@implementation UserViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self fetchUserData];
}
- (void)fetchUserData {
NSURL *url = [NSURL URLWithString:@"https://api.example.com/user/123"];
self.dataTask = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (data) {
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
self.user = [[UserModel alloc] initWithDictionary:json];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateUI];
});
}
}];
[self.dataTask resume];
}
- (void)updateUI {
self.nameLabel.text = self.user.name;
self.ageLabel.text = [NSString stringWithFormat:@"%ld", (long)self.user.age];
if (self.user.avatarUrl) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:self.user.avatarUrl]];
UIImage *image = [UIImage imageWithData:imageData];
dispatch_async(dispatch_get_main_queue(), ^{
self.avatarImageView.image = image;
});
});
}
}
- (void)dealloc {
[self.dataTask cancel];
}
@end
2.2 MVVM 模式实现 (使用 ReactiveObjC)
ViewModel 实现
objectivec
// UserViewModel.h
#import <ReactiveObjC/ReactiveObjC.h>
@class UserModel;
@interface UserViewModel : NSObject
@property (nonatomic, strong, readonly) UserModel *user;
@property (nonatomic, strong, readonly) RACCommand *fetchUserCommand;
@property (nonatomic, strong, readonly) RACSignal *nameSignal;
@property (nonatomic, strong, readonly) RACSignal *ageSignal;
@property (nonatomic, strong, readonly) RACSignal *avatarSignal;
@end
// UserViewModel.m
#import "UserViewModel.h"
#import "UserModel.h"
@interface UserViewModel ()
@property (nonatomic, strong) UserModel *user;
@end
@implementation UserViewModel
- (instancetype)init {
self = [super init];
if (self) {
[self setup];
}
return self;
}
- (void)setup {
@weakify(self);
self.fetchUserCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
@strongify(self);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { NSURL *url = [NSURL URLWithString:@"https://api.example.com/user/123"];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (data) {
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
self.user = [[UserModel alloc] initWithDictionary:json];
[subscriber sendNext:self.user];
[subscriber sendCompleted];
} else {
[subscriber sendError:error];
}
}];
[task resume];
return [RACDisposable disposableWithBlock:^{ [task cancel];
}];
}];
}];
self.nameSignal = RACObserve(self, user.name);
self.ageSignal = [RACObserve(self, user.age) map:^id(NSNumber *age) { return [NSString stringWithFormat:@"%ld", (long)[age integerValue]];
}];
self.avatarSignal = [RACObserve(self, user.avatarUrl) flattenMap:^RACSignal *(NSString *urlString) { if (!urlString) return [RACSignal return:nil];
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { NSURL *url = [NSURL URLWithString:urlString];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (data) {
UIImage *image = [UIImage imageWithData:data];
[subscriber sendNext:image];
[subscriber sendCompleted];
} else {
[subscriber sendError:error];
}
}];
[task resume];
return [RACDisposable disposableWithBlock:^{ [task cancel];
}];
}];
}];
}
@end
控制器实现
objectivec
// UserViewController.m (MVVM版本)
#import "UserViewController.h"
#import "UserViewModel.h"
#import <ReactiveObjC/ReactiveObjC.h>
@interface UserViewController ()
@property (nonatomic, strong) UserViewModel *viewModel;
@end
@implementation UserViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.viewModel = [[UserViewModel alloc] init];
// 绑定数据
[self bindViewModel];
// 执行获取数据的命令
[self.viewModel.fetchUserCommand execute:nil];
}
- (void)bindViewModel {
@weakify(self);
// 绑定名字
[self.viewModel.nameSignal subscribeNext:^(NSString *name) { @strongify(self); self.nameLabel.text = name; }];
// 绑定年龄
[self.viewModel.ageSignal subscribeNext:^(NSString *age) { @strongify(self); self.ageLabel.text = age; }];
// 绑定头像
[self.viewModel.avatarSignal subscribeNext:^(UIImage *image) { @strongify(self); self.avatarImageView.image = image; }];
// 处理错误
[self.viewModel.fetchUserCommand.errors subscribeNext:^(NSError *error) { NSLog(@"Error fetching user: %@", error.localizedDescription); }];
}
@end
3. Swift + UIKit 中的数据绑定
3.1 MVC 模式实现
swift
// UserModel.swift
struct User {
let name: String
let age: Int
let avatarUrl: String
}
// UserViewController.swift
import UIKit
class UserViewController: UIViewController {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var ageLabel: UILabel!
@IBOutlet weak var avatarImageView: UIImageView!
private var user: User?
private var dataTask: URLSessionDataTask?
override func viewDidLoad() {
super.viewDidLoad()
fetchUserData()
}
private func fetchUserData() {
let url = URL(string: "https://api.example.com/user/123")!
dataTask = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
if let json = json {
self.user = User(
name: json["name"] as? String ?? "",
age: json["age"] as? Int ?? 0,
avatarUrl: json["avatar"] as? String ?? ""
)
DispatchQueue.main.async {
self.updateUI()
}
}
} catch {
print("JSON error: (error.localizedDescription)")
}
}
}
dataTask?.resume()
}
private func updateUI() {
guard let user = user else { return }
nameLabel.text = user.name
ageLabel.text = "(user.age)"
if !user.avatarUrl.isEmpty {
DispatchQueue.global().async { [weak self] in
if let url = URL(string: user.avatarUrl),
let data = try? Data(contentsOf: url),
let image = UIImage(data: data) {
DispatchQueue.main.async {
self?.avatarImageView.image = image
}
}
}
}
}
deinit {
dataTask?.cancel()
}
}
3.2 MVVM 模式实现 (使用 Combine)
ViewModel 实现
swift
// UserViewModel.swift
import Combine
class UserViewModel {
@Published var name: String = ""
@Published var age: String = ""
@Published var avatarImage: UIImage?
@Published var isLoading: Bool = false
@Published var error: Error?
private var cancellables = Set<AnyCancellable>()
func fetchUser() {
isLoading = true
let url = URL(string: "https://api.example.com/user/123")!
URLSession.shared.dataTaskPublisher(for: url)
.map(.data)
.decode(type: UserResponse.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.error = error
}
} receiveValue: { [weak self] userResponse in
self?.updateUser(userResponse)
}
.store(in: &cancellables)
}
private func updateUser(_ user: UserResponse) {
name = user.name
age = "(user.age)"
guard let url = URL(string: user.avatarUrl) else { return }
URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.assign(to: .avatarImage, on: self)
.store(in: &cancellables)
}
}
struct UserResponse: Codable {
let name: String
let age: Int
let avatarUrl: String
}
控制器实现
swift
// UserViewController.swift (MVVM with Combine)
import UIKit
import Combine
class UserViewController: UIViewController {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var ageLabel: UILabel!
@IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
private var viewModel = UserViewModel()
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
viewModel.fetchUser()
}
private func bindViewModel() {
// 绑定名字
viewModel.$name
.receive(on: DispatchQueue.main)
.assign(to: .text, on: nameLabel)
.store(in: &cancellables)
// 绑定年龄
viewModel.$age
.receive(on: DispatchQueue.main)
.assign(to: .text, on: ageLabel)
.store(in: &cancellables)
// 绑定头像
viewModel.$avatarImage
.receive(on: DispatchQueue.main)
.sink { [weak self] image in
self?.avatarImageView.image = image
}
.store(in: &cancellables)
// 绑定加载状态
viewModel.$isLoading
.receive(on: DispatchQueue.main)
.sink { [weak self] isLoading in
if isLoading {
self?.activityIndicator.startAnimating()
} else {
self?.activityIndicator.stopAnimating()
}
}
.store(in: &cancellables)
// 处理错误
viewModel.$error
.compactMap { $0 }
.receive(on: DispatchQueue.main)
.sink { [weak self] error in
let alert = UIAlertController(
title: "Error",
message: error.localizedDescription,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
self?.present(alert, animated: true)
}
.store(in: &cancellables)
}
}
4. SwiftUI 中的数据绑定
4.1 原生 SwiftUI 实现 (类似 MVVM)
swift
// UserViewModel.swift
import Combine
class UserViewModel: ObservableObject {
@Published var user: User?
@Published var isLoading = false
@Published var error: Error?
private var cancellables = Set<AnyCancellable>()
func fetchUser() {
isLoading = true
let url = URL(string: "https://api.example.com/user/123")!
URLSession.shared.dataTaskPublisher(for: url)
.map(.data)
.decode(type: User.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.error = error
}
} receiveValue: { [weak self] user in
self?.user = user
}
.store(in: &cancellables)
}
}
struct User: Codable {
let name: String
let age: Int
let avatarUrl: String
}
// UserView.swift
import SwiftUI
struct UserView: View {
@StateObject private var viewModel = UserViewModel()
var body: some View {
VStack {
if viewModel.isLoading {
ProgressView()
} else if let user = viewModel.user {
AsyncImage(url: URL(string: user.avatarUrl)) { image in
image.resizable()
} placeholder: {
Color.gray
}
.frame(width: 100, height: 100)
.clipShape(Circle())
Text(user.name)
.font(.title)
Text("Age: (user.age)")
.font(.subheadline)
} else if let error = viewModel.error {
Text("Error: (error.localizedDescription)")
.foregroundColor(.red)
}
}
.onAppear {
viewModel.fetchUser()
}
}
}
4.2 使用第三方响应式框架 (RxSwift)
ViewModel 实现
swift
// UserViewModel.swift
import RxSwift
import RxCocoa
class UserViewModel {
// 输出
let name: Driver<String>
let age: Driver<String>
let avatarImage: Driver<UIImage?>
let isLoading: Driver<Bool>
let error: Driver<Error?>
// 输入
let fetchTrigger = PublishSubject<Void>()
private let disposeBag = DisposeBag()
init() {
let activityIndicator = ActivityIndicator()
let errorTracker = ErrorTracker()
let user = fetchTrigger
.flatMapLatest {
URLSession.shared.rx.data(request: URLRequest(url: URL(string: "https://api.example.com/user/123")!))
.map { try JSONDecoder().decode(User.self, from: $0) }
.trackActivity(activityIndicator)
.trackError(errorTracker)
.catchAndReturn(User(name: "", age: 0, avatarUrl: ""))
}
.asDriver(onErrorJustReturn: User(name: "", age: 0, avatarUrl: ""))
name = user.map { $0.name }.asDriver()
age = user.map { "($0.age)" }.asDriver()
avatarImage = user.flatMapLatest { user in
guard !user.avatarUrl.isEmpty, let url = URL(string: user.avatarUrl) else {
return Driver.just(nil)
}
return URLSession.shared.rx.data(request: URLRequest(url: url))
.map { UIImage(data: $0) }
.trackActivity(activityIndicator)
.trackError(errorTracker)
.asDriver(onErrorJustReturn: nil)
}
isLoading = activityIndicator.asDriver()
error = errorTracker.asDriver()
}
}
SwiftUI View 实现
swift
// UserView.swift (with RxSwift)
import SwiftUI
import RxSwift
import RxCocoa
struct RxUserView: View {
@ObservedObject private var viewModel = RxUserViewModel()
private let disposeBag = DisposeBag()
var body: some View {
VStack {
if viewModel.isLoading {
ProgressView()
} else {
if let image = viewModel.avatarImage {
Image(uiImage: image)
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
} else {
Color.gray
.frame(width: 100, height: 100)
.clipShape(Circle())
}
Text(viewModel.name)
.font(.title)
Text("Age: (viewModel.age)")
.font(.subheadline)
if let error = viewModel.error {
Text("Error: (error.localizedDescription)")
.foregroundColor(.red)
}
}
}
.onAppear {
viewModel.fetchUser()
}
}
}
// 适配器将RxSwift的Driver转换为SwiftUI可用的@Published属性
class RxUserViewModel: ObservableObject {
private let rxViewModel = UserViewModel()
private let disposeBag = DisposeBag()
@Published var name: String = ""
@Published var age: String = ""
@Published var avatarImage: UIImage?
@Published var isLoading: Bool = false
@Published var error: Error?
func fetchUser() {
rxViewModel.fetchTrigger.onNext(())
rxViewModel.name
.drive(onNext: { [weak self] in self?.name = $0 })
.disposed(by: disposeBag)
rxViewModel.age
.drive(onNext: { [weak self] in self?.age = $0 })
.disposed(by: disposeBag)
rxViewModel.avatarImage
.drive(onNext: { [weak self] in self?.avatarImage = $0 })
.disposed(by: disposeBag)
rxViewModel.isLoading
.drive(onNext: { [weak self] in self?.isLoading = $0 })
.disposed(by: disposeBag)
rxViewModel.error
.drive(onNext: { [weak self] in self?.error = $0 })
.disposed(by: disposeBag)
}
}
5. 实时数据更新实现
5.1 使用 WebSocket 实时更新 (Combine + SwiftUI)
swift
// RealTimeUserViewModel.swift
import Combine
import Foundation
class RealTimeUserViewModel: ObservableObject {
@Published var user: User?
@Published var isLoading = false
@Published var error: Error?
private var webSocketTask: URLSessionWebSocketTask?
private var cancellables = Set<AnyCancellable>()
func connect() {
isLoading = true
let url = URL(string: "wss://api.example.com/realtime/user/123")!
webSocketTask = URLSession.shared.webSocketTask(with: url)
webSocketTask?.resume()
listenForMessages()
}
private func listenForMessages() {
webSocketTask?.receive { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
DispatchQueue.main.async {
self.error = error
self.isLoading = false
}
case .success(let message):
switch message {
case .string(let text):
if let data = text.data(using: .utf8),
let user = try? JSONDecoder().decode(User.self, from: data) {
DispatchQueue.main.async {
self.user = user
self.isLoading = false
}
}
case .data(let data):
if let user = try? JSONDecoder().decode(User.self, from: data) {
DispatchQueue.main.async {
self.user = user
self.isLoading = false
}
}
@unknown default:
break
}
// 继续监听下一条消息
self.listenForMessages()
}
}
}
func disconnect() {
webSocketTask?.cancel(with: .normalClosure, reason: nil)
isLoading = false
}
deinit {
disconnect()
}
}
// RealTimeUserView.swift
import SwiftUI
struct RealTimeUserView: View {
@StateObject private var viewModel = RealTimeUserViewModel()
var body: some View {
VStack {
if viewModel.isLoading {
ProgressView()
} else if let user = viewModel.user {
AsyncImage(url: URL(string: user.avatarUrl)) { image in
image.resizable()
} placeholder: {
Color.gray
}
.frame(width: 100, height: 100)
.clipShape(Circle())
Text(user.name)
.font(.title)
Text("Age: (user.age)")
.font(.subheadline)
} else if let error = viewModel.error {
Text("Error: (error.localizedDescription)")
.foregroundColor(.red)
}
}
.onAppear {
viewModel.connect()
}
.onDisappear {
viewModel.disconnect()
}
}
}
5.2 使用 Firebase 实时数据库 (MVVM)
swift
// FirebaseUserViewModel.swift
import Combine
import FirebaseDatabase
class FirebaseUserViewModel: ObservableObject {
@Published var user: User?
@Published var isLoading = false
@Published var error: Error?
private var ref: DatabaseReference!
private var handle: DatabaseHandle?
init() {
ref = Database.database().reference()
}
func startListening() {
isLoading = true
handle = ref.child("users").child("123").observe(.value) { [weak self] snapshot in
guard let self = self else { return }
self.isLoading = false
guard let value = snapshot.value as? [String: Any] else {
self.error = NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid data format"])
return
}
do {
let data = try JSONSerialization.data(withJSONObject: value)
self.user = try JSONDecoder().decode(User.self, from: data)
self.error = nil
} catch {
self.error = error
}
} withCancel: { [weak self] error in
self?.error = error
self?.isLoading = false
}
}
func stopListening() {
if let handle = handle {
ref.child("users").child("123").removeObserver(withHandle: handle)
}
isLoading = false
}
deinit {
stopListening()
}
}
// FirebaseUserView.swift
import SwiftUI
struct FirebaseUserView: View {
@StateObject private var viewModel = FirebaseUserViewModel()
var body: some View {
VStack {
if viewModel.isLoading {
ProgressView()
} else if let user = viewModel.user {
AsyncImage(url: URL(string: user.avatarUrl)) { image in
image.resizable()
} placeholder: {
Color.gray
}
.frame(width: 100, height: 100)
.clipShape(Circle())
Text(user.name)
.font(.title)
Text("Age: (user.age)")
.font(.subheadline)
Button("Update Age") {
if let currentAge = viewModel.user?.age {
viewModel.ref.child("users").child("123").updateChildValues(["age": currentAge + 1])
}
}
.padding()
} else if let error = viewModel.error {
Text("Error: (error.localizedDescription)")
.foregroundColor(.red)
}
}
.onAppear {
viewModel.startListening()
}
.onDisappear {
viewModel.stopListening()
}
}
}
6. 总结与最佳实践
6.1 模式选择建议
-
对于新项目:
- 优先考虑 SwiftUI + Combine + MVVM
- 如果需要支持 iOS 13 以下,使用 UIKit + Combine/MVVM
-
对于现有项目:
- 逐步迁移到 MVVM 模式
- 引入 Combine 或 RxSwift 处理数据流
- 新功能使用 SwiftUI 实现
-
实时数据需求:
- WebSocket 适合自定义实时协议
- Firebase 提供完整的实时数据库解决方案
- Combine 的
PassthroughSubject或 RxSwift 的PublishSubject适合应用内实时事件
6.2 性能考虑
-
UIKit:
- 避免频繁的主线程更新
- 使用 diffable data sources 处理表格/集合视图更新
-
SwiftUI:
- 使用
@StateObject替代@ObservedObject避免不必要的重建 - 对于复杂视图,考虑使用
EquatableView或自定义视图的equatable()实现
- 使用
-
网络请求:
- 使用 Combine 的
share()或 RxSwift 的share(replay:scope:)避免重复请求 - 实现适当的缓存策略
- 使用 Combine 的
6.3 测试策略
-
ViewModel 测试:
- 测试业务逻辑和状态转换
- 模拟网络请求和依赖项
-
UI 测试:
- 对于 SwiftUI,使用 XCTest 检查视图状态
- 对于 UIKit,测试视图控制器的输出和行为
-
集成测试:
- 测试整个功能流程
- 包括网络层和持久层的交互
通过以上模式和技术的合理应用,可以构建出响应迅速、易于维护且可测试性高的 iOS 应用程序。
Swift + UIKit 中使用 RxSwift 实现数据绑定与 UI 实时驱动
1. RxSwift 基础概念
在开始具体实现前,先了解几个 RxSwift 核心概念:
- Observable: 可观察序列,表示数据流
- Observer: 观察者,订阅 Observable 并响应事件
- Subject: 既是 Observable 又是 Observer
- Operator: 操作符,用于转换、过滤、组合序列
- Disposable: 管理订阅的生命周期
- Scheduler: 决定在哪个线程执行操作
2. 项目设置
首先需要在项目中集成 RxSwift 和 RxCocoa:
- 使用 CocoaPods 添加依赖:
ruby
pod 'RxSwift'
pod 'RxCocoa'
- 在需要使用的文件中导入:
swift
import RxSwift
import RxCocoa
3. 基础数据绑定示例
3.1 简单的文本绑定
swift
class SimpleBindingViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var label: UILabel!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 将 textField 的文本变化绑定到 label
textField.rx.text
.orEmpty // 将 String? 转换为 String
.bind(to: label.rx.text)
.disposed(by: disposeBag)
}
}
3.2 按钮点击事件
swift
class ButtonClickViewController: UIViewController {
@IBOutlet weak var button: UIButton!
@IBOutlet weak var counterLabel: UILabel!
private let disposeBag = DisposeBag()
private let counter = BehaviorSubject(value: 0)
override func viewDidLoad() {
super.viewDidLoad()
// 按钮点击时增加计数器
button.rx.tap
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
let currentValue = (try? self.counter.value()) ?? 0
self.counter.onNext(currentValue + 1)
})
.disposed(by: disposeBag)
// 将计数器绑定到 label
counter
.map { "Clicked ($0) times" }
.bind(to: counterLabel.rx.text)
.disposed(by: disposeBag)
}
}
4. 完整 MVVM 示例:用户信息展示
4.1 Model 层
swift
struct User {
let id: Int
let name: String
let email: String
let avatarUrl: String
}
extension User: Decodable {
enum CodingKeys: String, CodingKey {
case id, name, email
case avatarUrl = "avatar_url"
}
}
4.2 ViewModel 层
swift
class UserViewModel {
// 输入
let fetchTrigger = PublishSubject<Void>()
// 输出
let name: Driver<String>
let email: Driver<String>
let avatarImage: Driver<UIImage?>
let isLoading: Driver<Bool>
let error: Driver<Error?>
private let disposeBag = DisposeBag()
init() {
// ActivityIndicator 用于跟踪加载状态
let activityIndicator = ActivityIndicator()
let errorTracker = ErrorTracker()
// 网络请求获取用户数据
let user = fetchTrigger
.flatMapLatest {
URLSession.shared.rx.data(request: URLRequest(url: URL(string: "https://api.example.com/users/1")!))
.map { try JSONDecoder().decode(User.self, from: $0) }
.trackActivity(activityIndicator)
.trackError(errorTracker)
.catchAndReturn(User(id: 0, name: "Error", email: "", avatarUrl: ""))
}
.asDriver(onErrorJustReturn: User(id: 0, name: "Error", email: "", avatarUrl: ""))
// 处理输出
name = user.map { $0.name }.asDriver()
email = user.map { $0.email }.asDriver()
avatarImage = user.flatMapLatest { user in
guard !user.avatarUrl.isEmpty, let url = URL(string: user.avatarUrl) else {
return Driver.just(nil)
}
return URLSession.shared.rx.data(request: URLRequest(url: url))
.map { UIImage(data: $0) }
.trackActivity(activityIndicator)
.trackError(errorTracker)
.asDriver(onErrorJustReturn: nil)
}
isLoading = activityIndicator.asDriver()
error = errorTracker.asDriver()
}
}
4.3 View 层 (ViewController)
swift
class UserViewController: UIViewController {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var emailLabel: UILabel!
@IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var refreshButton: UIButton!
private let viewModel = UserViewModel()
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
setupBindings()
}
private func setupBindings() {
// 输入绑定
refreshButton.rx.tap
.bind(to: viewModel.fetchTrigger)
.disposed(by: disposeBag)
// 输出绑定
viewModel.name
.drive(nameLabel.rx.text)
.disposed(by: disposeBag)
viewModel.email
.drive(emailLabel.rx.text)
.disposed(by: disposeBag)
viewModel.avatarImage
.drive(avatarImageView.rx.image)
.disposed(by: disposeBag)
viewModel.isLoading
.drive(onNext: { [weak self] isLoading in
self?.activityIndicator.isHidden = !isLoading
isLoading ? self?.activityIndicator.startAnimating() : self?.activityIndicator.stopAnimating()
})
.disposed(by: disposeBag)
viewModel.error
.drive(onNext: { [weak self] error in
guard let error = error else { return }
self?.showErrorAlert(error: error)
})
.disposed(by: disposeBag)
// 初始加载
viewModel.fetchTrigger.onNext(())
}
private func showErrorAlert(error: Error) {
let alert = UIAlertController(
title: "Error",
message: error.localizedDescription,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
5. 表格视图 (UITableView) 数据绑定
5.1 简单列表绑定
swift
class SimpleListViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
private let disposeBag = DisposeBag()
private let items = Observable.just(["Item 1", "Item 2", "Item 3", "Item 4"])
override func viewDidLoad() {
super.viewDidLoad()
// 注册 cell
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
// 绑定数据到表格
items
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, element, cell) in
cell.textLabel?.text = element
}
.disposed(by: disposeBag)
// 处理 cell 选择事件
tableView.rx.modelSelected(String.self)
.subscribe(onNext: { item in
print("Selected: (item)")
})
.disposed(by: disposeBag)
}
}
5.2 复杂列表绑定 (MVVM)
ViewModel
swift
struct Product {
let id: Int
let name: String
let price: Double
}
class ProductListViewModel {
let products: Driver<[Product]>
let isLoading: Driver<Bool>
let error: Driver<Error?>
private let fetchTrigger = PublishSubject<Void>()
init() {
let activityIndicator = ActivityIndicator()
let errorTracker = ErrorTracker()
products = fetchTrigger
.flatMapLatest {
// 模拟网络请求
Observable<[Product]>.create { observer in
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
let products = [ Product(id: 1, name: "iPhone", price: 999), Product(id: 2, name: "MacBook", price: 1999), Product(id: 3, name: "iPad", price: 799) ]
observer.onNext(products)
observer.onCompleted()
}
return Disposables.create()
}
.trackActivity(activityIndicator)
.trackError(errorTracker)
.asDriver(onErrorJustReturn: [])
}
.asDriver(onErrorJustReturn: [])
isLoading = activityIndicator.asDriver()
error = errorTracker.asDriver()
// 初始加载
fetchTrigger.onNext(())
}
func refresh() {
fetchTrigger.onNext(())
}
}
ViewController
swift
class ProductListViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var refreshButton: UIButton!
private let viewModel = ProductListViewModel()
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
setupBindings()
}
private func setupTableView() {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
private func setupBindings() {
// 绑定产品列表到表格
viewModel.products
.drive(tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, product, cell) in
cell.textLabel?.text = product.name
cell.detailTextLabel?.text = "$(product.price)"
}
.disposed(by: disposeBag)
// 绑定加载状态
viewModel.isLoading
.drive(onNext: { [weak self] isLoading in
self?.activityIndicator.isHidden = !isLoading
isLoading ? self?.activityIndicator.startAnimating() : self?.activityIndicator.stopAnimating()
})
.disposed(by: disposeBag)
// 绑定错误
viewModel.error
.drive(onNext: { [weak self] error in
guard let error = error else { return }
self?.showErrorAlert(error: error)
})
.disposed(by: disposeBag)
// 刷新按钮
refreshButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.viewModel.refresh()
})
.disposed(by: disposeBag)
// 处理选择事件
tableView.rx.modelSelected(Product.self)
.subscribe(onNext: { product in
print("Selected product: (product.name)")
})
.disposed(by: disposeBag)
}
private func showErrorAlert(error: Error) {
let alert = UIAlertController(
title: "Error",
message: error.localizedDescription,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
6. 表单验证示例
swift
class FormValidationViewController: UIViewController {
@IBOutlet weak var usernameTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var loginButton: UIButton!
@IBOutlet weak var usernameErrorLabel: UILabel!
@IBOutlet weak var passwordErrorLabel: UILabel!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
setupValidation()
}
private func setupValidation() {
// 用户名验证 (至少3个字符)
let usernameValid = usernameTextField.rx.text.orEmpty
.map { $0.count >= 3 }
.share(replay: 1)
// 密码验证 (至少6个字符)
let passwordValid = passwordTextField.rx.text.orEmpty
.map { $0.count >= 6 }
.share(replay: 1)
// 用户名错误提示
usernameValid
.skip(1) // 跳过初始空值
.map { $0 ? "" : "Username must be at least 3 characters" }
.bind(to: usernameErrorLabel.rx.text)
.disposed(by: disposeBag)
// 密码错误提示
passwordValid
.skip(1)
.map { $0 ? "" : "Password must be at least 6 characters" }
.bind(to: passwordErrorLabel.rx.text)
.disposed(by: disposeBag)
// 按钮启用状态
Observable.combineLatest(usernameValid, passwordValid) { $0 && $1 }
.bind(to: loginButton.rx.isEnabled)
.disposed(by: disposeBag)
// 按钮点击事件
loginButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.login()
})
.disposed(by: disposeBag)
}
private func login() {
guard let username = usernameTextField.text,
let password = passwordTextField.text else { return }
print("Attempting to login with username: (username), password: (password)")
// 实际登录逻辑...
}
}
7. 实时搜索示例
swift
class SearchViewController: UIViewController {
@IBOutlet weak var searchBar: UISearchBar!
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
private let disposeBag = DisposeBag()
private let viewModel = SearchViewModel()
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
setupBindings()
}
private func setupTableView() {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
private func setupBindings() {
// 将搜索框文本变化绑定到 ViewModel
searchBar.rx.text.orEmpty
.bind(to: viewModel.searchQuery)
.disposed(by: disposeBag)
// 绑定搜索结果到表格
viewModel.searchResults
.drive(tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, result, cell) in
cell.textLabel?.text = result
}
.disposed(by: disposeBag)
// 绑定加载状态
viewModel.isLoading
.drive(activityIndicator.rx.isAnimating)
.disposed(by: disposeBag)
// 处理选择事件
tableView.rx.modelSelected(String.self)
.subscribe(onNext: { [weak self] item in
self?.showDetail(for: item)
})
.disposed(by: disposeBag)
}
private func showDetail(for item: String) {
let alert = UIAlertController(title: "Selected", message: item, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
class SearchViewModel {
let searchQuery = BehaviorSubject(value: "")
let searchResults: Driver<[String]>
let isLoading: Driver<Bool>
private let disposeBag = DisposeBag()
init() {
let activityIndicator = ActivityIndicator()
// 模拟搜索 API 调用
searchResults = searchQuery
.debounce(.milliseconds(300), scheduler: MainScheduler.instance) // 防抖
.distinctUntilChanged() // 忽略相同值
.flatMapLatest { query -> Observable<[String]> in
if query.isEmpty {
return .just([])
}
// 模拟网络请求
return Observable<[String]>.create { observer in
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
let results = (1...5).map { "(query) result ($0)" }
observer.onNext(results)
observer.onCompleted()
}
return Disposables.create()
}
.trackActivity(activityIndicator)
}
.asDriver(onErrorJustReturn: [])
isLoading = activityIndicator.asDriver()
}
}
8. 最佳实践与注意事项
-
内存管理:
- 始终使用
disposed(by: disposeBag)管理订阅 - 在适当的时候使用
[weak self]避免循环引用 - 对于长时间运行的订阅考虑使用
takeUntil操作符
- 始终使用
-
线程管理:
- 使用
observeOn和subscribeOn控制线程 - UI 更新必须在主线程执行
- 使用
-
性能优化:
- 对于频繁变化的事件使用
debounce或throttle - 使用
share(replay:scope:)避免重复计算 - 对于表格/集合视图使用
distinctUntilChanged减少不必要的刷新
- 对于频繁变化的事件使用
-
错误处理:
- 使用
materialize()将错误转换为事件 - 使用
retry操作符处理可重试的错误 - 通过
ErrorTracker集中处理错误
- 使用
-
测试:
- 使用
RxTest和RxBlocking进行单元测试 - 测试 Observable 的输入输出关系
- 模拟时间相关的操作
- 使用
通过 RxSwift,我们可以以声明式的方式构建响应式 UI,实现数据与界面的自动同步,大大简化了 UIKit 应用的状态管理和数据绑定工作。