iOSの各アーキテクチャサンプル（忘備録）

# はじめに

現在進行形で使っていないアーキテクチャのことは忘れてしまうので、（忘備録として、最近流行りの）簡単なTODOアプリを使って、iOSの各アーキテクチャごとのサンプルを作ってみました。

ソースコードは以下で公開しております。

https://github.com/Yokohama-Hori/ArchitectureSample

## 備考

各アーキテクチャがざっくりこんな感じというのが一目で分かるように、最小構成で１ファイルにまとめています。教科書的な記事ではございません。

## 対象となるアーキテクチャ

- MVC
- MVVM
- RxSwift
- RxSwift+MVVM
- Clean Architecture
- VIPER
- SwiftUI
- SwiftUI+MVVM
- SwiftUI+Flux(GitHubで公開)
- SwiftUI+Redux(GitHubで公開)
- TCA

## 各アーキテクチャの特徴

各アーキテクチャの特徴はChatGPTにまとめてもらいました。

1. **MVC (Model-View-Controller):**
    - 最も基本的なiOSアーキテクチャ。
    - アプリケーションを3つの主要なコンポーネントに分割：Model（データとビジネスロジック）、View（ユーザーインターフェース）、Controller（ViewとModelの間の仲介者）。
    - 単純で理解しやすいが、大規模なアプリケーションでは問題が発生することがある。
2. **MVVM (Model-View-ViewModel):**
    - ビューモデル（ViewModel）が導入され、ビジネスロジックの一部をビューモデルに移し、ViewとModelを切り離す。
    - ユーザーインターフェースのロジックを分離し、テスト可能なコードを生成するのに役立つ。
    - データバインディングを使用して、ViewとViewModelの間でデータを同期させる。
3. **RxSwift:**
    - 非同期プログラミングのためのライブラリで、観測可能なシーケンスを使用してイベント駆動プログラムをサポート。
    - イベント駆動プログラミングの特性を活用し、リアクティブなアプリケーションを構築するのに適している。
4. **RxSwift+MVVM:**
    - MVVMアーキテクチャにRxSwiftを組み合わせたアーキテクチャ。
    - RxSwiftを使用して、ViewModelとViewの間でデータフローを管理し、非同期プログラミングをサポート。
5. **Clean Architecture:**
    - アプリケーションを独立した層に分割し、各層の責任を明確にするアーキテクチャ。
    - 主要な層には、エンティティ、ユースケース（ビジネスロジック）、リポジトリ（データアクセス）、プレゼンテーション（UI）が含まれる。
    - テスト、保守性、拡張性を向上させる。
6. **VIPER:**
    - View、Interactor、Presenter、Entity、Routerの略称で、iOS用のクリーンアーキテクチャの一種。
    - 各コンポーネントは特定の役割を果たし、強力な分離とテストが可能。
    - モジュールごとに分割され、大規模なアプリケーションの構築に適している。
7. **SwiftUI:**
    - Appleによって導入された新しいUIフレームワーク。
    - 壮大なビューツリーを宣言的な構文で構築し、状態管理を簡素化する。
    - クロスプラットフォーム開発をサポートし、UIKitと統合することも可能。
8. **SwiftUI+MVVM:**
    - SwiftUIとMVVMアーキテクチャを組み合わせたアーキテクチャ。
    - SwiftUIの宣言的なUIを使用しながら、ビジネスロジックをViewModelに分離し、テスト可能なコードを生成。
9. **TCA (The Composable Architecture):**
    - フレームワークではなく、コミュニティによって提供されるアーキテクチャパターン。
    - SwiftUIと組み合わせて使用し、UIコンポーネントをコンポーザブルに構築するのに役立つ。
    - 状態管理、アクション、リダクションなどのコンセプトを持つ。

# サンプルソース

## MVC

