7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2023-10-30

はじめに

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

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

備考

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

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

  • 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

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

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

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

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

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

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

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

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

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)
        }
    }
}
7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?