当前位置: 首页 > news >正文

实用指南:iOS Swift MVVM + RxSwift Generic Rules

You are an expert iOS Swift developer specializing in MVVM architecture with RxSwift. Write clean, maintainable, and testable code following Apple’s latest guidelines and Swift best practices.

Core Stack

  • Language: Swift 5.8+
  • UI Framework: UIKit
  • Architecture: MVVM (Model-View-ViewModel)
  • Reactive Framework: RxSwift + RxCocoa
  • Minimum Deployment: iOS 13.0+

Generic Project Structure

App/
├── Models/
│   ├── User.swift
│   ├── APIResponse.swift
│   └── CoreDataModels/
├── ViewModels/
│   ├── HomeViewModel.swift
│   ├── ProfileViewModel.swift
│   └── BaseViewModel.swift
├── Views/
│   ├── ViewControllers/
│   ├── CustomViews/
│   └── Cells/
├── Services/
│   ├── NetworkService.swift
│   ├── AuthService.swift
│   └── DataService.swift
├── Repositories/
│   └── UserRepository.swift
├── Extensions/
│   ├── UIView+Rx.swift
│   └── Observable+Extensions.swift
├── Utilities/
│   ├── Constants.swift
│   └── Helpers/
└── Resources/

MVVM Implementation Patterns

1. Model Layer

Keep models simple and focused on data representation.

struct User: Codable, Equatable {
let id: Int
let name: String
let email: String
let avatar: URL?
}
struct APIResponse<T: Codable>: Codable {let data: Tlet success: Boollet message: String?}enum LoadingState {case idlecase loadingcase loadedcase error(Error)}

2. ViewModel Pattern

Use Input/Output pattern for clear separation of concerns.

protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
final class UserListViewModel: ViewModelType {
private let userRepository: UserRepositoryProtocol
private let disposeBag = DisposeBag()
struct Input {
let viewDidLoad: Observable<Void>let refresh: Observable<Void>let selection: Observable<IndexPath>}struct Output {let users: Driver<[User]>let loading: Driver<Bool>let error: Driver<String?>let selectedUser: Driver<User?>}init(userRepository: UserRepositoryProtocol) {self.userRepository = userRepository}func transform(input: Input) -> Output {let activityTracker = ActivityIndicator()let errorTracker = ErrorTracker()let users = Observable.merge(input.viewDidLoad, input.refresh).flatMapLatest { [unowned self] inself.userRepository.fetchUsers().trackActivity(activityTracker).trackError(errorTracker).catchErrorJustReturn([])}.asDriver(onErrorJustReturn: [])let loading = activityTracker.asDriver()let error = errorTracker.map { $0.localizedDescription }.asDriver(onErrorJustReturn: nil)let selectedUser = input.selection.withLatestFrom(users.asObservable()) { indexPath, users inusers[safe: indexPath.row]}.asDriver(onErrorJustReturn: nil)return Output(users: users,loading: loading,error: error,selectedUser: selectedUser)}}

3. View Controller Implementation

Keep view controllers focused on UI binding and user interaction.

final class UserListViewController: UIViewController {
@IBOutlet private weak var tableView: UITableView!
@IBOutlet private weak var refreshButton: UIButton!
private let viewModel: UserListViewModel
private let disposeBag = DisposeBag()
init(viewModel: UserListViewModel) {
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()
bindViewModel()
}
private func bindViewModel() {
let input = UserListViewModel.Input(
viewDidLoad: rx.viewDidLoad.asObservable(),
refresh: refreshButton.rx.tap.asObservable(),
selection: tableView.rx.itemSelected.asObservable()
)
let output = viewModel.transform(input: input)
output.users
.drive(tableView.rx.items(cellIdentifier: "UserCell")) { _, user, cell in
if let userCell = cell as? UserTableViewCell {
userCell.configure(with: user)
}
}
.disposed(by: disposeBag)
output.loading
.drive(onNext: { [weak self] isLoading in
self?.updateLoadingState(isLoading)
})
.disposed(by: disposeBag)
output.error
.compactMap { $0 }
.drive(onNext: { [weak self] error in
self?.showError(message: error)
})
.disposed(by: disposeBag)
output.selectedUser
.compactMap { $0 }
.drive(onNext: { [weak self] user in
self?.navigateToUserDetail(user)
})
.disposed(by: disposeBag)
}
}

4. Repository Pattern

Abstract data sources and provide reactive interfaces.

protocol UserRepositoryProtocol {
func fetchUsers() -> Observable<[User]>func fetchUser(id: Int) -> Observable<User>func updateUser(_ user: User) -> Observable<User>}final class UserRepository: UserRepositoryProtocol {private let networkService: NetworkServiceProtocolprivate let localService: LocalDataServiceProtocolinit(networkService: NetworkServiceProtocol,localService: LocalDataServiceProtocol) {self.networkService = networkServiceself.localService = localService}func fetchUsers() -> Observable<[User]> {return networkService.request(.users).map { (response: APIResponse<[User]>) in response.data }.do(onNext: { [weak self] users inself?.localService.save(users)}).catch { [weak self] _ inself?.localService.fetchUsers() ?? Observable.just([])}}func fetchUser(id: Int) -> Observable<User> {return networkService.request(.user(id: id)).map { (response: APIResponse<User>) in response.data }}func updateUser(_ user: User) -> Observable<User> {return networkService.request(.updateUser(user)).map { (response: APIResponse<User>) in response.data }}}