```swift
import UIKit

// MARK: - Model
struct MVCTodoItem: Identifiable {
    var id = UUID()
    var task: String
}

// MARK: - View & Controller
class MVCTodoViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
    private let titleText: String
    private var tableView: UITableView!
    private var taskTextField: UITextField!
    private var addButton: UIButton!
    
    private var tasks: [MVCTodoItem] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupUI()
        
        taskTextField.delegate = self
        
        addButton.addTarget(self, action: #selector(addTask), for: .touchUpInside)
        
        tableView.delegate = self
        tableView.dataSource = self
    }
    
    @objc func addTask() {
        guard let task = taskTextField.text, !task.isEmpty else { return }
        let newTodo = MVCTodoItem(task: task)
        tasks.append(newTodo)
        tableView.reloadData()
        taskTextField.text = ""
    }
    
    // TableView DataSource Methods
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tasks.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = tasks[indexPath.row].task
        return cell
    }
    
    // TableView Delegate Methods
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            tasks.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .automatic)
        }
    }
    
    // TextField Delegate Methods
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        addTask()
        return true
    }
    
    private func setupUI() {
        view.backgroundColor = .white
        navigationItem.title = titleText
        
        taskTextField = UITextField(frame: CGRect(x: 20, y: 100, width: view.bounds.width - 140, height: 40))
        taskTextField.placeholder = "New task " + titleText
        taskTextField.borderStyle = .roundedRect
        view.addSubview(taskTextField)
        
        addButton = UIButton(frame: CGRect(x: view.bounds.width - 110, y: 100, width: 100, height: 40))
        addButton.setTitle("Add", for: .normal)
        addButton.setTitleColor(.blue, for: .normal)
        view.addSubview(addButton)
        
        tableView = UITableView(frame: CGRect(x: 0, y: 150, width: view.bounds.width, height: view.bounds.height - 150))
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        view.addSubview(tableView)
    }
    
    init(title: String) {
        self.titleText = title
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
```

## MVVM

```swift
import UIKit

// MARK: - Model
struct MVVMTodoItem: Identifiable {
    var id = UUID()
    var task: String
}

// MARK: - ViewModel
class MVVMTodoViewModel {
    var tasks: [MVVMTodoItem] = []
    
    func add(task: String) {
        let newTodo = MVVMTodoItem(task: task)
        tasks.append(newTodo)
    }
    
    func remove(at index: Int) {
        tasks.remove(at: index)
    }
}

// MARK: - View
class MVVMTodoViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
    private let titleText: String
    private var tableView: UITableView!
    private var taskTextField: UITextField!
    private var addButton: UIButton!
    
    private let viewModel = MVVMTodoViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupUI()
        
        taskTextField.delegate = self
        
        addButton.addTarget(self, action: #selector(addTask), for: .touchUpInside)
        
        tableView.delegate = self
        tableView.dataSource = self
    }
    
    @objc func addTask() {
        guard let task = taskTextField.text, !task.isEmpty else { return }
        viewModel.add(task: task)
        tableView.reloadData()
        taskTextField.text = ""
    }
    
    // TableView DataSource Methods
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.tasks.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = viewModel.tasks[indexPath.row].task
        return cell
    }
    
    // TableView Delegate Methods
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            viewModel.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .automatic)
        }
    }
    
    // TextField Delegate Methods
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        addTask()
        return true
    }
    
    private func setupUI() {
        view.backgroundColor = .white
        navigationItem.title = titleText
        
        taskTextField = UITextField(frame: CGRect(x: 20, y: 100, width: view.bounds.width - 140, height: 40))
        taskTextField.placeholder = "New task " + titleText
        taskTextField.borderStyle = .roundedRect
        view.addSubview(taskTextField)
        
        addButton = UIButton(frame: CGRect(x: view.bounds.width - 110, y: 100, width: 100, height: 40))
        addButton.setTitle("Add", for: .normal)
        addButton.setTitleColor(.blue, for: .normal)
        view.addSubview(addButton)
        
        tableView = UITableView(frame: CGRect(x: 0, y: 150, width: view.bounds.width, height: view.bounds.height - 150))
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        view.addSubview(tableView)
    }
    
    init(title: String) {
        self.titleText = title
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
```

## RxSwift

