ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

实用指南: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.

返回列表