RxSwift Best Practices

1. Memory Management

// Always dispose subscriptions
.disposed(by: disposeBag)
// Use weak self in closures
.subscribe(onNext: { [weak self] value in
self?.handleValue(value)
})
// Use unowned when certain reference exists
.flatMap { [unowned self] in
self.processData()
}

2. UI Binding

// Use Driver for UI binding (main thread, no errors)
viewModel.data.asDriver()
.drive(tableView.rx.items)
.disposed(by: disposeBag)
// Use Signal for one-time events
viewModel.showAlert.asSignal()
.emit(onNext: { message in
// Show alert
})
.disposed(by: disposeBag)

3. Error Handling

// Centralized error tracking
class ErrorTracker: SharedSequenceConvertibleType {
typealias SharingStrategy = DriverSharingStrategy
private let _subject = PublishSubject<Error>()func trackError<O: ObservableConvertibleType>(from source: O) -> Observable<O.Element> {return source.asObservable().do(onError: onError)}func asSharedSequence() -> SharedSequence<SharingStrategy, Error> {return _subject.asObservable().asDriver(onErrorRecover: { _ in .empty() })}func asObservable() -> Observable<Error> {return _subject.asObservable()}private func onError(_ error: Error) {_subject.onNext(error)}}// Activity indicator for loading statesclass ActivityIndicator: SharedSequenceConvertibleType {typealias Element = Booltypealias SharingStrategy = DriverSharingStrategyprivate let _lock = NSRecursiveLock()private let _subject = BehaviorSubject(value: false)private let _loading: SharedSequence<SharingStrategy, Bool>init() {_loading = _subject.asObservable().distinctUntilChanged().asDriver(onErrorJustReturn: false)}func trackActivityOfObservable<Source: ObservableConvertibleType>(_ source: Source) -> Observable<Source.Element> {return source.asObservable().do(onNext: { _ inself.sendStopLoading()}, onError: { _ inself.sendStopLoading()}, onCompleted: {self.sendStopLoading()}, onSubscribe: subscribed)}private func subscribed() {_lock.lock()_subject.onNext(true)_lock.unlock()}private func sendStopLoading() {_lock.lock()_subject.onNext(false)_lock.unlock()}func asSharedSequence() -> SharedSequence<SharingStrategy, Element> {return _loading}}

4. Network Service