```swift
import UIKit
import RxSwift
import RxCocoa

// MARK: - Model
struct RXTodoItem: Identifiable {
    var id = UUID()
    var task: String
}

// MARK: - View & Controller
class RXTodoViewController: UIViewController {
    private let titleText: String
    private var tableView: UITableView!
    private var taskTextField: UITextField!
    private var addButton: UIButton!
    
    private var tasksRelay = BehaviorRelay<[MVCTodoItem]>(value: [])
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupUI()
        
        // Bindings
        addButton.rx.tap
            .subscribe(onNext: { [weak self] in
                self?.addTask()
            })
            .disposed(by: disposeBag)
        
        taskTextField.rx.controlEvent(.editingDidEndOnExit)
            .subscribe(onNext: { [weak self] in
                self?.addTask()
            })
            .disposed(by: disposeBag)
        
        tasksRelay.bind(to: tableView.rx.items(cellIdentifier: "cell")) { (row, element, cell) in
            cell.textLabel?.text = element.task
        }
        .disposed(by: disposeBag)
        
        tableView.rx.itemDeleted
            .subscribe(onNext: { [weak self] indexPath in
                var tasks = self?.tasksRelay.value ?? []
                tasks.remove(at: indexPath.row)
                self?.tasksRelay.accept(tasks)
            })
            .disposed(by: disposeBag)
    }
    
    func addTask() {
        guard let task = taskTextField.text, !task.isEmpty else { return }
        let newTodo = MVCTodoItem(task: task)
        var currentTasks = tasksRelay.value
        currentTasks.append(newTodo)
        tasksRelay.accept(currentTasks)
        taskTextField.text = ""
    }
    
    private func setupUI() {
        view.backgroundColor = .white
        navigationItem.title = titleText
        
        taskTextField = UITextField(frame: CGRect(x: 20, y: 100, width: view.bounds.width - 140, height: 40))
        taskTextField.placeholder = "New task " + titleText
        taskTextField.borderStyle = .roundedRect
        view.addSubview(taskTextField)
        
        addButton = UIButton(frame: CGRect(x: view.bounds.width - 110, y: 100, width: 100, height: 40))
        addButton.setTitle("Add", for: .normal)
        addButton.setTitleColor(.blue, for: .normal)
        view.addSubview(addButton)
        
        tableView = UITableView(frame: CGRect(x: 0, y: 150, width: view.bounds.width, height: view.bounds.height - 150))
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        view.addSubview(tableView)
    }
    
    init(title: String) {
        self.titleText = title
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
```

## RxSwift+MVVM

```swift
import UIKit
import RxSwift
import RxCocoa

// MARK: - Model
struct RXMVVMTodoItem: Identifiable {
    var id = UUID()
    var task: String
}

class RXMVVMTodoViewModel {
    // Outputs
    let tasks: Observable<[RXMVVMTodoItem]>
    
    // Inputs
    let addTaskSubject = PublishSubject<String>()
    let removeTaskAtIndexSubject = PublishSubject<Int>()
    
    private let tasksRelay = BehaviorRelay<[RXMVVMTodoItem]>(value: [])
    private let disposeBag = DisposeBag()
    
    init() {
        self.tasks = tasksRelay.asObservable()
        
        addTaskSubject
            .map { RXMVVMTodoItem(task: $0) }
            .withLatestFrom(tasksRelay.asObservable()) { (newTask: RXMVVMTodoItem, tasks: [RXMVVMTodoItem]) -> [RXMVVMTodoItem] in
                return tasks + [newTask]
            }
            .bind(to: tasksRelay)
            .disposed(by: disposeBag)
        
        removeTaskAtIndexSubject
            .withLatestFrom(tasksRelay.asObservable()) { (index: Int, tasks: [RXMVVMTodoItem]) -> [RXMVVMTodoItem] in
                var tasks = tasks
                tasks.remove(at: index)
                return tasks
            }
            .bind(to: tasksRelay)
            .disposed(by: disposeBag)
    }
}

// MARK: - View
class RXMVVMTodoViewController: UIViewController {
    private let titleText: String
    private var tableView: UITableView!
    private var taskTextField: UITextField!
    private var addButton: UIButton!
    
    private let disposeBag = DisposeBag()
    private let viewModel = RXMVVMTodoViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupUI()
        
        // bind
        viewModel.tasks
            .bind(to: tableView.rx.items(cellIdentifier: "cell", cellType: UITableViewCell.self)) { row, task, cell in
                cell.textLabel?.text = task.task
            }
            .disposed(by: disposeBag)
        
        taskTextField.rx.controlEvent(.editingDidEndOnExit)
            .withLatestFrom(taskTextField.rx.text.orEmpty)
            .do(onNext: { [weak self] _ in
                self?.taskTextField.text = ""
            })
            .bind(to: viewModel.addTaskSubject)
            .disposed(by: disposeBag)
        
        tableView.rx.itemDeleted
            .map { $0.row }
            .bind(to: viewModel.removeTaskAtIndexSubject)
            .disposed(by: disposeBag)
        
        addButton.rx.tap
            .withLatestFrom(taskTextField.rx.text.orEmpty)
            .do(onNext: { [weak self] _ in
                self?.taskTextField.text = ""
            })
            .bind(to: viewModel.addTaskSubject)
            .disposed(by: disposeBag)
    }
    
    private func setupUI() {
        view.backgroundColor = .white
        navigationItem.title = titleText
        
        taskTextField = UITextField(frame: CGRect(x: 20, y: 100, width: view.bounds.width - 140, height: 40))
        taskTextField.placeholder = "New task " + titleText
        taskTextField.borderStyle = .roundedRect
        view.addSubview(taskTextField)
        
        addButton = UIButton(frame: CGRect(x: view.bounds.width - 110, y: 100, width: 100, height: 40))
        addButton.setTitle("Add", for: .normal)
        addButton.setTitleColor(.blue, for: .normal)
        view.addSubview(addButton)
        
        tableView = UITableView(frame: CGRect(x: 0, y: 150, width: view.bounds.width, height: view.bounds.height - 150))
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        view.addSubview(tableView)
    }
    
    init(title: String) {
        self.titleText = title
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
```

