# 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. ```swift struct User: Codable, Equatable { let id: Int let name: String let email: String let avatar: URL? } struct APIResponse: Codable { let data: T let success: Bool let message: String? } enum LoadingState { case idle case loading case loaded case error(Error) } ``` ### 2. ViewModel Pattern Use Input/Output pattern for clear separation of concerns. ```swift 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 let refresh: Observable let selection: Observable } struct Output { let users: Driver<[User]> let loading: Driver let error: Driver let selectedUser: Driver } 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] in self.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 in users[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. ```swift 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. ```swift protocol UserRepositoryProtocol { func fetchUsers() -> Observable<[User]> func fetchUser(id: Int) -> Observable func updateUser(_ user: User) -> Observable } final class UserRepository: UserRepositoryProtocol { private let networkService: NetworkServiceProtocol private let localService: LocalDataServiceProtocol init( networkService: NetworkServiceProtocol, localService: LocalDataServiceProtocol ) { self.networkService = networkService self.localService = localService } func fetchUsers() -> Observable<[User]> { return networkService.request(.users) .map { (response: APIResponse<[User]>) in response.data } .do(onNext: { [weak self] users in self?.localService.save(users) }) .catch { [weak self] _ in self?.localService.fetchUsers() ?? Observable.just([]) } } func fetchUser(id: Int) -> Observable { return networkService.request(.user(id: id)) .map { (response: APIResponse) in response.data } } func updateUser(_ user: User) -> Observable { return networkService.request(.updateUser(user)) .map { (response: APIResponse) in response.data } } } ``` ## RxSwift Best Practices ### 1. Memory Management ```swift // 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 ```swift // 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 ```swift // Centralized error tracking class ErrorTracker: SharedSequenceConvertibleType { typealias SharingStrategy = DriverSharingStrategy private let _subject = PublishSubject() func trackError(from source: O) -> Observable { return source.asObservable().do(onError: onError) } func asSharedSequence() -> SharedSequence { return _subject.asObservable().asDriver(onErrorRecover: { _ in .empty() }) } func asObservable() -> Observable { return _subject.asObservable() } private func onError(_ error: Error) { _subject.onNext(error) } } // Activity indicator for loading states class ActivityIndicator: SharedSequenceConvertibleType { typealias Element = Bool typealias SharingStrategy = DriverSharingStrategy private let _lock = NSRecursiveLock() private let _subject = BehaviorSubject(value: false) private let _loading: SharedSequence init() { _loading = _subject.asObservable() .distinctUntilChanged() .asDriver(onErrorJustReturn: false) } func trackActivityOfObservable(_ source: Source) -> Observable { return source.asObservable() .do(onNext: { _ in self.sendStopLoading() }, onError: { _ in self.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 { return _loading } } ``` ### 4. Network Service ```swift protocol NetworkServiceProtocol { func request(_ endpoint: APIEndpoint) -> Observable } final class NetworkService: NetworkServiceProtocol { private let session: URLSession init(session: URLSession = .shared) { self.session = session } func request(_ endpoint: APIEndpoint) -> Observable { return Observable.create { observer in let request = endpoint.asURLRequest() let task = self.session.dataTask(with: request) { data, response, error in if 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 noData case decodingError(Error) } enum APIEndpoint { case users case user(id: Int) case updateUser(User) func asURLRequest() -> URLRequest { // Implementation details var request = URLRequest(url: url) request.httpMethod = method.rawValue return request } } ``` ## Useful Extensions ```swift // Observable extensions extension Observable { func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable { return activityIndicator.trackActivityOfObservable(self) } func trackError(_ errorTracker: ErrorTracker) -> Observable { return errorTracker.trackError(from: self) } } // Array safe subscript extension Array { subscript(safe index: Int) -> Element? { return indices.contains(index) ? self[index] : nil } } // UITableView reachBottom extension Reactive where Base: UIScrollView { var reachedBottom: Observable { return contentOffset .flatMap { [weak base] contentOffset -> Observable in guard let scrollView = base else { return Observable.empty() } let visibleHeight = scrollView.frame.height - scrollView.contentInset.top - scrollView.contentInset.bottom let y = contentOffset.y + scrollView.contentInset.top let threshold = max(0.0, scrollView.contentSize.height - visibleHeight) return y > threshold ? Observable.just(()) : Observable.empty() } } } ``` ## Testing with RxTest ```swift 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>]()) let selection = scheduler.createHotObservable([Recorded>]()) 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() } // Then XCTAssertEqual(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 { return Observable.just(usersToReturn.first(where: { $0.id == id })!) } func updateUser(_ user: User) -> Observable { return Observable.just(user) } } ``` ## Code Guidelines ### Naming Conventions - ViewModels: `FeatureViewModel` - ViewControllers: `FeatureViewController` - Repositories: `FeatureRepository` - Services: `FeatureService` ### File Organization - Group by feature, not by type - Use meaningful folder names - Keep related files together ### Architecture Rules - ViewModels should not import UIKit - Views should not contain business logic - Use dependency injection for testability - Separate network and local data concerns ### 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.