はじめに
現在進行形で使っていないアーキテクチャのことは忘れてしまうので、(忘備録として、最近流行りの)簡単なTODOアプリを使って、iOSの各アーキテクチャごとのサンプルを作ってみました。
ソースコードは以下で公開しております。
備考
各アーキテクチャがざっくりこんな感じというのが一目で分かるように、最小構成で1ファイルにまとめています。教科書的な記事ではございません。
対象となるアーキテクチャ
- MVC
- MVVM
- RxSwift
- RxSwift+MVVM
- Clean Architecture
- VIPER
- SwiftUI
- SwiftUI+MVVM
- SwiftUI+Flux(GitHubで公開)
- SwiftUI+Redux(GitHubで公開)
- TCA
各アーキテクチャの特徴
各アーキテクチャの特徴はChatGPTにまとめてもらいました。
-
MVC (Model-View-Controller):
- 最も基本的なiOSアーキテクチャ。
- アプリケーションを3つの主要なコンポーネントに分割:Model(データとビジネスロジック)、View(ユーザーインターフェース)、Controller(ViewとModelの間の仲介者)。
- 単純で理解しやすいが、大規模なアプリケーションでは問題が発生することがある。
-
MVVM (Model-View-ViewModel):
- ビューモデル(ViewModel)が導入され、ビジネスロジックの一部をビューモデルに移し、ViewとModelを切り離す。
- ユーザーインターフェースのロジックを分離し、テスト可能なコードを生成するのに役立つ。
- データバインディングを使用して、ViewとViewModelの間でデータを同期させる。
-
RxSwift:
- 非同期プログラミングのためのライブラリで、観測可能なシーケンスを使用してイベント駆動プログラムをサポート。
- イベント駆動プログラミングの特性を活用し、リアクティブなアプリケーションを構築するのに適している。
-
RxSwift+MVVM:
- MVVMアーキテクチャにRxSwiftを組み合わせたアーキテクチャ。
- RxSwiftを使用して、ViewModelとViewの間でデータフローを管理し、非同期プログラミングをサポート。
-
Clean Architecture:
- アプリケーションを独立した層に分割し、各層の責任を明確にするアーキテクチャ。
- 主要な層には、エンティティ、ユースケース(ビジネスロジック)、リポジトリ(データアクセス)、プレゼンテーション(UI)が含まれる。
- テスト、保守性、拡張性を向上させる。
-
VIPER:
- View、Interactor、Presenter、Entity、Routerの略称で、iOS用のクリーンアーキテクチャの一種。
- 各コンポーネントは特定の役割を果たし、強力な分離とテストが可能。
- モジュールごとに分割され、大規模なアプリケーションの構築に適している。
-
SwiftUI:
- Appleによって導入された新しいUIフレームワーク。
- 壮大なビューツリーを宣言的な構文で構築し、状態管理を簡素化する。
- クロスプラットフォーム開発をサポートし、UIKitと統合することも可能。
-
SwiftUI+MVVM:
- SwiftUIとMVVMアーキテクチャを組み合わせたアーキテクチャ。
- SwiftUIの宣言的なUIを使用しながら、ビジネスロジックをViewModelに分離し、テスト可能なコードを生成。
-
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)
}
}
}