## Clean Architecture

```swift
import UIKit

// MARK: - Entity
struct CleanTodoItem {
    var id = UUID()
    var task: String
}

// MARK: - Use Cases
protocol CleanTodoUseCase {
    func addTask(task: String)
    func removeTask(at index: Int)
    func getTasks() -> [CleanTodoItem]
}

class CleanTodoInteractor: CleanTodoUseCase {
    private var tasks: [CleanTodoItem] = []
    
    func addTask(task: String) {
        let newTodo = CleanTodoItem(task: task)
        tasks.append(newTodo)
    }
    
    func removeTask(at index: Int) {
        tasks.remove(at: index)
    }
    
    func getTasks() -> [CleanTodoItem] {
        return tasks
    }
}

// MARK: - Presenter
class CleanTodoPresenter {
    weak var viewController: CleanTodoViewController?
    
    private let useCase: CleanTodoUseCase
    
    init(useCase: CleanTodoUseCase) {
        self.useCase = useCase
    }
    
    func addTask(task: String) {
        useCase.addTask(task: task)
        viewController?.refreshUI()
    }
    
    func removeTask(at index: Int) {
        useCase.removeTask(at: index)
        viewController?.deleteRow(at: index)
    }
    
    func getTasks() -> [CleanTodoItem] {
        return useCase.getTasks()
    }
}

// MARK: - View
class CleanTodoViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
    private let titleText: String
    private var tableView: UITableView!
    private var taskTextField: UITextField!
    private var addButton: UIButton!
    
    private var presenter: CleanTodoPresenter!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupUI()
        
        taskTextField.delegate = self
        
        addButton.addTarget(self, action: #selector(addTask), for: .touchUpInside)
        
        tableView.delegate = self
        tableView.dataSource = self
    }
    
    @objc func addTask() {
        guard let task = taskTextField.text, !task.isEmpty else { return }
        presenter.addTask(task: task)
    }
    
    func refreshUI() {
        tableView.reloadData()
        taskTextField.text = ""
    }
    
    func deleteRow(at index: Int) {
        tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
    }
    
    // TableView DataSource Methods
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return presenter.getTasks().count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = presenter.getTasks()[indexPath.row].task
        return cell
    }
    
    // TableView Delegate Methods
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            presenter.removeTask(at: indexPath.row)
        }
    }
    
    // TextField Delegate Methods
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        addTask()
        return true
    }
    
    private func setupUI() {
        view.backgroundColor = .white
        navigationItem.title = titleText
        
        taskTextField = UITextField(frame: CGRect(x: 20, y: 100, width: view.bounds.width - 140, height: 40))
        taskTextField.placeholder = "New task " + titleText
        taskTextField.borderStyle = .roundedRect
        view.addSubview(taskTextField)
        
        addButton = UIButton(frame: CGRect(x: view.bounds.width - 110, y: 100, width: 100, height: 40))
        addButton.setTitle("Add", for: .normal)
        addButton.setTitleColor(.blue, for: .normal)
        view.addSubview(addButton)
        
        tableView = UITableView(frame: CGRect(x: 0, y: 150, width: view.bounds.width, height: view.bounds.height - 150))
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        view.addSubview(tableView)
    }
    
    init(title: String, presenter: CleanTodoPresenter) {
        self.titleText = title
        self.presenter = presenter
        super.init(nibName: nil, bundle: nil)
        self.presenter.viewController = self
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
```