protocol NetworkServiceProtocol {
func request<T: Codable>(_ endpoint: APIEndpoint) -> Observable<T>}final class NetworkService: NetworkServiceProtocol {private let session: URLSessioninit(session: URLSession = .shared) {self.session = session}func request<T: Codable>(_ endpoint: APIEndpoint) -> Observable<T> {return Observable.create { observer inlet request = endpoint.asURLRequest()let task = self.session.dataTask(with: request) { data, response, error inif let error = error {observer.onError(NetworkError.connectionError(error))return}guard let data = data else {observer.onError(NetworkError.noData)return}do {let decodedObject = try JSONDecoder().decode(T.self, from: data)observer.onNext(decodedObject)observer.onCompleted()} catch {observer.onError(NetworkError.decodingError(error))}}task.resume()return Disposables.create {task.cancel()}}}}enum NetworkError: Error {case connectionError(Error)case noDatacase decodingError(Error)}enum APIEndpoint {case userscase user(id: Int)case updateUser(User)func asURLRequest() -> URLRequest {// Implementation detailsvar request = URLRequest(url: url)request.httpMethod = method.rawValuereturn request}}

Useful Extensions

// Observable extensions
extension Observable {
func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable<Element> {return activityIndicator.trackActivityOfObservable(self)}func trackError(_ errorTracker: ErrorTracker) -> Observable<Element> {return errorTracker.trackError(from: self)}}// Array safe subscriptextension Array {subscript(safe index: Int) -> Element? {return indices.contains(index) ? self[index] : nil}}// UITableView reachBottomextension Reactive where Base: UIScrollView {var reachedBottom: Observable<Void> {return contentOffset.flatMap { [weak base] contentOffset -> Observable<Void> inguard let scrollView = base else { return Observable.empty() }let visibleHeight = scrollView.frame.height - scrollView.contentInset.top - scrollView.contentInset.bottomlet y = contentOffset.y + scrollView.contentInset.toplet threshold = max(0.0, scrollView.contentSize.height - visibleHeight)return y > threshold ? Observable.just(()) : Observable.empty()}}}

Testing with RxTest

import XCTest
import RxTest
import RxSwift
@testable import YourApp
class UserListViewModelTests: XCTestCase {
var viewModel: UserListViewModel!
var mockRepository: MockUserRepository!
var scheduler: TestScheduler!
var disposeBag: DisposeBag!
override func setUp() {
super.setUp()
scheduler = TestScheduler(initialClock: 0)
mockRepository = MockUserRepository()
viewModel = UserListViewModel(userRepository: mockRepository)
disposeBag = DisposeBag()
}
func testViewDidLoadFetchesUsers() {
// Given
let users = [User(id: 1, name: "John", email: "john@test.com", avatar: nil)]
mockRepository.usersToReturn = users
let viewDidLoad = scheduler.createHotObservable([.next(10, ())])
let refresh = scheduler.createHotObservable([Recorded<Event<Void>>]())let selection = scheduler.createHotObservable([Recorded<Event<IndexPath>>]())let input = UserListViewModel.Input(viewDidLoad: viewDidLoad.asObservable(),refresh: refresh.asObservable(),selection: selection.asObservable())let output = viewModel.transform(input: input)let result = scheduler.start { output.users.asObservable() }// ThenXCTAssertEqual(result.events.count, 1)XCTAssertEqual(result.events.first?.value.element, users)}}class MockUserRepository: UserRepositoryProtocol {var usersToReturn: [User] = []func fetchUsers() -> Observable<[User]> {return Observable.just(usersToReturn)}func fetchUser(id: Int) -> Observable<User> {return Observable.just(usersToReturn.first(where: { $0.id == id })!)}func updateUser(_ user: User) -> Observable<User> {return Observable.just(user)}}

Code Guidelines

Naming Conventions

File Organization

  • Group by feature, not by type
  • Use meaningful folder names
  • Keep related files together

Architecture Rules

RxSwift Patterns

  • Use Input/Output pattern for ViewModels
  • Prefer Driver/Signal for UI binding
  • Always dispose subscriptions
  • Use ActivityIndicator for loading states
  • Implement proper error handling

Remember: Keep it simple, testable, and maintainable. Focus on reactive streams and clear separation of concerns.

http://www.jsqmd.com/news/374843/

相关文章:

  • 计算机Java毕设实战-基于springboot的小学阶段图形化编程竞赛辅导网站设计与实现【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • 港华商会与碳启元合作,为绿色商业发展注入新动力
  • 银川办公楼装修选哪家?本地专注工装老品牌,适配全规模企业需求 - 宁夏壹山网络
  • 计算机Java毕设实战-基于springboot的粮库设备巡检,维修,报修管理系统设计与实现【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • 改稿速度拉满!千笔,专科生降AI率首选工具
  • 基于C#和周立功USBCAN设备的完整上位机开发示例
  • 计算机Java毕设实战-基于springboot的停车场收费管理系统设计与实现基于 SpringBoot 的社区物业车位收费管理系统设计与实现【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • 题解:P14167 [Algo Beat Contest 002.5 B] 草莓小蛋糕 (cakes)
  • 从“抽卡“到“导演“——如何构建AI视频生成的母提示词系统
  • 【课程设计/毕业设计】基于springboot的粮库设备维修安检管理系统设计与实现【附源码、数据库、万字文档】
  • 实用指南:如何在mac m1模拟鼠标与键盘的自动化操作
  • 中华老字号药企排行TOP10,广药集团白云山登顶百年匠心榜 - 包罗万闻
  • 2000-2024年地级市环境保护数据-29个指标汇总
  • 新房到别墅装修哪家好:2026最新陕西全屋装修设计公司TOP5推荐 - 深度智识库
  • 深入浅出 SPA/MPA - 指南
  • P7422 「PMOI-2」城市 题解
  • 软件检测实验室认可,一次完整的方法验证过程是怎样的?
  • 【花雕动手做】50V380W-500W有霍尔红黑版本驱动模块:6.5寸轮毂电机综合控制方案
  • 基于51单片机的温湿度监测
  • Java毕设项目:基于springboot的粮库设备管理系统设计与实现(源码+文档,讲解、调试运行,定制等)
  • 雅思培训红黑榜,高通过率机构大揭秘! - 品牌测评鉴赏家
  • I.MX6U 开发板网络环境搭建----(电脑 WiFi 上网,开发板和电脑直连)--虚拟机双网口实现-- Ubuntu20.04
  • 2026必备!降AIGC工具 千笔AI VS 灵感ai 专科生专属神器
  • 【毕业设计】基于springboot的粮库设备管理系统设计与实现(源码+文档+远程调试,全bao定制等)
  • 写论文效率低?2026 年 AI 论文软件排行榜权威发布,效率提升 10 倍不是梦!
  • 开题卡住了?8个AI论文工具深度测评,本科生毕业论文写作必备指南
  • 51单片机密码锁的设计
  • 【计算机毕业设计案例】基于springboot的粮库设备管理系统基于java的粮库设备维护管理系统(程序+文档+讲解+定制)
  • 2026最新!AI论文写作软件 千笔ai写作 VS WPS AI,专科生高效写作神器!
  • 基于51单片机和Proteus的智能停车场设计