diff --git a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj index 4622d4d6c..a876db838 100644 --- a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj +++ b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj @@ -7,6 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + 63C5ECB62AC1949A003904C4 /* ListTitleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C5ECB52AC1949A003904C4 /* ListTitleCell.swift */; }; + 63C5ECB82AC2B957003904C4 /* DescriptionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C5ECB72AC2B957003904C4 /* DescriptionCell.swift */; }; + 63C5ECBA2AC2FE9A003904C4 /* AddTodoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C5ECB92AC2FE9A003904C4 /* AddTodoViewController.swift */; }; + 63C5ECCA2AC46CDB003904C4 /* ProjectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C5ECC92AC46CDA003904C4 /* ProjectManager.swift */; }; + 63C5ECCD2AC46F83003904C4 /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C5ECCC2AC46F83003904C4 /* DataManager.swift */; }; + 63CC81602AC6AA5B00E6355F /* ReuseIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CC815F2AC6AA5B00E6355F /* ReuseIdentifier.swift */; }; + 63CC816E2AC6D8CA00E6355F /* CellTitleNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CC816D2AC6D8CA00E6355F /* CellTitleNamespace.swift */; }; + 63CC81712AC7F5B600E6355F /* MainViewControllerUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CC81702AC7F5B600E6355F /* MainViewControllerUseCase.swift */; }; + 63CC81752AC95D7D00E6355F /* TitleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CC81742AC95D7D00E6355F /* TitleItem.swift */; }; + 63CC81782AC9665900E6355F /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CC81772AC9665900E6355F /* MainViewModel.swift */; }; C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0525F51E1D0094C4CF /* AppDelegate.swift */; }; C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0725F51E1D0094C4CF /* SceneDelegate.swift */; }; C7431F0A25F51E1D0094C4CF /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0925F51E1D0094C4CF /* MainViewController.swift */; }; @@ -15,6 +25,16 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 63C5ECB52AC1949A003904C4 /* ListTitleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTitleCell.swift; sourceTree = ""; }; + 63C5ECB72AC2B957003904C4 /* DescriptionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionCell.swift; sourceTree = ""; }; + 63C5ECB92AC2FE9A003904C4 /* AddTodoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTodoViewController.swift; sourceTree = ""; }; + 63C5ECC92AC46CDA003904C4 /* ProjectManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectManager.swift; sourceTree = ""; }; + 63C5ECCC2AC46F83003904C4 /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = ""; }; + 63CC815F2AC6AA5B00E6355F /* ReuseIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReuseIdentifier.swift; sourceTree = ""; }; + 63CC816D2AC6D8CA00E6355F /* CellTitleNamespace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellTitleNamespace.swift; sourceTree = ""; }; + 63CC81702AC7F5B600E6355F /* MainViewControllerUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewControllerUseCase.swift; sourceTree = ""; }; + 63CC81742AC95D7D00E6355F /* TitleItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleItem.swift; sourceTree = ""; }; + 63CC81772AC9665900E6355F /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = ""; }; C7431F0225F51E1D0094C4CF /* ProjectManager.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ProjectManager.app; sourceTree = BUILT_PRODUCTS_DIR; }; C7431F0525F51E1D0094C4CF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C7431F0725F51E1D0094C4CF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -35,6 +55,95 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 63C5ECB22AC1944F003904C4 /* App */ = { + isa = PBXGroup; + children = ( + C7431F0525F51E1D0094C4CF /* AppDelegate.swift */, + C7431F0725F51E1D0094C4CF /* SceneDelegate.swift */, + ); + path = App; + sourceTree = ""; + }; + 63C5ECB32AC19459003904C4 /* Controller */ = { + isa = PBXGroup; + children = ( + 63CC81762AC9663D00E6355F /* MainView */, + 63CC81792AC9726100E6355F /* AddTodoView */, + ); + path = Controller; + sourceTree = ""; + }; + 63C5ECB42AC19464003904C4 /* View */ = { + isa = PBXGroup; + children = ( + C7431F1025F51E1E0094C4CF /* LaunchScreen.storyboard */, + 63C5ECB52AC1949A003904C4 /* ListTitleCell.swift */, + 63C5ECB72AC2B957003904C4 /* DescriptionCell.swift */, + ); + path = View; + sourceTree = ""; + }; + 63C5ECBB2AC45243003904C4 /* Model */ = { + isa = PBXGroup; + children = ( + 63CC815E2AC6AA3600E6355F /* Namespace */, + 63C5ECCB2AC46F4E003904C4 /* DataManager */, + 63C5ECBE2AC45277003904C4 /* Entity */, + ); + path = Model; + sourceTree = ""; + }; + 63C5ECBE2AC45277003904C4 /* Entity */ = { + isa = PBXGroup; + children = ( + 63C5ECC92AC46CDA003904C4 /* ProjectManager.swift */, + 63CC81742AC95D7D00E6355F /* TitleItem.swift */, + ); + path = Entity; + sourceTree = ""; + }; + 63C5ECCB2AC46F4E003904C4 /* DataManager */ = { + isa = PBXGroup; + children = ( + 63C5ECCC2AC46F83003904C4 /* DataManager.swift */, + ); + path = DataManager; + sourceTree = ""; + }; + 63CC815E2AC6AA3600E6355F /* Namespace */ = { + isa = PBXGroup; + children = ( + 63CC815F2AC6AA5B00E6355F /* ReuseIdentifier.swift */, + 63CC816D2AC6D8CA00E6355F /* CellTitleNamespace.swift */, + ); + path = Namespace; + sourceTree = ""; + }; + 63CC816F2AC7F59700E6355F /* UseCase */ = { + isa = PBXGroup; + children = ( + 63CC81702AC7F5B600E6355F /* MainViewControllerUseCase.swift */, + ); + path = UseCase; + sourceTree = ""; + }; + 63CC81762AC9663D00E6355F /* MainView */ = { + isa = PBXGroup; + children = ( + C7431F0925F51E1D0094C4CF /* MainViewController.swift */, + 63CC81772AC9665900E6355F /* MainViewModel.swift */, + ); + path = MainView; + sourceTree = ""; + }; + 63CC81792AC9726100E6355F /* AddTodoView */ = { + isa = PBXGroup; + children = ( + 63C5ECB92AC2FE9A003904C4 /* AddTodoViewController.swift */, + ); + path = AddTodoView; + sourceTree = ""; + }; C7431EF925F51E1D0094C4CF = { isa = PBXGroup; children = ( @@ -54,12 +163,13 @@ C7431F0425F51E1D0094C4CF /* ProjectManager */ = { isa = PBXGroup; children = ( - C7431F0525F51E1D0094C4CF /* AppDelegate.swift */, - C7431F0725F51E1D0094C4CF /* SceneDelegate.swift */, - C7431F0925F51E1D0094C4CF /* MainViewController.swift */, - C7431F0E25F51E1E0094C4CF /* Assets.xcassets */, - C7431F1025F51E1E0094C4CF /* LaunchScreen.storyboard */, + 63CC816F2AC7F59700E6355F /* UseCase */, + 63C5ECBB2AC45243003904C4 /* Model */, + 63C5ECB22AC1944F003904C4 /* App */, + 63C5ECB32AC19459003904C4 /* Controller */, + 63C5ECB42AC19464003904C4 /* View */, C7431F1325F51E1E0094C4CF /* Info.plist */, + C7431F0E25F51E1E0094C4CF /* Assets.xcassets */, ); path = ProjectManager; sourceTree = ""; @@ -133,9 +243,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 63CC81752AC95D7D00E6355F /* TitleItem.swift in Sources */, + 63C5ECCA2AC46CDB003904C4 /* ProjectManager.swift in Sources */, + 63C5ECCD2AC46F83003904C4 /* DataManager.swift in Sources */, + 63CC816E2AC6D8CA00E6355F /* CellTitleNamespace.swift in Sources */, C7431F0A25F51E1D0094C4CF /* MainViewController.swift in Sources */, + 63CC81782AC9665900E6355F /* MainViewModel.swift in Sources */, + 63C5ECB62AC1949A003904C4 /* ListTitleCell.swift in Sources */, + 63C5ECBA2AC2FE9A003904C4 /* AddTodoViewController.swift in Sources */, C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */, C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */, + 63CC81602AC6AA5B00E6355F /* ReuseIdentifier.swift in Sources */, + 63CC81712AC7F5B600E6355F /* MainViewControllerUseCase.swift in Sources */, + 63C5ECB82AC2B957003904C4 /* DescriptionCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ProjectManager/ProjectManager/AppDelegate.swift b/ProjectManager/ProjectManager/App/AppDelegate.swift similarity index 100% rename from ProjectManager/ProjectManager/AppDelegate.swift rename to ProjectManager/ProjectManager/App/AppDelegate.swift diff --git a/ProjectManager/ProjectManager/SceneDelegate.swift b/ProjectManager/ProjectManager/App/SceneDelegate.swift similarity index 87% rename from ProjectManager/ProjectManager/SceneDelegate.swift rename to ProjectManager/ProjectManager/App/SceneDelegate.swift index 702cf323c..24edcb5d6 100644 --- a/ProjectManager/ProjectManager/SceneDelegate.swift +++ b/ProjectManager/ProjectManager/App/SceneDelegate.swift @@ -13,9 +13,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } + let dataManager: DataManagerProtocol = DataManager() + let useCase: MainViewControllerUseCase = MainViewControllerUseCaseImplementation() + let viewModel = MainViewModel(useCase: useCase) window = UIWindow(windowScene: windowScene) - let mainViewController = MainViewController() + let mainViewController = MainViewController(dataManager: dataManager, useCase: useCase, viewModel: viewModel) let navigationController = UINavigationController(rootViewController: mainViewController) window?.rootViewController = navigationController window?.makeKeyAndVisible() diff --git a/ProjectManager/ProjectManager/Controller/AddTodoView/AddTodoViewController.swift b/ProjectManager/ProjectManager/Controller/AddTodoView/AddTodoViewController.swift new file mode 100644 index 000000000..439752c19 --- /dev/null +++ b/ProjectManager/ProjectManager/Controller/AddTodoView/AddTodoViewController.swift @@ -0,0 +1,183 @@ +// +// AddTodoViewController.swift +// ProjectManager +// +// Created by Hemg on 2023/09/26. +// + +import UIKit + +protocol AddTodoDelegate: AnyObject { + func didAddTodoItem(title: String, body: String, date: Date) + func didEditTodoItem(title: String, body: String, date: Date, index: Int) +} + +final class AddTodoViewController: UIViewController { + private let titleTextField: UITextField = { + let textField = UITextField() + textField.translatesAutoresizingMaskIntoConstraints = false + textField.font = .preferredFont(forTextStyle: .title1) + textField.placeholder = "Title" + textField.layer.borderColor = UIColor.lightGray.cgColor + textField.layer.borderWidth = 1.0 + + return textField + }() + + private let bodyTextView: UITextView = { + let textView = UITextView() + textView.translatesAutoresizingMaskIntoConstraints = false + textView.font = .preferredFont(forTextStyle: .body) + textView.text = "여기는 할일 내용 입력하는 곳입니다." + textView.textColor = .placeholderText + textView.layer.borderColor = UIColor.lightGray.cgColor + textView.layer.borderWidth = 1.0 + + return textView + }() + + private let datePicker: UIDatePicker = { + let datePicker = UIDatePicker() + datePicker.translatesAutoresizingMaskIntoConstraints = false + datePicker.preferredDatePickerStyle = .wheels + datePicker.datePickerMode = .date + + return datePicker + }() + + weak var delegate: AddTodoDelegate? + private var todoItems: ProjectManager? + private var isNew: Bool + + init() { + self.isNew = true + super.init(nibName: nil, bundle: nil) + } + + init(todoItems: ProjectManager?) { + self.isNew = false + self.todoItems = todoItems + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + setUpViewController() + setUpBarButtonItem() + configureUI() + setUpViewLayout() + setUpItemValues() + } + + private func setUpViewController() { + view.backgroundColor = .systemBackground + title = "TODO" + bodyTextView.delegate = self + } + + private func setUpBarButtonItem() { + let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(doneButton)) + navigationItem.rightBarButtonItem = doneButton + + if isNew == true { + let cancelButton = UIBarButtonItem(title: "Cancel", style: .done, target: self, action: #selector(cancelButton)) + navigationItem.leftBarButtonItem = cancelButton + } else { + let editButton = UIBarButtonItem(title: "Edit", style: .done, target: self, action: #selector(editButton)) + navigationItem.leftBarButtonItem = editButton + } + } + + private func setUpItemValues() { + if let todoItems = todoItems { + titleTextField.text = todoItems.title + bodyTextView.text = todoItems.body + datePicker.date = todoItems.date + } + } + + @objc private func doneButton() { + setUpItemText() + dismiss(animated: true) + } + + @objc private func cancelButton() { + dismiss(animated: true) + } + + @objc private func editButton() { + setUpItemText() + dismiss(animated: true) + } + + private func configureUI() { + view.addSubview(titleTextField) + view.addSubview(bodyTextView) + view.addSubview(datePicker) + } + + private func setUpViewLayout() { + NSLayoutConstraint.activate([ + titleTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + titleTextField.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 4), + titleTextField.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -4), + + datePicker.topAnchor.constraint(equalTo: titleTextField.bottomAnchor, constant: 4), + datePicker.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 4), + datePicker.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -4), + + bodyTextView.topAnchor.constraint(equalTo: datePicker.bottomAnchor, constant: 4), + bodyTextView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 4), + bodyTextView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -4), + bodyTextView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -4) + ]) + } + + private func setUpItemText() { + if isNew == false { + let date = datePicker.date + guard let titleText = titleTextField.text, + let bodyText = bodyTextView.text else { return } + todoItems?.title = titleText + todoItems?.body = bodyText + todoItems?.date = date + + delegate?.didEditTodoItem(title: titleText, body: bodyText, date: date, index: 0) + } else { + let date = datePicker.date + guard let titleText = titleTextField.text, + let bodyText = bodyTextView.text else { return } + + delegate?.didAddTodoItem(title: titleText, body: bodyText, date: date) + } + } +} + +extension AddTodoViewController: UITextViewDelegate { + func textViewDidBeginEditing(_ textView: UITextView) { + if textView.text == "여기는 할일 내용 입력하는 곳입니다." { + textView.text = "" + textView.textColor = .label + } + } + + func textViewDidEndEditing(_ textView: UITextView) { + if textView.text.isEmpty { + textView.text = "여기는 할일 내용 입력하는 곳입니다." + textView.textColor = .placeholderText + } + } + + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + let currentText = textView.text ?? "" + guard let stringRange = Range(range, in: currentText) else { return false } + let changedText = currentText.replacingCharacters(in: stringRange, with: text) + + return changedText.count <= 999 + } +} diff --git a/ProjectManager/ProjectManager/Controller/MainView/MainViewController.swift b/ProjectManager/ProjectManager/Controller/MainView/MainViewController.swift new file mode 100644 index 000000000..266f0fb9b --- /dev/null +++ b/ProjectManager/ProjectManager/Controller/MainView/MainViewController.swift @@ -0,0 +1,325 @@ +// +// ProjectManager - MainViewController.swift +// Created by yagom. +// Copyright © yagom. All rights reserved. +// + +import UIKit + +final class MainViewController: UIViewController { + private let todoTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .systemGray5 + + return tableView + }() + + private let doingTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .systemGray5 + + return tableView + }() + + private let doneTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .systemGray5 + + return tableView + }() + + private let stackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .horizontal + stackView.alignment = .fill + stackView.distribution = .fillEqually + stackView.spacing = 12 + stackView.backgroundColor = .systemGray5 + + return stackView + }() + + private let dataManager: DataManagerProtocol + private var useCase: MainViewControllerUseCase + private let viewModel: MainViewModel + + init(dataManager: DataManagerProtocol, useCase: MainViewControllerUseCase, viewModel: MainViewModel) { + self.dataManager = dataManager + self.useCase = useCase + 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() + + setUpViewController() + setUpBarButtonItem() + configureUI() + setUpTableViewLayout() + setUpTableView() + addPressGesture(to: [todoTableView, doingTableView, doneTableView]) + } + + private func setUpViewController() { + view.backgroundColor = .systemBackground + title = "Project Manager" + } + + private func configureUI() { + stackView.addArrangedSubview(todoTableView) + stackView.addArrangedSubview(doingTableView) + stackView.addArrangedSubview(doneTableView) + + view.addSubview(stackView) + } + + private func setUpBarButtonItem() { + let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButton)) + navigationItem.rightBarButtonItem = addButton + } + + private func setUpTableViewLayout() { + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + ]) + } + + private func setUpTableView() { + viewModel.configureTableView(todoTableView, dataSourceAndDelegate: self) + viewModel.configureTableView(doingTableView, dataSourceAndDelegate: self) + viewModel.configureTableView(doneTableView, dataSourceAndDelegate: self) + } +} + +// MARK: Action +extension MainViewController { + private func setUpTableViewReloadData() { + todoTableView.reloadData() + doingTableView.reloadData() + doneTableView.reloadData() + } + + private func createMoveToStateAction(_ selectedCell: ProjectManager, state: TitleItem) -> UIAlertAction { + return UIAlertAction(title: state.title, style: .default) { [weak self] _ in + self?.viewModel.performMoveToState(selectedCell, state: state) + self?.setUpTableViewReloadData() + } + } + + private func addPressGesture(to tableViews: [UITableView]) { + for tableView in tableViews { + let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handlePress(_:))) + longPressGestureRecognizer.cancelsTouchesInView = true + tableView.addGestureRecognizer(longPressGestureRecognizer) + } + } + + @objc private func addButton() { + let addTODOView = AddTodoViewController() + let navigationController = UINavigationController(rootViewController: addTODOView) + let backgroundView = UIView(frame: view.bounds) + backgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.5) + addTODOView.view.sendSubviewToBack(backgroundView) + addTODOView.delegate = self + + present(navigationController, animated: true) + } + + @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { + if gestureRecognizer.state == .began { + let point = gestureRecognizer.location(in: gestureRecognizer.view) + if let tableView = gestureRecognizer.view as? UITableView, + let indexPath = tableView.indexPathForRow(at: point) { + let selectedCell: ProjectManager + switch (tableView, indexPath.section) { + case (todoTableView, 1): + selectedCell = useCase.todoItems[indexPath.row] + case (doingTableView, 1): + selectedCell = useCase.doingItems[indexPath.row] + case (doneTableView, 1): + selectedCell = useCase.doneItems[indexPath.row] + default: + return + } + + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + switch tableView { + case todoTableView: + alertController.addAction(createMoveToStateAction(selectedCell, state: .doing)) + alertController.addAction(createMoveToStateAction(selectedCell, state: .done)) + case doingTableView: + alertController.addAction(createMoveToStateAction(selectedCell, state: .todo)) + alertController.addAction(createMoveToStateAction(selectedCell, state: .done)) + case doneTableView: + alertController.addAction(createMoveToStateAction(selectedCell, state: .todo)) + alertController.addAction(createMoveToStateAction(selectedCell, state: .doing)) + default: + break + } + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + alertController.addAction(cancelAction) + + if let popoverPresentationController = alertController.popoverPresentationController { + popoverPresentationController.sourceView = tableView + popoverPresentationController.sourceRect = tableView.rectForRow(at: indexPath) + } + + present(alertController, animated: true) + } + } + } +} + +// MARK: UITableViewDataSource +extension MainViewController: UITableViewDataSource { + func numberOfSections(in tableView: UITableView) -> Int { + return 2 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch (tableView, section) { + case (todoTableView, 0), (doingTableView, 0), (doneTableView, 0): + return 1 + case (todoTableView, 1): + return useCase.todoItems.count + case (doingTableView, 1): + return useCase.doingItems.count + case (doneTableView, 1): + return useCase.doneItems.count + default: + return 0 + } + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let listCell = tableView.dequeueReusableCell(withIdentifier: ReuseIdentifier.listTitleCell, for: indexPath) as? ListTitleCell else { return UITableViewCell() } + + guard let descriptionCell = todoTableView.dequeueReusableCell(withIdentifier: ReuseIdentifier.descriptionCell, for: indexPath) as? DescriptionCell else { return UITableViewCell() } + + switch (tableView, indexPath.section) { + case (todoTableView, 0): + listCell.setModel(title: CellTitleNamespace.todo, count: useCase.todoItems.count) + listCell.backgroundColor = .systemGray5 + return listCell + case (todoTableView, 1): + let todoItem = useCase.todoItems[indexPath.row] + descriptionCell.setModel(title: todoItem.title, body: todoItem.body, date: todoItem.date) + descriptionCell.isUserInteractionEnabled = true + return descriptionCell + case (doingTableView, 0): + listCell.setModel(title: CellTitleNamespace.doing, count: useCase.doingItems.count) + listCell.backgroundColor = .systemGray5 + return listCell + case (doingTableView, 1): + let doingItem = useCase.doingItems[indexPath.row] + descriptionCell.setModel(title: doingItem.title, body: doingItem.body, date: doingItem.date) + descriptionCell.isUserInteractionEnabled = true + return descriptionCell + case (doneTableView, 0): + listCell.setModel(title: CellTitleNamespace.done, count: useCase.doneItems.count) + listCell.backgroundColor = .systemGray5 + return listCell + case (doneTableView, 1): + let doneItem = useCase.doneItems[indexPath.row] + descriptionCell.setModel(title: doneItem.title, body: doneItem.body, date: doneItem.date) + descriptionCell.isUserInteractionEnabled = true + return descriptionCell + default: + return UITableViewCell() + } + } +} + +// MARK: UITableViewDelegate +extension MainViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + guard indexPath.section == 1 else { return } + var selectedTodoData: ProjectManager + + switch tableView { + case todoTableView: + selectedTodoData = useCase.todoItems[indexPath.row] + case doingTableView: + selectedTodoData = useCase.doingItems[indexPath.row] + case doneTableView: + selectedTodoData = useCase.doneItems[indexPath.row] + default: + return + } + + let addTodoView = AddTodoViewController(todoItems: selectedTodoData) + let navigationController = UINavigationController(rootViewController: addTodoView) + + addTodoView.delegate = self + present(navigationController, animated: true) + } + + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let delete = UIContextualAction(style: .normal, title: "Delete") { [weak self] (_, _, completionHandler) in + guard let self else { return } + guard indexPath.section == 1 else { return } + var deletedItem: ProjectManager? + switch tableView { + case self.todoTableView: + deletedItem = self.useCase.todoItems.remove(at: indexPath.row) + case self.doingTableView: + deletedItem = self.useCase.doingItems.remove(at: indexPath.row) + case self.doneTableView: + deletedItem = self.useCase.doneItems.remove(at: indexPath.row) + default: + return + } + + guard let deletedItem = deletedItem else { return } + self.dataManager.deleteTodoItem(deletedItem) + tableView.reloadData() + completionHandler(true) + } + + delete.backgroundColor = .systemRed + delete.image = UIImage(systemName: "trash.fill") + + return UISwipeActionsConfiguration(actions: [delete]) + } +} + +extension MainViewController: AddTodoDelegate { + func didAddTodoItem(title: String, body: String, date: Date) { + dataManager.addTodoItem(title: title, body: body, date: date) + let newTodoItem = ProjectManager(title: title, body: body, date: date) + useCase.todoItems.append(newTodoItem) + todoTableView.reloadData() + } + + func didEditTodoItem(title: String, body: String, date: Date, index: Int) { + switch index { + case 0.. + + + + + + + + \ No newline at end of file diff --git a/ProjectManager/ProjectManager/UseCase/MainViewControllerUseCase.swift b/ProjectManager/ProjectManager/UseCase/MainViewControllerUseCase.swift new file mode 100644 index 000000000..29ebd80a9 --- /dev/null +++ b/ProjectManager/ProjectManager/UseCase/MainViewControllerUseCase.swift @@ -0,0 +1,67 @@ +// +// MainViewControllerUseCase.swift +// ProjectManager +// +// Created by Hemg on 2023/09/30. +// + +import UIKit + +protocol MainViewControllerUseCase { + var todoItems: [ProjectManager] { get set } + var doingItems: [ProjectManager] { get set } + var doneItems: [ProjectManager] { get set } + func moveToDoing(_ item: ProjectManager) + func moveToDone(_ item: ProjectManager) + func moveToTodo(_ item: ProjectManager) + func updateItems(_ items: [ProjectManager]?, title: String, body: String, date: Date, index: Int) -> [ProjectManager] +} + +final class MainViewControllerUseCaseImplementation: MainViewControllerUseCase { + var todoItems = [ProjectManager]() + var doingItems = [ProjectManager]() + var doneItems = [ProjectManager]() + + func moveToDoing(_ item: ProjectManager) { + if let index = todoItems.firstIndex(where: { $0 == item }) { + todoItems.remove(at: index) + } else if let index = doneItems.firstIndex(where: { $0 == item }) { + doneItems.remove(at: index) + } + + doingItems.append(item) + } + + func moveToDone(_ item: ProjectManager) { + if let index = todoItems.firstIndex(where: { $0 == item }) { + todoItems.remove(at: index) + } else if let index = doingItems.firstIndex(where: { $0 == item }) { + doingItems.remove(at: index) + } + + doneItems.append(item) + } + + func moveToTodo(_ item: ProjectManager) { + if let index = doingItems.firstIndex(where: { $0 == item }) { + doingItems.remove(at: index) + } else if let index = doneItems.firstIndex(where: { $0 == item }) { + doneItems.remove(at: index) + } + + todoItems.append(item) + } + + func updateItems(_ items: [ProjectManager]?, title: String, body: String, date: Date, index: Int) -> [ProjectManager] { + guard var mutableItems = items, + index >= 0 && index < mutableItems.count else { + return items ?? [] + } + + mutableItems[index].title = title + mutableItems[index].body = body + mutableItems[index].date = date + + return mutableItems + } +} diff --git a/ProjectManager/ProjectManager/Base.lproj/LaunchScreen.storyboard b/ProjectManager/ProjectManager/View/Base.lproj/LaunchScreen.storyboard similarity index 57% rename from ProjectManager/ProjectManager/Base.lproj/LaunchScreen.storyboard rename to ProjectManager/ProjectManager/View/Base.lproj/LaunchScreen.storyboard index 865e9329f..17f10a541 100644 --- a/ProjectManager/ProjectManager/Base.lproj/LaunchScreen.storyboard +++ b/ProjectManager/ProjectManager/View/Base.lproj/LaunchScreen.storyboard @@ -1,8 +1,11 @@ - - + + + - + + + @@ -11,10 +14,10 @@ - + - + @@ -22,4 +25,9 @@ + + + + + diff --git a/ProjectManager/ProjectManager/View/DescriptionCell.swift b/ProjectManager/ProjectManager/View/DescriptionCell.swift new file mode 100644 index 000000000..29503ca2a --- /dev/null +++ b/ProjectManager/ProjectManager/View/DescriptionCell.swift @@ -0,0 +1,90 @@ +// +// DescriptionCell.swift +// ProjectManager +// +// Created by Hemg on 2023/09/26. +// + +import UIKit + +final class DescriptionCell: UITableViewCell { + private let titleLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .title1) + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + + private let bodyLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .body) + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 3 + + return label + }() + + private let dateLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .callout) + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + + override func awakeFromNib() { + super.awakeFromNib() + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + setUpLabel() + configureUI() + + } + + override func prepareForReuse() { + titleLabel.text = nil + bodyLabel.text = nil + dateLabel.text = nil + } + + private func setUpLabel() { + contentView.addSubview(titleLabel) + contentView.addSubview(bodyLabel) + contentView.addSubview(dateLabel) + } + + private func configureUI() { + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4), + titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 4), + titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4), + + bodyLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4), + bodyLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 4), + bodyLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4), + + dateLabel.topAnchor.constraint(equalTo: bodyLabel.bottomAnchor, constant: 4), + dateLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 4), + dateLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4), + dateLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4) + ]) + } + + func setModel(title: String, body: String, date: Date) { + titleLabel.text = title + bodyLabel.text = body + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let dateString = dateFormatter.string(from: date) + dateLabel.text = dateString + + let currentDate = Date() + if currentDate > date { + dateLabel.textColor = .red + } + } +} diff --git a/ProjectManager/ProjectManager/View/ListTitleCell.swift b/ProjectManager/ProjectManager/View/ListTitleCell.swift new file mode 100644 index 000000000..cbe98f1d4 --- /dev/null +++ b/ProjectManager/ProjectManager/View/ListTitleCell.swift @@ -0,0 +1,74 @@ +// +// ListTitleCell.swift +// ProjectManager +// +// Created by Hemg on 2023/09/25. +// + +import UIKit + +final class ListTitleCell: UITableViewCell { + private let titleLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .title1) + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 1 + + return label + }() + + private let countLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .callout) + label.translatesAutoresizingMaskIntoConstraints = false + label.backgroundColor = .black + label.textColor = .white + label.textAlignment = .center + + return label + }() + + override func awakeFromNib() { + super.awakeFromNib() + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + setUpLabel() + configureUI() + } + + override func prepareForReuse() { + titleLabel.text = nil + countLabel.text = nil + } + + private func setUpLabel() { + contentView.addSubview(titleLabel) + contentView.addSubview(countLabel) + } + + private func configureUI() { + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4), + titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 4), + titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4), + + countLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8), + countLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + countLabel.widthAnchor.constraint(equalToConstant: 22.5), + countLabel.heightAnchor.constraint(equalToConstant: 22.5) + ]) + } + + override func layoutSubviews() { + super.layoutSubviews() + countLabel.layer.cornerRadius = countLabel.bounds.width / 2 + countLabel.clipsToBounds = true + } + + func setModel(title: String, count: Int) { + titleLabel.text = title + countLabel.text = String(count) + } +}