## VIPER

```swift
import UIKit

// MARK: - Entity
struct VIPERTodoItem: Identifiable {
    var id = UUID()
    var task: String
}

// MARK: - Interactor
protocol VIPERTodoInteractorInput {
    func addTask(task: String)
    func removeTask(at index: Int)
    func getTasks() -> [VIPERTodoItem]
}

class VIPERTodoInteractor: VIPERTodoInteractorInput {
    private var tasks: [VIPERTodoItem] = []
    
    func addTask(task: String) {
        let newTodo = VIPERTodoItem(task: task)
        tasks.append(newTodo)
    }
    
    func removeTask(at index: Int) {
        tasks.remove(at: index)
    }
    
    func getTasks() -> [VIPERTodoItem] {
        return tasks
    }
}

// MARK: - Presenter
protocol VIPERTodoPresenterInput {
    func addTask(task: String)
    func removeTask(at index: Int)
    func getTasks() -> [VIPERTodoItem]
}

class VIPERTodoPresenter: VIPERTodoPresenterInput {
    private weak var view: VIPERTodoViewProtocol?
    private var interactor: VIPERTodoInteractorInput
    
    init(view: VIPERTodoViewProtocol, interactor: VIPERTodoInteractorInput) {
        self.view = view
        self.interactor = interactor
    }
    
    func addTask(task: String) {
        interactor.addTask(task: task)
        view?.refreshUI()
    }
    
    func removeTask(at index: Int) {
        interactor.removeTask(at: index)
        view?.deleteRow(at: index)
    }
    
    func getTasks() -> [VIPERTodoItem] {
        interactor.getTasks()
    }
}

// MARK: - View
protocol VIPERTodoViewProtocol: AnyObject {
    func refreshUI()
    func deleteRow(at index: Int)
}

class VIPERTodoViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate, VIPERTodoViewProtocol {
    private let titleText: String
    private var tableView: UITableView!
    private var taskTextField: UITextField!
    private var addButton: UIButton!
    
    var presenter: VIPERTodoPresenterInput!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupUI()
        
        taskTextField.delegate = self
        
        addButton.addTarget(self, action: #selector(addTask), for: .touchUpInside)
        
        tableView.delegate = self
        tableView.dataSource = self
    }
    
    @objc func addTask() {
        guard let task = taskTextField.text else { return }
        presenter.addTask(task: task)
    }
    
    func refreshUI() {
        tableView.reloadData()
        taskTextField.text = ""
    }
    
    func deleteRow(at index: Int) {
        tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
    }
    
    // TableView DataSource Methods
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return presenter.getTasks().count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = presenter.getTasks()[indexPath.row].task
        return cell
    }
    
    // TableView Delegate Methods
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            presenter.removeTask(at: indexPath.row)
        }
    }
    
    // TextField Delegate Methods
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        addTask()
        return true
    }
    
    private func setupUI() {
        view.backgroundColor = .white
        navigationItem.title = titleText
        
        taskTextField = UITextField(frame: CGRect(x: 20, y: 100, width: view.bounds.width - 140, height: 40))
        taskTextField.placeholder = "New task " + titleText
        taskTextField.borderStyle = .roundedRect
        view.addSubview(taskTextField)
        
        addButton = UIButton(frame: CGRect(x: view.bounds.width - 110, y: 100, width: 100, height: 40))
        addButton.setTitle("Add", for: .normal)
        addButton.setTitleColor(.blue, for: .normal)
        view.addSubview(addButton)
        
        tableView = UITableView(frame: CGRect(x: 0, y: 150, width: view.bounds.width, height: view.bounds.height - 150))
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        view.addSubview(tableView)
    }
    
    init(title: String) {
        self.titleText = title
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

// MARK: - Router
protocol VIPERTodoRouterProtocol {
    func createTodoViewController(title: String) -> UIViewController
}

class VIPERTodoRouter: VIPERTodoRouterProtocol {
    func createTodoViewController(title: String) -> UIViewController {
        let interactor = VIPERTodoInteractor()
        let todoViewController = VIPERTodoViewController(title: title)
        let presenter = VIPERTodoPresenter(view: todoViewController, interactor: interactor)
        todoViewController.presenter = presenter
        return todoViewController
    }
}
```

## SwiftUI

```swift
import SwiftUI

// MARK: - Model
struct SwiftUITodoItem: Identifiable {
    var id = UUID()
    var task: String
}

// MARK: - View
struct SwiftUITodoView: View {
    let titleText: String
    
    @State private var newTask: String = ""
    @State private var tasks: [SwiftUITodoItem] = []
    
    var body: some View {
        VStack {
            HStack {
                TextField("New task " + titleText, text: $newTask)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                
                Button(action: addTask) {
                    Image(systemName: "plus.circle.fill")
                        .font(.largeTitle)
                }
            }
            .padding()
            
            List {
                ForEach(tasks) { task in
                    Text(task.task)
                }
                .onDelete(perform: removeTask)
            }
        }
        .navigationTitle(titleText)
    }
    
    func addTask() {
        let newTodo = SwiftUITodoItem(task: newTask)
        tasks.append(newTodo)
        newTask = ""
    }
    
    func removeTask(at offsets: IndexSet) {
        tasks.remove(atOffsets: offsets)
    }
}
```

## SwiftUI+MVVM

```swift
import SwiftUI

// MARK: - Model
struct SwiftUIMVVMTodoItem: Identifiable {
    var id = UUID()
    var task: String
}

// MARK: - ViewModel
class SwiftUIMVVMTodoViewModel: ObservableObject {
    @Published var tasks: [SwiftUIMVVMTodoItem] = []
    @Published var newTask: String = ""
    
    func addTask() {
        let newTodo = SwiftUIMVVMTodoItem(task: newTask)
        tasks.append(newTodo)
        newTask = ""
    }
    
    func removeTask(at offsets: IndexSet) {
        tasks.remove(atOffsets: offsets)
    }
}

// MARK: - View
struct SwiftUIMVVMTodoView: View {
    let titleText: String
    
    @ObservedObject private var viewModel = SwiftUIMVVMTodoViewModel()
    
    var body: some View {
        VStack {
            HStack {
                TextField("New task " + titleText, text: $viewModel.newTask)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                
                Button(action: viewModel.addTask) {
                    Image(systemName: "plus.circle.fill")
                        .font(.largeTitle)
                }
            }
            .padding()
            
            List {
                ForEach(viewModel.tasks) { task in
                    Text(task.task)
                }
                .onDelete(perform: viewModel.removeTask)
            }
        }
        .navigationTitle(titleText)
    }
}
```

## TCA

```swift
import SwiftUI
import ComposableArchitecture

// MARK: - Model
struct TCATodoItem: Identifiable, Equatable {
    var id: UUID
    var task: String
}

struct TCATodoState: Equatable {
    var todos: [TCATodoItem] = []
    var newTask: String = ""
}

enum TCATodoAction: Equatable {
    case addTask
    case updateNewTask(String)
    case removeTask(IndexSet)
}

// MARK: - Reducer
let todoReducer = Reducer<TCATodoState, TCATodoAction, Void> { state, action, _ in
    switch action {
    case .addTask:
        state.todos.append(TCATodoItem(id: UUID(), task: state.newTask))
        state.newTask = ""
        return .none
    case .updateNewTask(let text):
        state.newTask = text
        return .none
    case .removeTask(let indexSet):
        state.todos.remove(atOffsets: indexSet)
        return .none
    }
}

// MARK: - View
struct TCATodoView: View {
    let titleText: String
    let store: Store<TCATodoState, TCATodoAction>
    
    var body: some View {
        WithViewStore(self.store) { viewStore in
            VStack {
                HStack {
                    TextField("New task " + titleText, text: viewStore.binding(
                        get: \.newTask,
                        send: TCATodoAction.updateNewTask
                    ))
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    
                    Button(action: {
                        viewStore.send(.addTask)
                    }) {
                        Image(systemName: "plus.circle.fill")
                            .font(.largeTitle)
                    }
                }
                .padding()
                
                List {
                    ForEach(viewStore.todos) { todo in
                        Text(todo.task)
                    }
                    .onDelete { indexSet in
                        viewStore.send(.removeTask(indexSet))
                    }
                }
            }
            .navigationTitle(titleText)
        }
    }
}
```