From e2902c276c2fa128f3381d53be0fed7c666f03a9 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Sun, 24 Sep 2023 19:19:09 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat:=20CoreData=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectManager.xcodeproj/project.pbxproj | 57 +++++++++++++- .../xcschemes/ProjectManager.xcscheme | 77 +++++++++++++++++++ .../Model/CoreData/CoreDataError.swift | 32 ++++++++ .../Model/CoreData/CoreDataManager.swift | 62 +++++++++++++++ .../Model/CoreData/ToDo+CoreDataClass.swift | 15 ++++ .../CoreData/ToDo+CoreDataProperties.swift | 29 +++++++ .../ProjectManager/SceneDelegate.swift | 7 +- .../ToDo.xcdatamodel/contents | 11 +++ .../ToDoListViewController.swift | 28 +++++++ .../ProjectManager/ViewController.swift | 18 ----- 10 files changed, 312 insertions(+), 24 deletions(-) create mode 100644 ProjectManager/ProjectManager.xcodeproj/xcshareddata/xcschemes/ProjectManager.xcscheme create mode 100644 ProjectManager/ProjectManager/Model/CoreData/CoreDataError.swift create mode 100644 ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift create mode 100644 ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataClass.swift create mode 100644 ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift create mode 100644 ProjectManager/ProjectManager/ToDo.xcdatamodeld/ToDo.xcdatamodel/contents create mode 100644 ProjectManager/ProjectManager/ToDoListViewController.swift delete mode 100644 ProjectManager/ProjectManager/ViewController.swift diff --git a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj index c3dac7efb..c1814d683 100644 --- a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj +++ b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj @@ -12,18 +12,28 @@ BA6463102AB94FA10080E80D /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = BA64630F2AB94FA10080E80D /* FirebaseFirestore */; }; BA6463122AB94FA10080E80D /* FirebaseFirestoreCombine-Community in Frameworks */ = {isa = PBXBuildFile; productRef = BA6463112AB94FA10080E80D /* FirebaseFirestoreCombine-Community */; }; BA6463142AB94FA10080E80D /* FirebaseFirestoreSwift in Frameworks */ = {isa = PBXBuildFile; productRef = BA6463132AB94FA10080E80D /* FirebaseFirestoreSwift */; }; + BA6463582AC042A90080E80D /* ToDo.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BA6463562AC042A90080E80D /* ToDo.xcdatamodeld */; }; + BA64635F2AC043680080E80D /* ToDo+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64635D2AC043680080E80D /* ToDo+CoreDataClass.swift */; }; + BA6463602AC043680080E80D /* ToDo+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64635E2AC043680080E80D /* ToDo+CoreDataProperties.swift */; }; + BA6463642AC043F50080E80D /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463632AC043F50080E80D /* CoreDataManager.swift */; }; + BA6463662AC0440E0080E80D /* CoreDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463652AC0440E0080E80D /* CoreDataError.swift */; }; C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0525F51E1D0094C4CF /* AppDelegate.swift */; }; C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0725F51E1D0094C4CF /* SceneDelegate.swift */; }; - C7431F0A25F51E1D0094C4CF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0925F51E1D0094C4CF /* ViewController.swift */; }; + C7431F0A25F51E1D0094C4CF /* ToDoListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0925F51E1D0094C4CF /* ToDoListViewController.swift */; }; C7431F0F25F51E1E0094C4CF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C7431F0E25F51E1E0094C4CF /* Assets.xcassets */; }; C7431F1225F51E1E0094C4CF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C7431F1025F51E1E0094C4CF /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + BA6463572AC042A90080E80D /* ToDo.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = ToDo.xcdatamodel; sourceTree = ""; }; + BA64635D2AC043680080E80D /* ToDo+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ToDo+CoreDataClass.swift"; sourceTree = ""; }; + BA64635E2AC043680080E80D /* ToDo+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ToDo+CoreDataProperties.swift"; sourceTree = ""; }; + BA6463632AC043F50080E80D /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; + BA6463652AC0440E0080E80D /* CoreDataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataError.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 = ""; }; - C7431F0925F51E1D0094C4CF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + C7431F0925F51E1D0094C4CF /* ToDoListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListViewController.swift; sourceTree = ""; }; C7431F0E25F51E1E0094C4CF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C7431F1125F51E1E0094C4CF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; C7431F1325F51E1E0094C4CF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -45,6 +55,25 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + BA6463612AC043CE0080E80D /* Model */ = { + isa = PBXGroup; + children = ( + BA6463622AC043D30080E80D /* CoreData */, + ); + path = Model; + sourceTree = ""; + }; + BA6463622AC043D30080E80D /* CoreData */ = { + isa = PBXGroup; + children = ( + BA64635D2AC043680080E80D /* ToDo+CoreDataClass.swift */, + BA64635E2AC043680080E80D /* ToDo+CoreDataProperties.swift */, + BA6463632AC043F50080E80D /* CoreDataManager.swift */, + BA6463652AC0440E0080E80D /* CoreDataError.swift */, + ); + path = CoreData; + sourceTree = ""; + }; C7431EF925F51E1D0094C4CF = { isa = PBXGroup; children = ( @@ -64,12 +93,14 @@ C7431F0425F51E1D0094C4CF /* ProjectManager */ = { isa = PBXGroup; children = ( + BA6463612AC043CE0080E80D /* Model */, C7431F0525F51E1D0094C4CF /* AppDelegate.swift */, C7431F0725F51E1D0094C4CF /* SceneDelegate.swift */, - C7431F0925F51E1D0094C4CF /* ViewController.swift */, + C7431F0925F51E1D0094C4CF /* ToDoListViewController.swift */, C7431F0E25F51E1E0094C4CF /* Assets.xcassets */, C7431F1025F51E1E0094C4CF /* LaunchScreen.storyboard */, C7431F1325F51E1E0094C4CF /* Info.plist */, + BA6463562AC042A90080E80D /* ToDo.xcdatamodeld */, ); path = ProjectManager; sourceTree = ""; @@ -153,9 +184,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C7431F0A25F51E1D0094C4CF /* ViewController.swift in Sources */, + C7431F0A25F51E1D0094C4CF /* ToDoListViewController.swift in Sources */, C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */, C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */, + BA6463662AC0440E0080E80D /* CoreDataError.swift in Sources */, + BA6463602AC043680080E80D /* ToDo+CoreDataProperties.swift in Sources */, + BA64635F2AC043680080E80D /* ToDo+CoreDataClass.swift in Sources */, + BA6463642AC043F50080E80D /* CoreDataManager.swift in Sources */, + BA6463582AC042A90080E80D /* ToDo.xcdatamodeld in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -388,6 +424,19 @@ productName = FirebaseFirestoreSwift; }; /* End XCSwiftPackageProductDependency section */ + +/* Begin XCVersionGroup section */ + BA6463562AC042A90080E80D /* ToDo.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + BA6463572AC042A90080E80D /* ToDo.xcdatamodel */, + ); + currentVersion = BA6463572AC042A90080E80D /* ToDo.xcdatamodel */; + path = ToDo.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = C7431EFA25F51E1D0094C4CF /* Project object */; } diff --git a/ProjectManager/ProjectManager.xcodeproj/xcshareddata/xcschemes/ProjectManager.xcscheme b/ProjectManager/ProjectManager.xcodeproj/xcshareddata/xcschemes/ProjectManager.xcscheme new file mode 100644 index 000000000..c4576fde4 --- /dev/null +++ b/ProjectManager/ProjectManager.xcodeproj/xcshareddata/xcschemes/ProjectManager.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ProjectManager/ProjectManager/Model/CoreData/CoreDataError.swift b/ProjectManager/ProjectManager/Model/CoreData/CoreDataError.swift new file mode 100644 index 000000000..117debc57 --- /dev/null +++ b/ProjectManager/ProjectManager/Model/CoreData/CoreDataError.swift @@ -0,0 +1,32 @@ +// +// CoreDataError.swift +// ProjectManager +// +// Created by Min Hyun on 2023/09/24. +// + +enum CoreDataError: Error { + case dataNotFound + case saveFailure + case updateFailure + case deleteFailure + case unknown + + static let alertTitle = "데이터 오류" + + var alertMessage: String { + switch self { + case .dataNotFound: + return "데이터를 찾을 수 없습니다" + case .saveFailure: + return "저장에 실패하였습니다" + case .updateFailure: + return "수정에 실패하였습니다" + case .deleteFailure: + return "삭제에 실패하였습니다" + case .unknown: + return "알 수 없는 오류입니다" + } + } +} + diff --git a/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift b/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift new file mode 100644 index 000000000..2bcda6995 --- /dev/null +++ b/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift @@ -0,0 +1,62 @@ +// +// CoreDataManager.swift +// ProjectManager +// +// Created by Min Hyun on 2023/09/24. +// + +import CoreData + +class CoreDataManager { + let persistentContainer: NSPersistentContainer = { + let container = NSPersistentContainer(name: "ToDo") + container.loadPersistentStores(completionHandler: { (_, _) in }) + return container + }() + + func fetchData(entityName: String, predicate: NSPredicate? = nil, sort: String? = nil) throws -> [NSManagedObject] { + let request: NSFetchRequest = NSFetchRequest(entityName: entityName) + if let predicate { + request.predicate = predicate + } + if let sort { + let sorted = NSSortDescriptor(key: sort, ascending: true) + request.sortDescriptors = [sorted] + } + do { + let entities: [NSManagedObject] = try persistentContainer.viewContext.fetch(request) + return entities + } catch { + throw CoreDataError.dataNotFound + } + } + + @discardableResult + func createData(type: T.Type, values: [(key: String, value: Any?)]) throws -> T { + let newData = T(context: persistentContainer.viewContext) + return try updateData(entity: newData, values: values) + } + + @discardableResult + func updateData(entity: T, values: [(key: String, value: Any?)]) throws -> T { + values.forEach { entity.setValue($0.value, forKey: $0.key) } + try saveContext() + return entity + } + + func deleteData(entity: T) throws { + persistentContainer.viewContext.delete(entity) + try saveContext() + } + + func saveContext() throws { + let context = persistentContainer.viewContext + if context.hasChanges { +// do { +// try context.save() +// } catch { +// throw CoreDataError.saveFailure +// } + } + } +} diff --git a/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataClass.swift b/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataClass.swift new file mode 100644 index 000000000..485fbfbf5 --- /dev/null +++ b/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// ToDo+CoreDataClass.swift +// ProjectManager +// +// Created by Min Hyun on 2023/09/24. +// +// + +import Foundation +import CoreData + +@objc(ToDo) +public class ToDo: NSManagedObject { + +} diff --git a/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift b/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift new file mode 100644 index 000000000..912ecb8c8 --- /dev/null +++ b/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift @@ -0,0 +1,29 @@ +// +// ToDo+CoreDataProperties.swift +// ProjectManager +// +// Created by Min Hyun on 2023/09/24. +// +// + +import Foundation +import CoreData + + +extension ToDo { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "ToDo") + } + + @NSManaged public var title: String? + @NSManaged public var dueDate: Date? + @NSManaged public var body: String? + @NSManaged public var createdAt: Date? + @NSManaged public var status: String? + +} + +extension ToDo : Identifiable { + @NSManaged public var id: UUID? +} diff --git a/ProjectManager/ProjectManager/SceneDelegate.swift b/ProjectManager/ProjectManager/SceneDelegate.swift index 1e63da054..89a2b4973 100644 --- a/ProjectManager/ProjectManager/SceneDelegate.swift +++ b/ProjectManager/ProjectManager/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 } - + window = UIWindow(windowScene: windowScene) - window?.rootViewController = ViewController() + let coreDataManager = CoreDataManager() + let baseViewController = ToDoListViewController(coreDataManager) + let navigationViewController = UINavigationController(rootViewController: baseViewController) + window?.rootViewController = navigationViewController window?.makeKeyAndVisible() } diff --git a/ProjectManager/ProjectManager/ToDo.xcdatamodeld/ToDo.xcdatamodel/contents b/ProjectManager/ProjectManager/ToDo.xcdatamodeld/ToDo.xcdatamodel/contents new file mode 100644 index 000000000..44decf38f --- /dev/null +++ b/ProjectManager/ProjectManager/ToDo.xcdatamodeld/ToDo.xcdatamodel/contents @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ProjectManager/ProjectManager/ToDoListViewController.swift b/ProjectManager/ProjectManager/ToDoListViewController.swift new file mode 100644 index 000000000..7224e51f0 --- /dev/null +++ b/ProjectManager/ProjectManager/ToDoListViewController.swift @@ -0,0 +1,28 @@ +// +// ProjectManager - ToDoListViewController.swift +// Created by yagom. +// Copyright © yagom. All rights reserved. +// + +import UIKit + +class ToDoListViewController: UIViewController { + let coreDataManager: CoreDataManager + + init(_ coreDataManager: CoreDataManager) { + self.coreDataManager = coreDataManager + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + } + + +} + diff --git a/ProjectManager/ProjectManager/ViewController.swift b/ProjectManager/ProjectManager/ViewController.swift deleted file mode 100644 index 1eb291309..000000000 --- a/ProjectManager/ProjectManager/ViewController.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ProjectManager - ViewController.swift -// Created by yagom. -// Copyright © yagom. All rights reserved. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .systemBackground - } - - -} - From c2f2b34b00cc4b1ae5e43dc8c5d0cf88f212cec4 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Sun, 24 Sep 2023 20:01:35 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat:=20ViewModel=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectManager.xcodeproj/project.pbxproj | 46 +++++++- .../{ => App}/AppDelegate.swift | 0 .../{ => App}/SceneDelegate.swift | 0 .../{ => List}/ToDoListViewController.swift | 0 .../List/ToDoListViewModel.swift | 108 ++++++++++++++++++ .../ProjectManager/Model/Observable.swift | 26 +++++ .../ProjectManager/Model/ToDoStatus.swift | 23 ++++ .../Protocol/ViewModelProtocol.swift | 19 +++ 8 files changed, 219 insertions(+), 3 deletions(-) rename ProjectManager/ProjectManager/{ => App}/AppDelegate.swift (100%) rename ProjectManager/ProjectManager/{ => App}/SceneDelegate.swift (100%) rename ProjectManager/ProjectManager/{ => List}/ToDoListViewController.swift (100%) create mode 100644 ProjectManager/ProjectManager/List/ToDoListViewModel.swift create mode 100644 ProjectManager/ProjectManager/Model/Observable.swift create mode 100644 ProjectManager/ProjectManager/Model/ToDoStatus.swift create mode 100644 ProjectManager/ProjectManager/Protocol/ViewModelProtocol.swift diff --git a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj index c1814d683..19deebf68 100644 --- a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj +++ b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj @@ -17,6 +17,10 @@ BA6463602AC043680080E80D /* ToDo+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64635E2AC043680080E80D /* ToDo+CoreDataProperties.swift */; }; BA6463642AC043F50080E80D /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463632AC043F50080E80D /* CoreDataManager.swift */; }; BA6463662AC0440E0080E80D /* CoreDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463652AC0440E0080E80D /* CoreDataError.swift */; }; + BA6463682AC044D70080E80D /* ToDoStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463672AC044D70080E80D /* ToDoStatus.swift */; }; + BA64636A2AC045380080E80D /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463692AC045380080E80D /* Observable.swift */; }; + BA64636D2AC045700080E80D /* ViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64636C2AC045700080E80D /* ViewModelProtocol.swift */; }; + BA64636F2AC04A700080E80D /* ToDoListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64636E2AC04A700080E80D /* ToDoListViewModel.swift */; }; C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0525F51E1D0094C4CF /* AppDelegate.swift */; }; C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0725F51E1D0094C4CF /* SceneDelegate.swift */; }; C7431F0A25F51E1D0094C4CF /* ToDoListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0925F51E1D0094C4CF /* ToDoListViewController.swift */; }; @@ -30,6 +34,10 @@ BA64635E2AC043680080E80D /* ToDo+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ToDo+CoreDataProperties.swift"; sourceTree = ""; }; BA6463632AC043F50080E80D /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; BA6463652AC0440E0080E80D /* CoreDataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataError.swift; sourceTree = ""; }; + BA6463672AC044D70080E80D /* ToDoStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoStatus.swift; sourceTree = ""; }; + BA6463692AC045380080E80D /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; + BA64636C2AC045700080E80D /* ViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelProtocol.swift; sourceTree = ""; }; + BA64636E2AC04A700080E80D /* ToDoListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListViewModel.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 = ""; }; @@ -59,6 +67,8 @@ isa = PBXGroup; children = ( BA6463622AC043D30080E80D /* CoreData */, + BA6463672AC044D70080E80D /* ToDoStatus.swift */, + BA6463692AC045380080E80D /* Observable.swift */, ); path = Model; sourceTree = ""; @@ -74,6 +84,32 @@ path = CoreData; sourceTree = ""; }; + BA64636B2AC0455B0080E80D /* Protocol */ = { + isa = PBXGroup; + children = ( + BA64636C2AC045700080E80D /* ViewModelProtocol.swift */, + ); + path = Protocol; + sourceTree = ""; + }; + BA6463702AC04AAB0080E80D /* List */ = { + isa = PBXGroup; + children = ( + C7431F0925F51E1D0094C4CF /* ToDoListViewController.swift */, + BA64636E2AC04A700080E80D /* ToDoListViewModel.swift */, + ); + path = List; + sourceTree = ""; + }; + BA6463712AC04AC50080E80D /* App */ = { + isa = PBXGroup; + children = ( + C7431F0525F51E1D0094C4CF /* AppDelegate.swift */, + C7431F0725F51E1D0094C4CF /* SceneDelegate.swift */, + ); + path = App; + sourceTree = ""; + }; C7431EF925F51E1D0094C4CF = { isa = PBXGroup; children = ( @@ -93,10 +129,10 @@ C7431F0425F51E1D0094C4CF /* ProjectManager */ = { isa = PBXGroup; children = ( + BA6463712AC04AC50080E80D /* App */, + BA64636B2AC0455B0080E80D /* Protocol */, BA6463612AC043CE0080E80D /* Model */, - C7431F0525F51E1D0094C4CF /* AppDelegate.swift */, - C7431F0725F51E1D0094C4CF /* SceneDelegate.swift */, - C7431F0925F51E1D0094C4CF /* ToDoListViewController.swift */, + BA6463702AC04AAB0080E80D /* List */, C7431F0E25F51E1E0094C4CF /* Assets.xcassets */, C7431F1025F51E1E0094C4CF /* LaunchScreen.storyboard */, C7431F1325F51E1E0094C4CF /* Info.plist */, @@ -187,8 +223,12 @@ C7431F0A25F51E1D0094C4CF /* ToDoListViewController.swift in Sources */, C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */, C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */, + BA64636D2AC045700080E80D /* ViewModelProtocol.swift in Sources */, + BA64636F2AC04A700080E80D /* ToDoListViewModel.swift in Sources */, BA6463662AC0440E0080E80D /* CoreDataError.swift in Sources */, BA6463602AC043680080E80D /* ToDo+CoreDataProperties.swift in Sources */, + BA6463682AC044D70080E80D /* ToDoStatus.swift in Sources */, + BA64636A2AC045380080E80D /* Observable.swift in Sources */, BA64635F2AC043680080E80D /* ToDo+CoreDataClass.swift in Sources */, BA6463642AC043F50080E80D /* CoreDataManager.swift in Sources */, BA6463582AC042A90080E80D /* ToDo.xcdatamodeld in Sources */, 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 100% rename from ProjectManager/ProjectManager/SceneDelegate.swift rename to ProjectManager/ProjectManager/App/SceneDelegate.swift diff --git a/ProjectManager/ProjectManager/ToDoListViewController.swift b/ProjectManager/ProjectManager/List/ToDoListViewController.swift similarity index 100% rename from ProjectManager/ProjectManager/ToDoListViewController.swift rename to ProjectManager/ProjectManager/List/ToDoListViewController.swift diff --git a/ProjectManager/ProjectManager/List/ToDoListViewModel.swift b/ProjectManager/ProjectManager/List/ToDoListViewModel.swift new file mode 100644 index 000000000..6c81e02b9 --- /dev/null +++ b/ProjectManager/ProjectManager/List/ToDoListViewModel.swift @@ -0,0 +1,108 @@ +// +// ToDoViewModel.swift +// ProjectManager +// +// Created by Min Hyun on 2023/09/24. +// + +import CoreData + +class ToDoListViewModel: ViewModelProtocol { + var dataList: Observable<[ToDoStatus: [ToDo]]> + var errorMessage: Observable = Observable(nil) + var error: Observable = Observable(false) + let coreDataManager: CoreDataManager + + init(dataManager: CoreDataManager) { + coreDataManager = dataManager + dataList = Observable([:]) + addTestData() + } + + func fetchData() { + do { + try ToDoStatus.allCases.forEach { status in + let predicated = NSPredicate(format: "status == %@", status.name) + let filtered = try coreDataManager.fetchData(entityName:"ToDo", predicate: predicated, sort: "createdAt") + dataList.value[status] = filtered as? [ToDo] + } + } catch CoreDataError.dataNotFound { + setError(CoreDataError.dataNotFound.alertMessage) + } catch { + setError(CoreDataError.unknown.alertMessage) + } + } + + func createData(title: String?, body: String?, dueDate: Date?) { + guard let title, let body, let dueDate else { return } + let values: [(key: String, value: Any?)] = [ + (key: "id", value: UUID()), + (key: "title", value: title), + (key: "body", value: body), + (key: "dueDate", value: dueDate), + (key: "createdAt", value: Date()), + (key: "status", value: ToDoStatus.todo.name) + ] + do { + try coreDataManager.createData(type: ToDo.self, values: values) + fetchData() + } catch CoreDataError.saveFailure { + self.setError(CoreDataError.saveFailure.alertMessage) + } catch { + self.setError(CoreDataError.unknown.alertMessage) + } + } + + func updateData(_ entity: ToDo, title: String?, body: String?, dueDate: Date?) { + let values: [(key: String, value: Any?)] = [ + (key: "title", value: title), + (key: "body", value: body), + (key: "dueDate", value: dueDate) + ] + do { + try coreDataManager.updateData(entity: entity, values: values) + fetchData() + } catch CoreDataError.updateFailure { + self.setError(CoreDataError.updateFailure.alertMessage) + } catch { + self.setError(CoreDataError.unknown.alertMessage) + } + } + + func deleteData(_ entity: ToDo) { + do { + try coreDataManager.deleteData(entity: entity) + fetchData() + } catch CoreDataError.deleteFailure { + self.setError(CoreDataError.deleteFailure.alertMessage) + } catch { + self.setError(CoreDataError.unknown.alertMessage) + } + } + + func setError(_ message: String) { + self.errorMessage = Observable(message) + self.error = Observable(true) + } + + func addTestData() { + // 불러오기 테스트용 + do { + let values: [(key: String, value: Any?)] = [ + (key: "id", value: UUID()), + (key: "title", value: "테스트1"), + (key: "body", value: "테스트용입니다"), + (key: "dueDate", value: Date()), + (key: "createdAt", value: Date()), + (key: "status", value: ToDoStatus.todo.name) + ] + try coreDataManager.createData(type: ToDo.self, values: values) + try ToDoStatus.allCases.forEach { status in + let predicated = NSPredicate(format: "status == %@", status.name) + let filtered = try coreDataManager.fetchData(entityName:"ToDo", predicate: predicated, sort: "createdAt") + dataList.value[status] = filtered as? [ToDo] + } + } catch { + } + } +} diff --git a/ProjectManager/ProjectManager/Model/Observable.swift b/ProjectManager/ProjectManager/Model/Observable.swift new file mode 100644 index 000000000..c34b81f76 --- /dev/null +++ b/ProjectManager/ProjectManager/Model/Observable.swift @@ -0,0 +1,26 @@ +// +// Observable.swift +// ProjectManager +// +// Created by Min Hyun on 2023/09/24. +// + +class Observable { + private var listener: ((T) -> Void)? + + var value: T { + didSet { + listener?(value) + } + } + + init(_ value: T) { + self.value = value + } + + func bind(_ closure: @escaping (T) -> Void) { + closure(value) + listener = closure + } +} + diff --git a/ProjectManager/ProjectManager/Model/ToDoStatus.swift b/ProjectManager/ProjectManager/Model/ToDoStatus.swift new file mode 100644 index 000000000..b4c72d9d5 --- /dev/null +++ b/ProjectManager/ProjectManager/Model/ToDoStatus.swift @@ -0,0 +1,23 @@ +// +// ToDoStatus.swift +// ProjectManager +// +// Created by Min Hyun on 2023/09/24. +// + +enum ToDoStatus: Int, CaseIterable { + case todo = 0 + case doing = 1 + case done = 2 + + var name: String { + switch self { + case .todo: + return "TODO" + case .doing: + return "DOING" + case .done: + return "DONE" + } + } +} diff --git a/ProjectManager/ProjectManager/Protocol/ViewModelProtocol.swift b/ProjectManager/ProjectManager/Protocol/ViewModelProtocol.swift new file mode 100644 index 000000000..4cca415c0 --- /dev/null +++ b/ProjectManager/ProjectManager/Protocol/ViewModelProtocol.swift @@ -0,0 +1,19 @@ +// +// ViewModel.swift +// ProjectManager +// +// Created by Min Hyun on 2023/09/24. +// + +import Foundation + +protocol ViewModelProtocol { + var dataList: Observable<[ToDoStatus: [ToDo]]> { get set } + var errorMessage: Observable { get set } + var error: Observable { get set } + func fetchData() + func createData(title: String?, body: String?, dueDate: Date?) + func updateData(_ entity: ToDo, title: String?, body: String?, dueDate: Date?) + func deleteData(_ entity: ToDo) + func setError(_ message: String) +} From 927d71bd3fd91eac39fdb8474539125222cf67e2 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Sun, 24 Sep 2023 20:50:33 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20ViewController=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20View,=20Cell=20=EC=B6=94=EA=B0=80,=20?= =?UTF-8?q?=EB=B0=94=EC=9D=B8=EB=94=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectManager.xcodeproj/project.pbxproj | 24 ++++ .../ProjectManager/App/AppDelegate.swift | 2 +- .../ProjectManager/App/SceneDelegate.swift | 2 +- .../ProjectManager/Builder/AlertBuilder.swift | 81 +++++++++++++ .../List/ToDoListHeaderView.swift | 64 ++++++++++ .../ProjectManager/List/ToDoListView.swift | 114 ++++++++++++++++++ .../List/ToDoListViewCell.swift | 65 ++++++++++ .../List/ToDoListViewController.swift | 94 ++++++++++++++- .../List/ToDoListViewModel.swift | 47 +++++++- .../Model/CoreData/CoreDataError.swift | 2 +- .../Model/CoreData/CoreDataManager.swift | 2 +- .../Model/CoreData/ToDo+CoreDataClass.swift | 2 +- .../CoreData/ToDo+CoreDataProperties.swift | 2 +- .../ProjectManager/Model/Observable.swift | 2 +- .../ProjectManager/Model/ToDoStatus.swift | 6 +- .../Protocol/ViewModelProtocol.swift | 2 +- 16 files changed, 488 insertions(+), 23 deletions(-) create mode 100644 ProjectManager/ProjectManager/Builder/AlertBuilder.swift create mode 100644 ProjectManager/ProjectManager/List/ToDoListHeaderView.swift create mode 100644 ProjectManager/ProjectManager/List/ToDoListView.swift create mode 100644 ProjectManager/ProjectManager/List/ToDoListViewCell.swift diff --git a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj index 19deebf68..3f4650ba9 100644 --- a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj +++ b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj @@ -21,6 +21,10 @@ BA64636A2AC045380080E80D /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463692AC045380080E80D /* Observable.swift */; }; BA64636D2AC045700080E80D /* ViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64636C2AC045700080E80D /* ViewModelProtocol.swift */; }; BA64636F2AC04A700080E80D /* ToDoListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64636E2AC04A700080E80D /* ToDoListViewModel.swift */; }; + BA6463732AC04F590080E80D /* ToDoListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463722AC04F590080E80D /* ToDoListView.swift */; }; + BA6463752AC04F920080E80D /* ToDoListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463742AC04F920080E80D /* ToDoListHeaderView.swift */; }; + BA6463782AC04FE40080E80D /* AlertBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463772AC04FE40080E80D /* AlertBuilder.swift */; }; + BA64637A2AC050AD0080E80D /* ToDoListViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463792AC050AD0080E80D /* ToDoListViewCell.swift */; }; C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0525F51E1D0094C4CF /* AppDelegate.swift */; }; C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0725F51E1D0094C4CF /* SceneDelegate.swift */; }; C7431F0A25F51E1D0094C4CF /* ToDoListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0925F51E1D0094C4CF /* ToDoListViewController.swift */; }; @@ -38,6 +42,10 @@ BA6463692AC045380080E80D /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; BA64636C2AC045700080E80D /* ViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelProtocol.swift; sourceTree = ""; }; BA64636E2AC04A700080E80D /* ToDoListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListViewModel.swift; sourceTree = ""; }; + BA6463722AC04F590080E80D /* ToDoListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListView.swift; sourceTree = ""; }; + BA6463742AC04F920080E80D /* ToDoListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListHeaderView.swift; sourceTree = ""; }; + BA6463772AC04FE40080E80D /* AlertBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertBuilder.swift; sourceTree = ""; }; + BA6463792AC050AD0080E80D /* ToDoListViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListViewCell.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 = ""; }; @@ -97,6 +105,9 @@ children = ( C7431F0925F51E1D0094C4CF /* ToDoListViewController.swift */, BA64636E2AC04A700080E80D /* ToDoListViewModel.swift */, + BA6463722AC04F590080E80D /* ToDoListView.swift */, + BA6463742AC04F920080E80D /* ToDoListHeaderView.swift */, + BA6463792AC050AD0080E80D /* ToDoListViewCell.swift */, ); path = List; sourceTree = ""; @@ -110,6 +121,14 @@ path = App; sourceTree = ""; }; + BA6463762AC04FDA0080E80D /* Builder */ = { + isa = PBXGroup; + children = ( + BA6463772AC04FE40080E80D /* AlertBuilder.swift */, + ); + path = Builder; + sourceTree = ""; + }; C7431EF925F51E1D0094C4CF = { isa = PBXGroup; children = ( @@ -129,6 +148,7 @@ C7431F0425F51E1D0094C4CF /* ProjectManager */ = { isa = PBXGroup; children = ( + BA6463762AC04FDA0080E80D /* Builder */, BA6463712AC04AC50080E80D /* App */, BA64636B2AC0455B0080E80D /* Protocol */, BA6463612AC043CE0080E80D /* Model */, @@ -221,13 +241,17 @@ buildActionMask = 2147483647; files = ( C7431F0A25F51E1D0094C4CF /* ToDoListViewController.swift in Sources */, + BA64637A2AC050AD0080E80D /* ToDoListViewCell.swift in Sources */, C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */, C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */, + BA6463752AC04F920080E80D /* ToDoListHeaderView.swift in Sources */, BA64636D2AC045700080E80D /* ViewModelProtocol.swift in Sources */, BA64636F2AC04A700080E80D /* ToDoListViewModel.swift in Sources */, BA6463662AC0440E0080E80D /* CoreDataError.swift in Sources */, BA6463602AC043680080E80D /* ToDo+CoreDataProperties.swift in Sources */, BA6463682AC044D70080E80D /* ToDoStatus.swift in Sources */, + BA6463782AC04FE40080E80D /* AlertBuilder.swift in Sources */, + BA6463732AC04F590080E80D /* ToDoListView.swift in Sources */, BA64636A2AC045380080E80D /* Observable.swift in Sources */, BA64635F2AC043680080E80D /* ToDo+CoreDataClass.swift in Sources */, BA6463642AC043F50080E80D /* CoreDataManager.swift in Sources */, diff --git a/ProjectManager/ProjectManager/App/AppDelegate.swift b/ProjectManager/ProjectManager/App/AppDelegate.swift index 8df091a06..dd9892a56 100644 --- a/ProjectManager/ProjectManager/App/AppDelegate.swift +++ b/ProjectManager/ProjectManager/App/AppDelegate.swift @@ -2,7 +2,7 @@ // ProjectManager - AppDelegate.swift // Created by yagom. // Copyright © yagom. All rights reserved. -// +// Last modified by Max. import UIKit diff --git a/ProjectManager/ProjectManager/App/SceneDelegate.swift b/ProjectManager/ProjectManager/App/SceneDelegate.swift index 89a2b4973..4b9bab220 100644 --- a/ProjectManager/ProjectManager/App/SceneDelegate.swift +++ b/ProjectManager/ProjectManager/App/SceneDelegate.swift @@ -2,7 +2,7 @@ // ProjectManager - SceneDelegate.swift // Created by yagom. // Copyright © yagom. All rights reserved. -// +// Last modified by Max. import UIKit diff --git a/ProjectManager/ProjectManager/Builder/AlertBuilder.swift b/ProjectManager/ProjectManager/Builder/AlertBuilder.swift new file mode 100644 index 000000000..942bfc11b --- /dev/null +++ b/ProjectManager/ProjectManager/Builder/AlertBuilder.swift @@ -0,0 +1,81 @@ +// +// AlertBuilder.swift +// ProjectManager +// +// Created by Max on 2023/09/24. +// + +import UIKit + +final class AlertBuilder { + let alertController: UIAlertController + private let viewController: UIViewController + private var controllerTitle: String = "" + private var controllerMessage: String = "" + private var alertActions: [UIAlertAction] = [] + + init(viewController: UIViewController, prefferedStyle: UIAlertController.Style) { + self.viewController = viewController + self.alertController = UIAlertController(title: nil, message: nil, preferredStyle: prefferedStyle) + } + + func setControllerTitle(title: String) { + self.controllerTitle = title + } + + func setControllerMessage(message: String) { + self.controllerMessage = message + } + + func addAction(_ actionType: AlertActionType, action: ((UIAlertAction) -> Void)? = nil) { + let action = UIAlertAction(title: actionType.title, style: actionType.style, handler: action) + alertActions.append(action) + } + + @discardableResult + func show() -> Self { + alertController.title = controllerTitle + alertController.message = controllerMessage + alertActions.forEach { alertController.addAction($0) } + + viewController.present(alertController, animated: true) + + return self + } +} + +extension AlertBuilder { + enum AlertActionType { + case confirm + case cancel + case delete + case other(title: String, style: UIAlertAction.Style) + + var title: String { + switch self { + case .confirm: + return "확인" + case .cancel: + return "취소" + case .delete: + return "삭제" + case .other(let title, _): + return title + } + } + + var style: UIAlertAction.Style { + switch self { + case .cancel: + return .cancel + case .delete: + return .destructive + case .other(_, let style): + return style + default: + return .default + } + } + } +} + diff --git a/ProjectManager/ProjectManager/List/ToDoListHeaderView.swift b/ProjectManager/ProjectManager/List/ToDoListHeaderView.swift new file mode 100644 index 000000000..41186c9a2 --- /dev/null +++ b/ProjectManager/ProjectManager/List/ToDoListHeaderView.swift @@ -0,0 +1,64 @@ +// +// ToDoListHeaderView.swift +// ProjectManager +// +// Created by Max on 2023/09/24. +// + +import UIKit + +class ToDoListHeaderView: UIView { + private let status: ToDoStatus + + private let headerTitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .preferredFont(forTextStyle: .title1) + + return label + }() + + let totalCountLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .preferredFont(forTextStyle: .callout) + label.backgroundColor = .black + label.textColor = .white + label.clipsToBounds = true + label.layer.cornerRadius = 5 + label.layer.masksToBounds = true + + return label + }() + + init(_ status: ToDoStatus) { + self.status = status + super.init(frame: .init()) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + self.backgroundColor = UIColor(red: 0.95, green: 0.95, blue: 0.98, alpha: 1) + headerTitleLabel.text = status.name + self.addSubview(headerTitleLabel) + self.addSubview(totalCountLabel) + + NSLayoutConstraint.activate([ + headerTitleLabel.topAnchor.constraint(equalTo: self.topAnchor), + headerTitleLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 10), + headerTitleLabel.heightAnchor.constraint(equalTo: self.heightAnchor), + headerTitleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), + totalCountLabel.leftAnchor.constraint(equalTo: headerTitleLabel.rightAnchor, constant: 5), + totalCountLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor) + ]) + } + + func setupTotalCount(_ count: Int) { + totalCountLabel.text = "\(count)" + } +} + diff --git a/ProjectManager/ProjectManager/List/ToDoListView.swift b/ProjectManager/ProjectManager/List/ToDoListView.swift new file mode 100644 index 000000000..16f2d76e5 --- /dev/null +++ b/ProjectManager/ProjectManager/List/ToDoListView.swift @@ -0,0 +1,114 @@ +// +// ToDoListView.swift +// ProjectManager +// +// Created by Max on 2023/09/24. +// + +import UIKit + +class ToDoListView: UIView { + var viewController: ToDoListViewController? + private let status: ToDoStatus + private let headerView: ToDoListHeaderView + let today = Date().timeIntervalSinceReferenceDate + + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .long + formatter.timeStyle = .none + return formatter + }() + + private let tableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.tag = 0 + tableView.backgroundColor = UIColor(red: 0.95, green: 0.95, blue: 0.98, alpha: 1) + return tableView + }() + + init(_ status: ToDoStatus) { + self.status = status + self.headerView = ToDoListHeaderView(status) + super.init(frame: .init()) + + setupUI() + setupTableView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + self.backgroundColor = .systemBackground + self.addSubview(tableView) + + NSLayoutConstraint.activate([ + tableView.widthAnchor.constraint(equalTo: self.widthAnchor), + tableView.heightAnchor.constraint(equalTo: self.heightAnchor), + tableView.centerXAnchor.constraint(equalTo: self.centerXAnchor), + tableView.centerYAnchor.constraint(equalTo: self.centerYAnchor) + ]) + } + + private func setupTableView() { + tableView.dataSource = self + tableView.delegate = self + tableView.register(ToDoListViewCell.self, forCellReuseIdentifier: status.name) + } + + func reloadTableView() { + self.tableView.reloadData() + self.headerView.setupTotalCount(self.viewController?.viewModel.dataList.value[status]?.count ?? 0) + } +} + +extension ToDoListView: UITableViewDelegate, UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + viewController?.viewModel.dataList.value[status]?.count ?? 0 + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return headerView + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: status.name, + for: indexPath) as? + ToDoListViewCell else { return UITableViewCell() } + + guard let data = viewController?.viewModel.dataList.value[status]?[indexPath.row], + let title = data.title, + let dueDate = data.dueDate, + let body = data.body, + let status = data.status else { + return UITableViewCell() + } + + let isDone = status == ToDoStatus.done.name + let isPast = floor(today/86400) > floor(dueDate.timeIntervalSinceReferenceDate/86400) && !isDone + let date = dateFormatter.string(from: dueDate) + + cell.setupUI() + cell.setModel(title: title, date: date, body: body, isPast: isPast) + + return cell + } + + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> + UISwipeActionsConfiguration? { + let delete = UIContextualAction(style: .normal, title: "") { (_, _, success: @escaping (Bool) -> Void) in + guard let selectedData = self.viewController?.viewModel.dataList.value[self.status]?[indexPath.row] else { + return + } + self.viewController?.viewModel.deleteData(selectedData) + } + + delete.backgroundColor = .systemRed + delete.title = "Delete" + + return UISwipeActionsConfiguration(actions: [delete]) + } +} diff --git a/ProjectManager/ProjectManager/List/ToDoListViewCell.swift b/ProjectManager/ProjectManager/List/ToDoListViewCell.swift new file mode 100644 index 000000000..6897867b9 --- /dev/null +++ b/ProjectManager/ProjectManager/List/ToDoListViewCell.swift @@ -0,0 +1,65 @@ +// +// TableViewCell.swift +// ProjectManager +// +// Created by Max on 2023/09/24. +// + +import UIKit + +class ToDoListViewCell: UITableViewCell { + private let titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .preferredFont(forTextStyle: .headline) + return label + }() + + private let bodyLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .preferredFont(forTextStyle: .body) + label.numberOfLines = 3 + return label + }() + + private let dateLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .preferredFont(forTextStyle: .callout) + return label + }() + + func setupUI() { + self.backgroundColor = .white + contentView.addSubview(titleLabel) + contentView.addSubview(bodyLabel) + contentView.addSubview(dateLabel) + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), + titleLabel.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.9), + titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 10), + bodyLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5), + bodyLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 10), + dateLabel.topAnchor.constraint(equalTo: bodyLabel.bottomAnchor, constant: 5), + dateLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 10), + dateLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10) + ]) + } + + func setModel(title: String, date: String, body: String, isPast: Bool) { + titleLabel.text = title + bodyLabel.text = body + dateLabel.text = date + + if isPast { + dateLabel.textColor = .systemRed + } + } + + override func prepareForReuse() { + dateLabel.textColor = .black + } + +} diff --git a/ProjectManager/ProjectManager/List/ToDoListViewController.swift b/ProjectManager/ProjectManager/List/ToDoListViewController.swift index 7224e51f0..8c063a7c0 100644 --- a/ProjectManager/ProjectManager/List/ToDoListViewController.swift +++ b/ProjectManager/ProjectManager/List/ToDoListViewController.swift @@ -2,15 +2,32 @@ // ProjectManager - ToDoListViewController.swift // Created by yagom. // Copyright © yagom. All rights reserved. -// +// Last modified by Max. import UIKit class ToDoListViewController: UIViewController { - let coreDataManager: CoreDataManager + let viewModel: ToDoListViewModel - init(_ coreDataManager: CoreDataManager) { - self.coreDataManager = coreDataManager + private let stackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.alignment = .top + stackView.spacing = 10 + return stackView + }() + + private let toDoView: ToDoListView + private let doingView: ToDoListView + private let doneView: ToDoListView + + init(_ dataManager: CoreDataManager) { + self.viewModel = ToDoListViewModel(dataManager: dataManager) + self.toDoView = ToDoListView(.toDo) + self.doingView = ToDoListView(.doing) + self.doneView = ToDoListView(.done) super.init(nibName: nil, bundle: nil) } @@ -18,11 +35,76 @@ class ToDoListViewController: UIViewController { fatalError("init(coder:) has not been implemented") } + override func viewWillAppear(_ animated: Bool) { + readData() + } + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground + setupUI() + setupNavigationBar() + setupView() + setupBinding() } - - + + private func setupUI() { + let safeArea = view.safeAreaLayoutGuide + view.backgroundColor = .systemBackground + stackView.backgroundColor = UIColor(red: 0.8, green: 0.8, blue: 0.8, alpha: 1) + + stackView.addArrangedSubview(toDoView) + stackView.addArrangedSubview(doingView) + stackView.addArrangedSubview(doneView) + view.addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.widthAnchor.constraint(equalTo: safeArea.widthAnchor), + stackView.heightAnchor.constraint(equalTo: safeArea.heightAnchor), + stackView.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: safeArea.centerYAnchor), + toDoView.heightAnchor.constraint(equalTo: stackView.heightAnchor), + toDoView.centerYAnchor.constraint(equalTo: stackView.centerYAnchor), + doingView.heightAnchor.constraint(equalTo: stackView.heightAnchor), + doingView.centerYAnchor.constraint(equalTo: stackView.centerYAnchor), + doneView.heightAnchor.constraint(equalTo: stackView.heightAnchor), + doneView.centerYAnchor.constraint(equalTo: stackView.centerYAnchor) + ]) + } + + private func setupNavigationBar() { + self.title = "Project Manager" + let addToDo = UIAction(image: UIImage(systemName: "plus")) { _ in } + navigationItem.rightBarButtonItem = UIBarButtonItem(primaryAction: addToDo) + } + + private func setupView() { + toDoView.viewController = self + doingView.viewController = self + doneView.viewController = self + } + + private func readData() { + viewModel.fetchData() + } + + private func setupBinding() { + viewModel.dataList.bind { [weak self] _ in + guard let self else { return } + self.toDoView.reloadTableView() + self.doingView.reloadTableView() + self.doneView.reloadTableView() + } + + viewModel.errorMessage.bind { [weak self] _ in + guard let self, + let message = viewModel.errorMessage.value else { return } + let alertBuilder = AlertBuilder(viewController: self, prefferedStyle: .alert) + alertBuilder.setControllerTitle(title: CoreDataError.alertTitle) + alertBuilder.setControllerMessage(message: message) + alertBuilder.addAction(.confirm) + alertBuilder.show() + } + } } diff --git a/ProjectManager/ProjectManager/List/ToDoListViewModel.swift b/ProjectManager/ProjectManager/List/ToDoListViewModel.swift index 6c81e02b9..b2568603b 100644 --- a/ProjectManager/ProjectManager/List/ToDoListViewModel.swift +++ b/ProjectManager/ProjectManager/List/ToDoListViewModel.swift @@ -2,12 +2,14 @@ // ToDoViewModel.swift // ProjectManager // -// Created by Min Hyun on 2023/09/24. +// Created by Max on 2023/09/24. // import CoreData class ToDoListViewModel: ViewModelProtocol { + typealias DataFormat = (key: String, value: Any?) + var dataList: Observable<[ToDoStatus: [ToDo]]> var errorMessage: Observable = Observable(nil) var error: Observable = Observable(false) @@ -35,14 +37,15 @@ class ToDoListViewModel: ViewModelProtocol { func createData(title: String?, body: String?, dueDate: Date?) { guard let title, let body, let dueDate else { return } - let values: [(key: String, value: Any?)] = [ + let values: [DataFormat] = [ (key: "id", value: UUID()), (key: "title", value: title), (key: "body", value: body), (key: "dueDate", value: dueDate), (key: "createdAt", value: Date()), - (key: "status", value: ToDoStatus.todo.name) + (key: "status", value: ToDoStatus.toDo.name) ] + do { try coreDataManager.createData(type: ToDo.self, values: values) fetchData() @@ -88,15 +91,47 @@ class ToDoListViewModel: ViewModelProtocol { func addTestData() { // 불러오기 테스트용 do { - let values: [(key: String, value: Any?)] = [ + let toDoValues: [(key: String, value: Any?)] = [ (key: "id", value: UUID()), (key: "title", value: "테스트1"), (key: "body", value: "테스트용입니다"), (key: "dueDate", value: Date()), (key: "createdAt", value: Date()), - (key: "status", value: ToDoStatus.todo.name) + (key: "status", value: ToDoStatus.toDo.name) ] - try coreDataManager.createData(type: ToDo.self, values: values) + + let doingValues: [DataFormat] = [ + (key: "id", value: UUID()), + (key: "title", value: "테스트2"), + (key: "body", value: "테스트용입니다"), + (key: "dueDate", value: Date()), + (key: "createdAt", value: Date()), + (key: "status", value: ToDoStatus.doing.name) + ] + + let doneValues: [DataFormat] = [ + (key: "id", value: UUID()), + (key: "title", value: "테스트3"), + (key: "body", value: "테스트용입니다"), + (key: "dueDate", value: Date()), + (key: "createdAt", value: Date()), + (key: "status", value: ToDoStatus.done.name) + ] + + let doneValues2: [DataFormat] = [ + (key: "id", value: UUID()), + (key: "title", value: "테스트4"), + (key: "body", value: "테스트용입니다"), + (key: "dueDate", value: Date()), + (key: "createdAt", value: Date()), + (key: "status", value: ToDoStatus.done.name) + ] + + try coreDataManager.createData(type: ToDo.self, values: toDoValues) + try coreDataManager.createData(type: ToDo.self, values: doingValues) + try coreDataManager.createData(type: ToDo.self, values: doneValues) + try coreDataManager.createData(type: ToDo.self, values: doneValues2) + try ToDoStatus.allCases.forEach { status in let predicated = NSPredicate(format: "status == %@", status.name) let filtered = try coreDataManager.fetchData(entityName:"ToDo", predicate: predicated, sort: "createdAt") diff --git a/ProjectManager/ProjectManager/Model/CoreData/CoreDataError.swift b/ProjectManager/ProjectManager/Model/CoreData/CoreDataError.swift index 117debc57..5ac0ef834 100644 --- a/ProjectManager/ProjectManager/Model/CoreData/CoreDataError.swift +++ b/ProjectManager/ProjectManager/Model/CoreData/CoreDataError.swift @@ -2,7 +2,7 @@ // CoreDataError.swift // ProjectManager // -// Created by Min Hyun on 2023/09/24. +// Created by Max on 2023/09/24. // enum CoreDataError: Error { diff --git a/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift b/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift index 2bcda6995..cb6cdff22 100644 --- a/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift +++ b/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift @@ -2,7 +2,7 @@ // CoreDataManager.swift // ProjectManager // -// Created by Min Hyun on 2023/09/24. +// Created by Max on 2023/09/24. // import CoreData diff --git a/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataClass.swift b/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataClass.swift index 485fbfbf5..7b26ce832 100644 --- a/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataClass.swift +++ b/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataClass.swift @@ -2,7 +2,7 @@ // ToDo+CoreDataClass.swift // ProjectManager // -// Created by Min Hyun on 2023/09/24. +// Created by Max on 2023/09/24. // // diff --git a/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift b/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift index 912ecb8c8..a08539513 100644 --- a/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift +++ b/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift @@ -2,7 +2,7 @@ // ToDo+CoreDataProperties.swift // ProjectManager // -// Created by Min Hyun on 2023/09/24. +// Created by Max on 2023/09/24. // // diff --git a/ProjectManager/ProjectManager/Model/Observable.swift b/ProjectManager/ProjectManager/Model/Observable.swift index c34b81f76..4967fc4a5 100644 --- a/ProjectManager/ProjectManager/Model/Observable.swift +++ b/ProjectManager/ProjectManager/Model/Observable.swift @@ -2,7 +2,7 @@ // Observable.swift // ProjectManager // -// Created by Min Hyun on 2023/09/24. +// Created by Max on 2023/09/24. // class Observable { diff --git a/ProjectManager/ProjectManager/Model/ToDoStatus.swift b/ProjectManager/ProjectManager/Model/ToDoStatus.swift index b4c72d9d5..055099a21 100644 --- a/ProjectManager/ProjectManager/Model/ToDoStatus.swift +++ b/ProjectManager/ProjectManager/Model/ToDoStatus.swift @@ -2,17 +2,17 @@ // ToDoStatus.swift // ProjectManager // -// Created by Min Hyun on 2023/09/24. +// Created by Max on 2023/09/24. // enum ToDoStatus: Int, CaseIterable { - case todo = 0 + case toDo = 0 case doing = 1 case done = 2 var name: String { switch self { - case .todo: + case .toDo: return "TODO" case .doing: return "DOING" diff --git a/ProjectManager/ProjectManager/Protocol/ViewModelProtocol.swift b/ProjectManager/ProjectManager/Protocol/ViewModelProtocol.swift index 4cca415c0..58d3694a6 100644 --- a/ProjectManager/ProjectManager/Protocol/ViewModelProtocol.swift +++ b/ProjectManager/ProjectManager/Protocol/ViewModelProtocol.swift @@ -2,7 +2,7 @@ // ViewModel.swift // ProjectManager // -// Created by Min Hyun on 2023/09/24. +// Created by Max on 2023/09/24. // import Foundation From 71b6d8993e618c4ccfc9e605390b05c2c62a9044 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Sun, 24 Sep 2023 21:33:10 +0900 Subject: [PATCH 04/20] =?UTF-8?q?chore:=20=ED=8C=8C=EC=9D=BC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectManager.xcodeproj/project.pbxproj | 22 +++++++++++++------ .../List/{ => Cell}/ToDoListHeaderView.swift | 0 .../List/{ => Cell}/ToDoListViewCell.swift | 0 .../ProjectManager/List/ToDoListView.swift | 2 +- .../{ => ViewModel}/ToDoListViewModel.swift | 0 .../ViewModel}/ViewModelProtocol.swift | 0 6 files changed, 16 insertions(+), 8 deletions(-) rename ProjectManager/ProjectManager/List/{ => Cell}/ToDoListHeaderView.swift (100%) rename ProjectManager/ProjectManager/List/{ => Cell}/ToDoListViewCell.swift (100%) rename ProjectManager/ProjectManager/List/{ => ViewModel}/ToDoListViewModel.swift (100%) rename ProjectManager/ProjectManager/{Protocol => List/ViewModel}/ViewModelProtocol.swift (100%) diff --git a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj index 3f4650ba9..346ec1ee4 100644 --- a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj +++ b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj @@ -92,22 +92,22 @@ path = CoreData; sourceTree = ""; }; - BA64636B2AC0455B0080E80D /* Protocol */ = { + BA64636B2AC0455B0080E80D /* ViewModel */ = { isa = PBXGroup; children = ( BA64636C2AC045700080E80D /* ViewModelProtocol.swift */, + BA64636E2AC04A700080E80D /* ToDoListViewModel.swift */, ); - path = Protocol; + path = ViewModel; sourceTree = ""; }; BA6463702AC04AAB0080E80D /* List */ = { isa = PBXGroup; children = ( + BA64636B2AC0455B0080E80D /* ViewModel */, + BA64637B2AC0613D0080E80D /* Cell */, C7431F0925F51E1D0094C4CF /* ToDoListViewController.swift */, - BA64636E2AC04A700080E80D /* ToDoListViewModel.swift */, BA6463722AC04F590080E80D /* ToDoListView.swift */, - BA6463742AC04F920080E80D /* ToDoListHeaderView.swift */, - BA6463792AC050AD0080E80D /* ToDoListViewCell.swift */, ); path = List; sourceTree = ""; @@ -129,6 +129,15 @@ path = Builder; sourceTree = ""; }; + BA64637B2AC0613D0080E80D /* Cell */ = { + isa = PBXGroup; + children = ( + BA6463742AC04F920080E80D /* ToDoListHeaderView.swift */, + BA6463792AC050AD0080E80D /* ToDoListViewCell.swift */, + ); + path = Cell; + sourceTree = ""; + }; C7431EF925F51E1D0094C4CF = { isa = PBXGroup; children = ( @@ -148,9 +157,8 @@ C7431F0425F51E1D0094C4CF /* ProjectManager */ = { isa = PBXGroup; children = ( - BA6463762AC04FDA0080E80D /* Builder */, BA6463712AC04AC50080E80D /* App */, - BA64636B2AC0455B0080E80D /* Protocol */, + BA6463762AC04FDA0080E80D /* Builder */, BA6463612AC043CE0080E80D /* Model */, BA6463702AC04AAB0080E80D /* List */, C7431F0E25F51E1E0094C4CF /* Assets.xcassets */, diff --git a/ProjectManager/ProjectManager/List/ToDoListHeaderView.swift b/ProjectManager/ProjectManager/List/Cell/ToDoListHeaderView.swift similarity index 100% rename from ProjectManager/ProjectManager/List/ToDoListHeaderView.swift rename to ProjectManager/ProjectManager/List/Cell/ToDoListHeaderView.swift diff --git a/ProjectManager/ProjectManager/List/ToDoListViewCell.swift b/ProjectManager/ProjectManager/List/Cell/ToDoListViewCell.swift similarity index 100% rename from ProjectManager/ProjectManager/List/ToDoListViewCell.swift rename to ProjectManager/ProjectManager/List/Cell/ToDoListViewCell.swift diff --git a/ProjectManager/ProjectManager/List/ToDoListView.swift b/ProjectManager/ProjectManager/List/ToDoListView.swift index 16f2d76e5..458577d82 100644 --- a/ProjectManager/ProjectManager/List/ToDoListView.swift +++ b/ProjectManager/ProjectManager/List/ToDoListView.swift @@ -8,7 +8,7 @@ import UIKit class ToDoListView: UIView { - var viewController: ToDoListViewController? + weak var viewController: ToDoListViewController? private let status: ToDoStatus private let headerView: ToDoListHeaderView let today = Date().timeIntervalSinceReferenceDate diff --git a/ProjectManager/ProjectManager/List/ToDoListViewModel.swift b/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift similarity index 100% rename from ProjectManager/ProjectManager/List/ToDoListViewModel.swift rename to ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift diff --git a/ProjectManager/ProjectManager/Protocol/ViewModelProtocol.swift b/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift similarity index 100% rename from ProjectManager/ProjectManager/Protocol/ViewModelProtocol.swift rename to ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift From f2d87853813138c00577a32e5e6e75ff4b8f6dbf Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Mon, 25 Sep 2023 11:08:27 +0900 Subject: [PATCH 05/20] =?UTF-8?q?refactor:=20delegate=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectManager/List/ToDoListView.swift | 12 ++++++------ .../List/ToDoListViewController.swift | 14 +++++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/ProjectManager/ProjectManager/List/ToDoListView.swift b/ProjectManager/ProjectManager/List/ToDoListView.swift index 458577d82..891b0bd66 100644 --- a/ProjectManager/ProjectManager/List/ToDoListView.swift +++ b/ProjectManager/ProjectManager/List/ToDoListView.swift @@ -8,7 +8,7 @@ import UIKit class ToDoListView: UIView { - weak var viewController: ToDoListViewController? + weak var delegate: ToDoListViewDelegate? private let status: ToDoStatus private let headerView: ToDoListHeaderView let today = Date().timeIntervalSinceReferenceDate @@ -61,13 +61,13 @@ class ToDoListView: UIView { func reloadTableView() { self.tableView.reloadData() - self.headerView.setupTotalCount(self.viewController?.viewModel.dataList.value[status]?.count ?? 0) + self.headerView.setupTotalCount(self.delegate?.viewModel.dataList.value[status]?.count ?? 0) } } extension ToDoListView: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - viewController?.viewModel.dataList.value[status]?.count ?? 0 + delegate?.viewModel.dataList.value[status]?.count ?? 0 } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { @@ -79,7 +79,7 @@ extension ToDoListView: UITableViewDelegate, UITableViewDataSource { for: indexPath) as? ToDoListViewCell else { return UITableViewCell() } - guard let data = viewController?.viewModel.dataList.value[status]?[indexPath.row], + guard let data = delegate?.viewModel.dataList.value[status]?[indexPath.row], let title = data.title, let dueDate = data.dueDate, let body = data.body, @@ -100,10 +100,10 @@ extension ToDoListView: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let delete = UIContextualAction(style: .normal, title: "") { (_, _, success: @escaping (Bool) -> Void) in - guard let selectedData = self.viewController?.viewModel.dataList.value[self.status]?[indexPath.row] else { + guard let selectedData = self.delegate?.viewModel.dataList.value[self.status]?[indexPath.row] else { return } - self.viewController?.viewModel.deleteData(selectedData) + self.delegate?.viewModel.deleteData(selectedData) } delete.backgroundColor = .systemRed diff --git a/ProjectManager/ProjectManager/List/ToDoListViewController.swift b/ProjectManager/ProjectManager/List/ToDoListViewController.swift index 8c063a7c0..83c138cf4 100644 --- a/ProjectManager/ProjectManager/List/ToDoListViewController.swift +++ b/ProjectManager/ProjectManager/List/ToDoListViewController.swift @@ -6,8 +6,12 @@ import UIKit -class ToDoListViewController: UIViewController { - let viewModel: ToDoListViewModel +protocol ToDoListViewDelegate: AnyObject { + var viewModel: ToDoListViewModel { get set } +} + +class ToDoListViewController: UIViewController, ToDoListViewDelegate { + var viewModel: ToDoListViewModel private let stackView: UIStackView = { let stackView = UIStackView() @@ -79,9 +83,9 @@ class ToDoListViewController: UIViewController { } private func setupView() { - toDoView.viewController = self - doingView.viewController = self - doneView.viewController = self + toDoView.delegate = self + doingView.delegate = self + doneView.delegate = self } private func readData() { From 58588f7b5300440005dfaa8b5ef4461c5b685510 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Mon, 25 Sep 2023 19:18:20 +0900 Subject: [PATCH 06/20] =?UTF-8?q?refactor:=20CoreData=20=EC=96=B4=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectManager/List/ToDoListView.swift | 11 ++++++----- .../Model/CoreData/ToDo+CoreDataProperties.swift | 12 ++++++------ .../ToDo.xcdatamodeld/ToDo.xcdatamodel/contents | 10 +++++----- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/ProjectManager/ProjectManager/List/ToDoListView.swift b/ProjectManager/ProjectManager/List/ToDoListView.swift index 891b0bd66..ce828d2ce 100644 --- a/ProjectManager/ProjectManager/List/ToDoListView.swift +++ b/ProjectManager/ProjectManager/List/ToDoListView.swift @@ -79,14 +79,15 @@ extension ToDoListView: UITableViewDelegate, UITableViewDataSource { for: indexPath) as? ToDoListViewCell else { return UITableViewCell() } - guard let data = delegate?.viewModel.dataList.value[status]?[indexPath.row], - let title = data.title, - let dueDate = data.dueDate, - let body = data.body, - let status = data.status else { + guard let data = delegate?.viewModel.dataList.value[status]?[indexPath.row] else { return UITableViewCell() } + let title = data.title + let dueDate = data.dueDate + let body = data.body + let status = data.status + let isDone = status == ToDoStatus.done.name let isPast = floor(today/86400) > floor(dueDate.timeIntervalSinceReferenceDate/86400) && !isDone let date = dateFormatter.string(from: dueDate) diff --git a/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift b/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift index a08539513..4788fca0a 100644 --- a/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift +++ b/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift @@ -16,14 +16,14 @@ extension ToDo { return NSFetchRequest(entityName: "ToDo") } - @NSManaged public var title: String? - @NSManaged public var dueDate: Date? - @NSManaged public var body: String? - @NSManaged public var createdAt: Date? - @NSManaged public var status: String? + @NSManaged public var title: String + @NSManaged public var dueDate: Date + @NSManaged public var body: String + @NSManaged public var createdAt: Date + @NSManaged public var status: String } extension ToDo : Identifiable { - @NSManaged public var id: UUID? + @NSManaged public var id: UUID } diff --git a/ProjectManager/ProjectManager/ToDo.xcdatamodeld/ToDo.xcdatamodel/contents b/ProjectManager/ProjectManager/ToDo.xcdatamodeld/ToDo.xcdatamodel/contents index 44decf38f..58fa67964 100644 --- a/ProjectManager/ProjectManager/ToDo.xcdatamodeld/ToDo.xcdatamodel/contents +++ b/ProjectManager/ProjectManager/ToDo.xcdatamodeld/ToDo.xcdatamodel/contents @@ -1,11 +1,11 @@ - - - - + + + + - + \ No newline at end of file From 1d1e8a88ba80299692586b7b9bb22e640a314ea9 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Mon, 25 Sep 2023 19:19:25 +0900 Subject: [PATCH 07/20] =?UTF-8?q?refactor:=20ToDoStatus=20rawValue=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ProjectManager/ProjectManager/Model/ToDoStatus.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ProjectManager/ProjectManager/Model/ToDoStatus.swift b/ProjectManager/ProjectManager/Model/ToDoStatus.swift index 055099a21..85d7de9d5 100644 --- a/ProjectManager/ProjectManager/Model/ToDoStatus.swift +++ b/ProjectManager/ProjectManager/Model/ToDoStatus.swift @@ -5,10 +5,10 @@ // Created by Max on 2023/09/24. // -enum ToDoStatus: Int, CaseIterable { - case toDo = 0 - case doing = 1 - case done = 2 +enum ToDoStatus: CaseIterable { + case toDo + case doing + case done var name: String { switch self { From 65f175795e2303f9da77b5ac28dc8e3a43f488f2 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Mon, 25 Sep 2023 19:27:23 +0900 Subject: [PATCH 08/20] =?UTF-8?q?refactor:=20CoreDataManager=20struct?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95,=20ViewModel=20Error=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../List/ViewModel/ToDoListViewModel.swift | 24 +++++++++---------- .../List/ViewModel/ViewModelProtocol.swift | 4 ++-- .../Model/CoreData/CoreDataManager.swift | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift b/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift index b2568603b..b4aa4d53f 100644 --- a/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift +++ b/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift @@ -12,7 +12,7 @@ class ToDoListViewModel: ViewModelProtocol { var dataList: Observable<[ToDoStatus: [ToDo]]> var errorMessage: Observable = Observable(nil) - var error: Observable = Observable(false) + var error: Observable = Observable(nil) let coreDataManager: CoreDataManager init(dataManager: CoreDataManager) { @@ -29,9 +29,9 @@ class ToDoListViewModel: ViewModelProtocol { dataList.value[status] = filtered as? [ToDo] } } catch CoreDataError.dataNotFound { - setError(CoreDataError.dataNotFound.alertMessage) + setError(CoreDataError.dataNotFound) } catch { - setError(CoreDataError.unknown.alertMessage) + setError(CoreDataError.unknown) } } @@ -50,9 +50,9 @@ class ToDoListViewModel: ViewModelProtocol { try coreDataManager.createData(type: ToDo.self, values: values) fetchData() } catch CoreDataError.saveFailure { - self.setError(CoreDataError.saveFailure.alertMessage) + self.setError(CoreDataError.saveFailure) } catch { - self.setError(CoreDataError.unknown.alertMessage) + self.setError(CoreDataError.unknown) } } @@ -66,9 +66,9 @@ class ToDoListViewModel: ViewModelProtocol { try coreDataManager.updateData(entity: entity, values: values) fetchData() } catch CoreDataError.updateFailure { - self.setError(CoreDataError.updateFailure.alertMessage) + self.setError(CoreDataError.updateFailure) } catch { - self.setError(CoreDataError.unknown.alertMessage) + self.setError(CoreDataError.unknown) } } @@ -77,15 +77,15 @@ class ToDoListViewModel: ViewModelProtocol { try coreDataManager.deleteData(entity: entity) fetchData() } catch CoreDataError.deleteFailure { - self.setError(CoreDataError.deleteFailure.alertMessage) + self.setError(CoreDataError.deleteFailure) } catch { - self.setError(CoreDataError.unknown.alertMessage) + self.setError(CoreDataError.unknown) } } - func setError(_ message: String) { - self.errorMessage = Observable(message) - self.error = Observable(true) + func setError(_ error: CoreDataError) { + self.errorMessage = Observable(error.alertMessage) + self.error = Observable(error) } func addTestData() { diff --git a/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift b/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift index 58d3694a6..77a5ece15 100644 --- a/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift +++ b/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift @@ -10,10 +10,10 @@ import Foundation protocol ViewModelProtocol { var dataList: Observable<[ToDoStatus: [ToDo]]> { get set } var errorMessage: Observable { get set } - var error: Observable { get set } + var error: Observable { get set } func fetchData() func createData(title: String?, body: String?, dueDate: Date?) func updateData(_ entity: ToDo, title: String?, body: String?, dueDate: Date?) func deleteData(_ entity: ToDo) - func setError(_ message: String) + func setError(_ error: CoreDataError) } diff --git a/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift b/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift index cb6cdff22..8f8848fbc 100644 --- a/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift +++ b/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift @@ -7,7 +7,7 @@ import CoreData -class CoreDataManager { +struct CoreDataManager { let persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "ToDo") container.loadPersistentStores(completionHandler: { (_, _) in }) From 6365a6c1366dd6d7889cce470672972a7ab84a14 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Mon, 25 Sep 2023 19:32:25 +0900 Subject: [PATCH 09/20] =?UTF-8?q?refactor:=20AlertBuilder=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20Cell=20prepareForReuse=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ProjectManager/ProjectManager/Builder/AlertBuilder.swift | 9 ++------- .../ProjectManager/List/Cell/ToDoListViewCell.swift | 2 ++ .../ProjectManager/List/ToDoListViewController.swift | 3 ++- .../List/ViewModel/ToDoListViewModel.swift | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/ProjectManager/ProjectManager/Builder/AlertBuilder.swift b/ProjectManager/ProjectManager/Builder/AlertBuilder.swift index 942bfc11b..fdddaa515 100644 --- a/ProjectManager/ProjectManager/Builder/AlertBuilder.swift +++ b/ProjectManager/ProjectManager/Builder/AlertBuilder.swift @@ -9,13 +9,11 @@ import UIKit final class AlertBuilder { let alertController: UIAlertController - private let viewController: UIViewController private var controllerTitle: String = "" private var controllerMessage: String = "" private var alertActions: [UIAlertAction] = [] init(viewController: UIViewController, prefferedStyle: UIAlertController.Style) { - self.viewController = viewController self.alertController = UIAlertController(title: nil, message: nil, preferredStyle: prefferedStyle) } @@ -32,15 +30,12 @@ final class AlertBuilder { alertActions.append(action) } - @discardableResult - func show() -> Self { + func makeAlertController() -> UIAlertController { alertController.title = controllerTitle alertController.message = controllerMessage alertActions.forEach { alertController.addAction($0) } - viewController.present(alertController, animated: true) - - return self + return alertController } } diff --git a/ProjectManager/ProjectManager/List/Cell/ToDoListViewCell.swift b/ProjectManager/ProjectManager/List/Cell/ToDoListViewCell.swift index 6897867b9..a7180f088 100644 --- a/ProjectManager/ProjectManager/List/Cell/ToDoListViewCell.swift +++ b/ProjectManager/ProjectManager/List/Cell/ToDoListViewCell.swift @@ -59,6 +59,8 @@ class ToDoListViewCell: UITableViewCell { } override func prepareForReuse() { + titleLabel.textColor = .black + bodyLabel.textColor = .black dateLabel.textColor = .black } diff --git a/ProjectManager/ProjectManager/List/ToDoListViewController.swift b/ProjectManager/ProjectManager/List/ToDoListViewController.swift index 83c138cf4..8a94e6613 100644 --- a/ProjectManager/ProjectManager/List/ToDoListViewController.swift +++ b/ProjectManager/ProjectManager/List/ToDoListViewController.swift @@ -107,7 +107,8 @@ class ToDoListViewController: UIViewController, ToDoListViewDelegate { alertBuilder.setControllerTitle(title: CoreDataError.alertTitle) alertBuilder.setControllerMessage(message: message) alertBuilder.addAction(.confirm) - alertBuilder.show() + let alertController = alertBuilder.makeAlertController() + present(alertController, animated: true) } } } diff --git a/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift b/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift index b4aa4d53f..eefdc52d4 100644 --- a/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift +++ b/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift @@ -7,7 +7,7 @@ import CoreData -class ToDoListViewModel: ViewModelProtocol { +final class ToDoListViewModel: ViewModelProtocol { typealias DataFormat = (key: String, value: Any?) var dataList: Observable<[ToDoStatus: [ToDo]]> From 10da8eaa89c1e6d3e9bb9beff77218d9f382448c Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Mon, 25 Sep 2023 19:43:42 +0900 Subject: [PATCH 10/20] =?UTF-8?q?refactor:=20error=20title=20=EC=97=B0?= =?UTF-8?q?=EC=82=B0=20=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectManager/List/ToDoListViewController.swift | 8 ++++---- .../ProjectManager/Model/CoreData/CoreDataError.swift | 9 +++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/ProjectManager/ProjectManager/List/ToDoListViewController.swift b/ProjectManager/ProjectManager/List/ToDoListViewController.swift index 8a94e6613..9a9ba2a4e 100644 --- a/ProjectManager/ProjectManager/List/ToDoListViewController.swift +++ b/ProjectManager/ProjectManager/List/ToDoListViewController.swift @@ -100,12 +100,12 @@ class ToDoListViewController: UIViewController, ToDoListViewDelegate { self.doneView.reloadTableView() } - viewModel.errorMessage.bind { [weak self] _ in + viewModel.error.bind { [weak self] _ in guard let self, - let message = viewModel.errorMessage.value else { return } + let error = viewModel.error.value else { return } let alertBuilder = AlertBuilder(viewController: self, prefferedStyle: .alert) - alertBuilder.setControllerTitle(title: CoreDataError.alertTitle) - alertBuilder.setControllerMessage(message: message) + alertBuilder.setControllerTitle(title: error.alertTitle) + alertBuilder.setControllerMessage(message: error.alertMessage) alertBuilder.addAction(.confirm) let alertController = alertBuilder.makeAlertController() present(alertController, animated: true) diff --git a/ProjectManager/ProjectManager/Model/CoreData/CoreDataError.swift b/ProjectManager/ProjectManager/Model/CoreData/CoreDataError.swift index 5ac0ef834..3ea32fb21 100644 --- a/ProjectManager/ProjectManager/Model/CoreData/CoreDataError.swift +++ b/ProjectManager/ProjectManager/Model/CoreData/CoreDataError.swift @@ -11,8 +11,13 @@ enum CoreDataError: Error { case updateFailure case deleteFailure case unknown - - static let alertTitle = "데이터 오류" + + var alertTitle: String { + switch self { + default: + return "데이터 오류" + } + } var alertMessage: String { switch self { From 85e929048d195afd205c58e5d22dcc39d808d896 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Mon, 25 Sep 2023 19:57:08 +0900 Subject: [PATCH 11/20] =?UTF-8?q?refactor:=20CoreDataManager=EC=97=90=20Ne?= =?UTF-8?q?sted=20=EA=B5=AC=EC=A1=B0=EC=B2=B4=20Value=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../List/ViewModel/ToDoListViewModel.swift | 94 ++++++++++--------- .../Model/CoreData/CoreDataManager.swift | 16 +++- 2 files changed, 64 insertions(+), 46 deletions(-) diff --git a/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift b/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift index eefdc52d4..a02a667ad 100644 --- a/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift +++ b/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift @@ -8,17 +8,17 @@ import CoreData final class ToDoListViewModel: ViewModelProtocol { - typealias DataFormat = (key: String, value: Any?) - - var dataList: Observable<[ToDoStatus: [ToDo]]> + var dataList: Observable<[ToDoStatus: [ToDo]]> = Observable([:]) var errorMessage: Observable = Observable(nil) var error: Observable = Observable(nil) let coreDataManager: CoreDataManager init(dataManager: CoreDataManager) { coreDataManager = dataManager - dataList = Observable([:]) + +#if DEBUG addTestData() +#endif } func fetchData() { @@ -37,13 +37,13 @@ final class ToDoListViewModel: ViewModelProtocol { func createData(title: String?, body: String?, dueDate: Date?) { guard let title, let body, let dueDate else { return } - let values: [DataFormat] = [ - (key: "id", value: UUID()), - (key: "title", value: title), - (key: "body", value: body), - (key: "dueDate", value: dueDate), - (key: "createdAt", value: Date()), - (key: "status", value: ToDoStatus.toDo.name) + let values: [CoreDataManager.Value] = [ + CoreDataManager.Value(key: "id", value: UUID()), + CoreDataManager.Value(key: "title", value: title), + CoreDataManager.Value(key: "body", value: body), + CoreDataManager.Value(key: "dueDate", value: dueDate), + CoreDataManager.Value(key: "createdAt", value: Date()), + CoreDataManager.Value(key: "status", value: ToDoStatus.toDo.name) ] do { @@ -57,10 +57,10 @@ final class ToDoListViewModel: ViewModelProtocol { } func updateData(_ entity: ToDo, title: String?, body: String?, dueDate: Date?) { - let values: [(key: String, value: Any?)] = [ - (key: "title", value: title), - (key: "body", value: body), - (key: "dueDate", value: dueDate) + let values: [CoreDataManager.Value] = [ + CoreDataManager.Value(key: "title", value: title), + CoreDataManager.Value(key: "body", value: body), + CoreDataManager.Value(key: "dueDate", value: dueDate) ] do { try coreDataManager.updateData(entity: entity, values: values) @@ -87,44 +87,46 @@ final class ToDoListViewModel: ViewModelProtocol { self.errorMessage = Observable(error.alertMessage) self.error = Observable(error) } - +} + +extension ToDoListViewModel { func addTestData() { // 불러오기 테스트용 do { - let toDoValues: [(key: String, value: Any?)] = [ - (key: "id", value: UUID()), - (key: "title", value: "테스트1"), - (key: "body", value: "테스트용입니다"), - (key: "dueDate", value: Date()), - (key: "createdAt", value: Date()), - (key: "status", value: ToDoStatus.toDo.name) + let toDoValues: [CoreDataManager.Value] = [ + CoreDataManager.Value(key: "id", value: UUID()), + CoreDataManager.Value(key: "title", value: "테스트"), + CoreDataManager.Value(key: "body", value: "테스트용입니다"), + CoreDataManager.Value(key: "dueDate", value: Date()), + CoreDataManager.Value(key: "createdAt", value: Date()), + CoreDataManager.Value(key: "status", value: ToDoStatus.toDo.name) ] - let doingValues: [DataFormat] = [ - (key: "id", value: UUID()), - (key: "title", value: "테스트2"), - (key: "body", value: "테스트용입니다"), - (key: "dueDate", value: Date()), - (key: "createdAt", value: Date()), - (key: "status", value: ToDoStatus.doing.name) + let doingValues: [CoreDataManager.Value] = [ + CoreDataManager.Value(key: "id", value: UUID()), + CoreDataManager.Value(key: "title", value: "테스트2"), + CoreDataManager.Value(key: "body", value: "테스트용입니다2"), + CoreDataManager.Value(key: "dueDate", value: Date()), + CoreDataManager.Value(key: "createdAt", value: Date()), + CoreDataManager.Value(key: "status", value: ToDoStatus.doing.name) ] - let doneValues: [DataFormat] = [ - (key: "id", value: UUID()), - (key: "title", value: "테스트3"), - (key: "body", value: "테스트용입니다"), - (key: "dueDate", value: Date()), - (key: "createdAt", value: Date()), - (key: "status", value: ToDoStatus.done.name) + let doneValues: [CoreDataManager.Value] = [ + CoreDataManager.Value(key: "id", value: UUID()), + CoreDataManager.Value(key: "title", value: "테스트3"), + CoreDataManager.Value(key: "body", value: "테스트용입니다3"), + CoreDataManager.Value(key: "dueDate", value: Date()), + CoreDataManager.Value(key: "createdAt", value: Date()), + CoreDataManager.Value(key: "status", value: ToDoStatus.done.name) ] - let doneValues2: [DataFormat] = [ - (key: "id", value: UUID()), - (key: "title", value: "테스트4"), - (key: "body", value: "테스트용입니다"), - (key: "dueDate", value: Date()), - (key: "createdAt", value: Date()), - (key: "status", value: ToDoStatus.done.name) + let doneValues2: [CoreDataManager.Value] = [ + CoreDataManager.Value(key: "id", value: UUID()), + CoreDataManager.Value(key: "title", value: "테스트4"), + CoreDataManager.Value(key: "body", value: "테스트용입니다4"), + CoreDataManager.Value(key: "dueDate", value: Date()), + CoreDataManager.Value(key: "createdAt", value: Date()), + CoreDataManager.Value(key: "status", value: ToDoStatus.done.name) ] try coreDataManager.createData(type: ToDo.self, values: toDoValues) @@ -141,3 +143,7 @@ final class ToDoListViewModel: ViewModelProtocol { } } } + +extension ToDoListViewModel { + +} diff --git a/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift b/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift index 8f8848fbc..2b6513abc 100644 --- a/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift +++ b/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift @@ -32,13 +32,13 @@ struct CoreDataManager { } @discardableResult - func createData(type: T.Type, values: [(key: String, value: Any?)]) throws -> T { + func createData(type: T.Type, values: [Value]) throws -> T { let newData = T(context: persistentContainer.viewContext) return try updateData(entity: newData, values: values) } @discardableResult - func updateData(entity: T, values: [(key: String, value: Any?)]) throws -> T { + func updateData(entity: T, values: [Value]) throws -> T { values.forEach { entity.setValue($0.value, forKey: $0.key) } try saveContext() return entity @@ -60,3 +60,15 @@ struct CoreDataManager { } } } + +extension CoreDataManager { + struct Value { + let key: String + let value: Any? + + init(key: String, value: Any?) { + self.key = key + self.value = value + } + } +} From 35afc35a1e0410ef3867823494688e03896f573a Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Mon, 25 Sep 2023 20:42:34 +0900 Subject: [PATCH 12/20] =?UTF-8?q?refactor:=20ViewModel=EC=9D=98=20dataList?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC,=20handle=20error=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../List/Cell/ToDoListHeaderView.swift | 2 +- .../ProjectManager/List/ToDoListView.swift | 48 +++++----- .../List/ToDoListViewController.swift | 31 +++--- .../List/ViewModel/ToDoListViewModel.swift | 96 +++++++++++-------- .../List/ViewModel/ViewModelProtocol.swift | 7 +- .../Model/CoreData/CoreDataManager.swift | 7 -- .../ProjectManager/Model/ToDoStatus.swift | 19 +--- 7 files changed, 106 insertions(+), 104 deletions(-) diff --git a/ProjectManager/ProjectManager/List/Cell/ToDoListHeaderView.swift b/ProjectManager/ProjectManager/List/Cell/ToDoListHeaderView.swift index 41186c9a2..5c568d6fb 100644 --- a/ProjectManager/ProjectManager/List/Cell/ToDoListHeaderView.swift +++ b/ProjectManager/ProjectManager/List/Cell/ToDoListHeaderView.swift @@ -43,7 +43,7 @@ class ToDoListHeaderView: UIView { private func setupUI() { self.backgroundColor = UIColor(red: 0.95, green: 0.95, blue: 0.98, alpha: 1) - headerTitleLabel.text = status.name + headerTitleLabel.text = status.rawValue self.addSubview(headerTitleLabel) self.addSubview(totalCountLabel) diff --git a/ProjectManager/ProjectManager/List/ToDoListView.swift b/ProjectManager/ProjectManager/List/ToDoListView.swift index ce828d2ce..331dbaa6d 100644 --- a/ProjectManager/ProjectManager/List/ToDoListView.swift +++ b/ProjectManager/ProjectManager/List/ToDoListView.swift @@ -8,11 +8,22 @@ import UIKit class ToDoListView: UIView { - weak var delegate: ToDoListViewDelegate? + private let viewModel: ToDoListViewModel private let status: ToDoStatus private let headerView: ToDoListHeaderView let today = Date().timeIntervalSinceReferenceDate + var toDoEntities: [ToDo] { + switch status { + case .toDo: + return viewModel.toDoList.value + case .doing: + return viewModel.doingList.value + case .done: + return viewModel.doneList.value + } + } + private let dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long @@ -28,8 +39,9 @@ class ToDoListView: UIView { return tableView }() - init(_ status: ToDoStatus) { + init(_ status: ToDoStatus, viewModel: ToDoListViewModel) { self.status = status + self.viewModel = viewModel self.headerView = ToDoListHeaderView(status) super.init(frame: .init()) @@ -56,18 +68,18 @@ class ToDoListView: UIView { private func setupTableView() { tableView.dataSource = self tableView.delegate = self - tableView.register(ToDoListViewCell.self, forCellReuseIdentifier: status.name) + tableView.register(ToDoListViewCell.self, forCellReuseIdentifier: status.rawValue) } func reloadTableView() { self.tableView.reloadData() - self.headerView.setupTotalCount(self.delegate?.viewModel.dataList.value[status]?.count ?? 0) + self.headerView.setupTotalCount(self.toDoEntities.count) } } extension ToDoListView: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - delegate?.viewModel.dataList.value[status]?.count ?? 0 + toDoEntities.count } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { @@ -75,25 +87,17 @@ extension ToDoListView: UITableViewDelegate, UITableViewDataSource { } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: status.name, + guard let cell = tableView.dequeueReusableCell(withIdentifier: status.rawValue, for: indexPath) as? ToDoListViewCell else { return UITableViewCell() } - guard let data = delegate?.viewModel.dataList.value[status]?[indexPath.row] else { - return UITableViewCell() - } - - let title = data.title - let dueDate = data.dueDate - let body = data.body - let status = data.status - - let isDone = status == ToDoStatus.done.name - let isPast = floor(today/86400) > floor(dueDate.timeIntervalSinceReferenceDate/86400) && !isDone - let date = dateFormatter.string(from: dueDate) + let toDoEntity = toDoEntities[indexPath.row] + let isDone = toDoEntity.status == ToDoStatus.done.rawValue + let isPast = floor(today/86400) > floor(toDoEntity.dueDate.timeIntervalSinceReferenceDate/86400) && !isDone + let date = dateFormatter.string(from: toDoEntity.dueDate) cell.setupUI() - cell.setModel(title: title, date: date, body: body, isPast: isPast) + cell.setModel(title: toDoEntity.title, date: date, body: toDoEntity.body, isPast: isPast) return cell } @@ -101,10 +105,8 @@ extension ToDoListView: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let delete = UIContextualAction(style: .normal, title: "") { (_, _, success: @escaping (Bool) -> Void) in - guard let selectedData = self.delegate?.viewModel.dataList.value[self.status]?[indexPath.row] else { - return - } - self.delegate?.viewModel.deleteData(selectedData) + let selectedEntity = self.toDoEntities[indexPath.row] + self.viewModel.deleteData(selectedEntity) } delete.backgroundColor = .systemRed diff --git a/ProjectManager/ProjectManager/List/ToDoListViewController.swift b/ProjectManager/ProjectManager/List/ToDoListViewController.swift index 9a9ba2a4e..ff6563804 100644 --- a/ProjectManager/ProjectManager/List/ToDoListViewController.swift +++ b/ProjectManager/ProjectManager/List/ToDoListViewController.swift @@ -6,11 +6,7 @@ import UIKit -protocol ToDoListViewDelegate: AnyObject { - var viewModel: ToDoListViewModel { get set } -} - -class ToDoListViewController: UIViewController, ToDoListViewDelegate { +class ToDoListViewController: UIViewController { var viewModel: ToDoListViewModel private let stackView: UIStackView = { @@ -29,9 +25,9 @@ class ToDoListViewController: UIViewController, ToDoListViewDelegate { init(_ dataManager: CoreDataManager) { self.viewModel = ToDoListViewModel(dataManager: dataManager) - self.toDoView = ToDoListView(.toDo) - self.doingView = ToDoListView(.doing) - self.doneView = ToDoListView(.done) + self.toDoView = ToDoListView(.toDo, viewModel: viewModel) + self.doingView = ToDoListView(.doing, viewModel: viewModel) + self.doneView = ToDoListView(.done, viewModel: viewModel) super.init(nibName: nil, bundle: nil) } @@ -48,7 +44,6 @@ class ToDoListViewController: UIViewController, ToDoListViewDelegate { view.backgroundColor = .systemBackground setupUI() setupNavigationBar() - setupView() setupBinding() } @@ -82,21 +77,23 @@ class ToDoListViewController: UIViewController, ToDoListViewDelegate { navigationItem.rightBarButtonItem = UIBarButtonItem(primaryAction: addToDo) } - private func setupView() { - toDoView.delegate = self - doingView.delegate = self - doneView.delegate = self - } - private func readData() { - viewModel.fetchData() + ToDoStatus.allCases.forEach { viewModel.fetchData($0) } } private func setupBinding() { - viewModel.dataList.bind { [weak self] _ in + viewModel.toDoList.bind { [weak self] _ in guard let self else { return } self.toDoView.reloadTableView() + } + + viewModel.doingList.bind { [weak self] _ in + guard let self else { return } self.doingView.reloadTableView() + } + + viewModel.doneList.bind { [weak self] _ in + guard let self else { return } self.doneView.reloadTableView() } diff --git a/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift b/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift index a02a667ad..d7fcf79c9 100644 --- a/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift +++ b/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift @@ -8,7 +8,9 @@ import CoreData final class ToDoListViewModel: ViewModelProtocol { - var dataList: Observable<[ToDoStatus: [ToDo]]> = Observable([:]) + var toDoList: Observable<[ToDo]> = Observable([]) + var doingList: Observable<[ToDo]> = Observable([]) + var doneList: Observable<[ToDo]> = Observable([]) var errorMessage: Observable = Observable(nil) var error: Observable = Observable(nil) let coreDataManager: CoreDataManager @@ -20,18 +22,24 @@ final class ToDoListViewModel: ViewModelProtocol { addTestData() #endif } - - func fetchData() { + + func fetchData(_ status: ToDoStatus) { do { - try ToDoStatus.allCases.forEach { status in - let predicated = NSPredicate(format: "status == %@", status.name) - let filtered = try coreDataManager.fetchData(entityName:"ToDo", predicate: predicated, sort: "createdAt") - dataList.value[status] = filtered as? [ToDo] + let predicated = NSPredicate(format: "status == %@", status.rawValue) + let filtered = try coreDataManager.fetchData(entityName:"ToDo", predicate: predicated, sort: "createdAt") + + guard let result = filtered as? [ToDo] else { return } + + switch status { + case .toDo: + toDoList.value = result + case .doing: + doingList.value = result + case .done: + doneList.value = result } - } catch CoreDataError.dataNotFound { - setError(CoreDataError.dataNotFound) - } catch { - setError(CoreDataError.unknown) + } catch(let error) { + handle(error: error) } } @@ -43,20 +51,19 @@ final class ToDoListViewModel: ViewModelProtocol { CoreDataManager.Value(key: "body", value: body), CoreDataManager.Value(key: "dueDate", value: dueDate), CoreDataManager.Value(key: "createdAt", value: Date()), - CoreDataManager.Value(key: "status", value: ToDoStatus.toDo.name) + CoreDataManager.Value(key: "status", value: ToDoStatus.toDo.rawValue) ] do { try coreDataManager.createData(type: ToDo.self, values: values) - fetchData() - } catch CoreDataError.saveFailure { - self.setError(CoreDataError.saveFailure) - } catch { - self.setError(CoreDataError.unknown) + fetchData(.toDo) + } catch(let error) { + handle(error: error) } } func updateData(_ entity: ToDo, title: String?, body: String?, dueDate: Date?) { + guard let status = ToDoStatus(rawValue: entity.status) else { return } let values: [CoreDataManager.Value] = [ CoreDataManager.Value(key: "title", value: title), CoreDataManager.Value(key: "body", value: body), @@ -64,21 +71,27 @@ final class ToDoListViewModel: ViewModelProtocol { ] do { try coreDataManager.updateData(entity: entity, values: values) - fetchData() - } catch CoreDataError.updateFailure { - self.setError(CoreDataError.updateFailure) - } catch { - self.setError(CoreDataError.unknown) + fetchData(status) + } catch(let error) { + handle(error: error) } } func deleteData(_ entity: ToDo) { + guard let status = ToDoStatus(rawValue: entity.status) else { return } + do { try coreDataManager.deleteData(entity: entity) - fetchData() - } catch CoreDataError.deleteFailure { - self.setError(CoreDataError.deleteFailure) - } catch { + fetchData(status) + } catch(let error) { + handle(error: error) + } + } + + func handle(error: Error) { + if let coreDataError = error as? CoreDataError { + self.setError(coreDataError) + } else { self.setError(CoreDataError.unknown) } } @@ -99,7 +112,7 @@ extension ToDoListViewModel { CoreDataManager.Value(key: "body", value: "테스트용입니다"), CoreDataManager.Value(key: "dueDate", value: Date()), CoreDataManager.Value(key: "createdAt", value: Date()), - CoreDataManager.Value(key: "status", value: ToDoStatus.toDo.name) + CoreDataManager.Value(key: "status", value: ToDoStatus.toDo.rawValue) ] let doingValues: [CoreDataManager.Value] = [ @@ -108,7 +121,7 @@ extension ToDoListViewModel { CoreDataManager.Value(key: "body", value: "테스트용입니다2"), CoreDataManager.Value(key: "dueDate", value: Date()), CoreDataManager.Value(key: "createdAt", value: Date()), - CoreDataManager.Value(key: "status", value: ToDoStatus.doing.name) + CoreDataManager.Value(key: "status", value: ToDoStatus.doing.rawValue) ] let doneValues: [CoreDataManager.Value] = [ @@ -117,7 +130,7 @@ extension ToDoListViewModel { CoreDataManager.Value(key: "body", value: "테스트용입니다3"), CoreDataManager.Value(key: "dueDate", value: Date()), CoreDataManager.Value(key: "createdAt", value: Date()), - CoreDataManager.Value(key: "status", value: ToDoStatus.done.name) + CoreDataManager.Value(key: "status", value: ToDoStatus.done.rawValue) ] let doneValues2: [CoreDataManager.Value] = [ @@ -126,7 +139,7 @@ extension ToDoListViewModel { CoreDataManager.Value(key: "body", value: "테스트용입니다4"), CoreDataManager.Value(key: "dueDate", value: Date()), CoreDataManager.Value(key: "createdAt", value: Date()), - CoreDataManager.Value(key: "status", value: ToDoStatus.done.name) + CoreDataManager.Value(key: "status", value: ToDoStatus.done.rawValue) ] try coreDataManager.createData(type: ToDo.self, values: toDoValues) @@ -134,16 +147,21 @@ extension ToDoListViewModel { try coreDataManager.createData(type: ToDo.self, values: doneValues) try coreDataManager.createData(type: ToDo.self, values: doneValues2) - try ToDoStatus.allCases.forEach { status in - let predicated = NSPredicate(format: "status == %@", status.name) - let filtered = try coreDataManager.fetchData(entityName:"ToDo", predicate: predicated, sort: "createdAt") - dataList.value[status] = filtered as? [ToDo] - } + let toDoPredicated = NSPredicate(format: "status == %@", ToDoStatus.toDo.rawValue) + let toDofiltered = try coreDataManager.fetchData(entityName:"ToDo", predicate: toDoPredicated, sort: "createdAt") + let doingPredicated = NSPredicate(format: "status == %@", ToDoStatus.doing.rawValue) + let doingfiltered = try coreDataManager.fetchData(entityName:"ToDo", predicate: doingPredicated, sort: "createdAt") + let donePredicated = NSPredicate(format: "status == %@", ToDoStatus.done.rawValue) + let donefiltered = try coreDataManager.fetchData(entityName:"ToDo", predicate: donePredicated, sort: "createdAt") + + guard let toDoResult = toDofiltered as? [ToDo], + let doingResult = doingfiltered as? [ToDo], + let doneResult = donefiltered as? [ToDo] else { return } + + toDoList.value = toDoResult + doingList.value = doingResult + doneList.value = doneResult } catch { } } } - -extension ToDoListViewModel { - -} diff --git a/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift b/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift index 77a5ece15..b4dc3df97 100644 --- a/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift +++ b/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift @@ -8,12 +8,15 @@ import Foundation protocol ViewModelProtocol { - var dataList: Observable<[ToDoStatus: [ToDo]]> { get set } + var toDoList: Observable<[ToDo]> { get set } + var doingList: Observable<[ToDo]> { get set } + var doneList: Observable<[ToDo]>{ get set } var errorMessage: Observable { get set } var error: Observable { get set } - func fetchData() + func fetchData(_ status: ToDoStatus) func createData(title: String?, body: String?, dueDate: Date?) func updateData(_ entity: ToDo, title: String?, body: String?, dueDate: Date?) func deleteData(_ entity: ToDo) + func handel(error: Error) func setError(_ error: CoreDataError) } diff --git a/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift b/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift index 2b6513abc..9d0c82920 100644 --- a/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift +++ b/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift @@ -51,13 +51,6 @@ struct CoreDataManager { func saveContext() throws { let context = persistentContainer.viewContext - if context.hasChanges { -// do { -// try context.save() -// } catch { -// throw CoreDataError.saveFailure -// } - } } } diff --git a/ProjectManager/ProjectManager/Model/ToDoStatus.swift b/ProjectManager/ProjectManager/Model/ToDoStatus.swift index 85d7de9d5..c2b95755b 100644 --- a/ProjectManager/ProjectManager/Model/ToDoStatus.swift +++ b/ProjectManager/ProjectManager/Model/ToDoStatus.swift @@ -5,19 +5,8 @@ // Created by Max on 2023/09/24. // -enum ToDoStatus: CaseIterable { - case toDo - case doing - case done - - var name: String { - switch self { - case .toDo: - return "TODO" - case .doing: - return "DOING" - case .done: - return "DONE" - } - } +enum ToDoStatus: String, CaseIterable { + case toDo = "TODO" + case doing = "DOING" + case done = "DONE" } From d7227d608d73fbd1103ab50c0701a1dbd68c0f85 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Mon, 25 Sep 2023 20:44:57 +0900 Subject: [PATCH 13/20] =?UTF-8?q?chore:=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectManager/List/ViewModel/ViewModelProtocol.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift b/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift index b4dc3df97..957dab703 100644 --- a/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift +++ b/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift @@ -17,6 +17,6 @@ protocol ViewModelProtocol { func createData(title: String?, body: String?, dueDate: Date?) func updateData(_ entity: ToDo, title: String?, body: String?, dueDate: Date?) func deleteData(_ entity: ToDo) - func handel(error: Error) + func handle(error: Error) func setError(_ error: CoreDataError) } From cd820ed6f69241a9059fc2738486b26f3708a685 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Tue, 26 Sep 2023 10:06:48 +0900 Subject: [PATCH 14/20] =?UTF-8?q?refactor:=20=EC=9D=98=EC=A1=B4=EC=84=B1?= =?UTF-8?q?=20=EC=A3=BC=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ProjectManager/ProjectManager/App/SceneDelegate.swift | 3 ++- ProjectManager/ProjectManager/List/ToDoListView.swift | 4 ++-- .../ProjectManager/List/ToDoListViewController.swift | 6 +++--- .../ProjectManager/List/ViewModel/ToDoListViewModel.swift | 7 +++++-- .../ProjectManager/List/ViewModel/ViewModelProtocol.swift | 3 +-- .../ProjectManager/Model/CoreData/CoreDataManager.swift | 4 +--- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/ProjectManager/ProjectManager/App/SceneDelegate.swift b/ProjectManager/ProjectManager/App/SceneDelegate.swift index 4b9bab220..f389d1cc9 100644 --- a/ProjectManager/ProjectManager/App/SceneDelegate.swift +++ b/ProjectManager/ProjectManager/App/SceneDelegate.swift @@ -16,7 +16,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window = UIWindow(windowScene: windowScene) let coreDataManager = CoreDataManager() - let baseViewController = ToDoListViewController(coreDataManager) + let toDoViewModel = ToDoListViewModel(dataManager: coreDataManager) + let baseViewController = ToDoListViewController(toDoViewModel) let navigationViewController = UINavigationController(rootViewController: baseViewController) window?.rootViewController = navigationViewController window?.makeKeyAndVisible() diff --git a/ProjectManager/ProjectManager/List/ToDoListView.swift b/ProjectManager/ProjectManager/List/ToDoListView.swift index 331dbaa6d..f35353999 100644 --- a/ProjectManager/ProjectManager/List/ToDoListView.swift +++ b/ProjectManager/ProjectManager/List/ToDoListView.swift @@ -8,7 +8,7 @@ import UIKit class ToDoListView: UIView { - private let viewModel: ToDoListViewModel + private let viewModel: ViewModelProtocol private let status: ToDoStatus private let headerView: ToDoListHeaderView let today = Date().timeIntervalSinceReferenceDate @@ -39,7 +39,7 @@ class ToDoListView: UIView { return tableView }() - init(_ status: ToDoStatus, viewModel: ToDoListViewModel) { + init(_ status: ToDoStatus, viewModel: ViewModelProtocol) { self.status = status self.viewModel = viewModel self.headerView = ToDoListHeaderView(status) diff --git a/ProjectManager/ProjectManager/List/ToDoListViewController.swift b/ProjectManager/ProjectManager/List/ToDoListViewController.swift index ff6563804..2b1a6e03d 100644 --- a/ProjectManager/ProjectManager/List/ToDoListViewController.swift +++ b/ProjectManager/ProjectManager/List/ToDoListViewController.swift @@ -7,7 +7,7 @@ import UIKit class ToDoListViewController: UIViewController { - var viewModel: ToDoListViewModel + var viewModel: ViewModelProtocol private let stackView: UIStackView = { let stackView = UIStackView() @@ -23,8 +23,8 @@ class ToDoListViewController: UIViewController { private let doingView: ToDoListView private let doneView: ToDoListView - init(_ dataManager: CoreDataManager) { - self.viewModel = ToDoListViewModel(dataManager: dataManager) + init(_ viewModel: ViewModelProtocol) { + self.viewModel = viewModel self.toDoView = ToDoListView(.toDo, viewModel: viewModel) self.doingView = ToDoListView(.doing, viewModel: viewModel) self.doneView = ToDoListView(.done, viewModel: viewModel) diff --git a/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift b/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift index d7fcf79c9..09417857d 100644 --- a/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift +++ b/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift @@ -13,6 +13,7 @@ final class ToDoListViewModel: ViewModelProtocol { var doneList: Observable<[ToDo]> = Observable([]) var errorMessage: Observable = Observable(nil) var error: Observable = Observable(nil) + let coreDataManager: CoreDataManager init(dataManager: CoreDataManager) { @@ -22,7 +23,7 @@ final class ToDoListViewModel: ViewModelProtocol { addTestData() #endif } - + func fetchData(_ status: ToDoStatus) { do { let predicated = NSPredicate(format: "status == %@", status.rawValue) @@ -87,7 +88,9 @@ final class ToDoListViewModel: ViewModelProtocol { handle(error: error) } } - +} + +extension ToDoListViewModel { func handle(error: Error) { if let coreDataError = error as? CoreDataError { self.setError(coreDataError) diff --git a/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift b/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift index 957dab703..5451e38f3 100644 --- a/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift +++ b/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift @@ -13,10 +13,9 @@ protocol ViewModelProtocol { var doneList: Observable<[ToDo]>{ get set } var errorMessage: Observable { get set } var error: Observable { get set } + func fetchData(_ status: ToDoStatus) func createData(title: String?, body: String?, dueDate: Date?) func updateData(_ entity: ToDo, title: String?, body: String?, dueDate: Date?) func deleteData(_ entity: ToDo) - func handle(error: Error) - func setError(_ error: CoreDataError) } diff --git a/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift b/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift index 9d0c82920..60331f279 100644 --- a/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift +++ b/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift @@ -49,9 +49,7 @@ struct CoreDataManager { try saveContext() } - func saveContext() throws { - let context = persistentContainer.viewContext - } + func saveContext() throws {} } extension CoreDataManager { From 43a34de948d9b334faf90c8792a2860790be9c44 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Tue, 26 Sep 2023 21:39:12 +0900 Subject: [PATCH 15/20] =?UTF-8?q?feat:=20=EC=9E=90=EC=8B=9D=20=EB=B7=B0?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=B6=84=ED=95=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectManager.xcodeproj/project.pbxproj | 76 +++++--- .../ProjectManager/App/SceneDelegate.swift | 4 +- .../List/ToDoListViewController.swift | 112 ------------ .../List/ViewModel/ToDoListViewModel.swift | 170 ------------------ .../List/ViewModel/ViewModelProtocol.swift | 21 --- .../Model/CoreData/CoreDataManager.swift | 16 +- .../Model/KeywordArgument.swift | 19 ++ .../ProjectManager/Model/Observable.swift | 2 +- .../BaseView/ToDoListBaseViewController.swift | 105 +++++++++++ .../List/BaseView/ToDoListBaseViewModel.swift | 113 ++++++++++++ .../List/Cell/ToDoListHeaderView.swift | 0 .../List/Cell/ToDoListTableViewCell.swift} | 2 +- .../ToDoListChildViewController.swift | 129 +++++++++++++ .../ChildView/ToDoListChildViewModel.swift | 83 +++++++++ .../List/ToDoListViewController.swift} | 38 ++-- .../View/ViewModelProtocol.swift | 24 +++ 16 files changed, 550 insertions(+), 364 deletions(-) delete mode 100644 ProjectManager/ProjectManager/List/ToDoListViewController.swift delete mode 100644 ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift delete mode 100644 ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift create mode 100644 ProjectManager/ProjectManager/Model/KeywordArgument.swift create mode 100644 ProjectManager/ProjectManager/View/List/BaseView/ToDoListBaseViewController.swift create mode 100644 ProjectManager/ProjectManager/View/List/BaseView/ToDoListBaseViewModel.swift rename ProjectManager/ProjectManager/{ => View}/List/Cell/ToDoListHeaderView.swift (100%) rename ProjectManager/ProjectManager/{List/Cell/ToDoListViewCell.swift => View/List/Cell/ToDoListTableViewCell.swift} (97%) create mode 100644 ProjectManager/ProjectManager/View/List/ChildView/ToDoListChildViewController.swift create mode 100644 ProjectManager/ProjectManager/View/List/ChildView/ToDoListChildViewModel.swift rename ProjectManager/ProjectManager/{List/ToDoListView.swift => View/List/ToDoListViewController.swift} (81%) create mode 100644 ProjectManager/ProjectManager/View/ViewModelProtocol.swift diff --git a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj index 346ec1ee4..793181e4a 100644 --- a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj +++ b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj @@ -20,14 +20,16 @@ BA6463682AC044D70080E80D /* ToDoStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463672AC044D70080E80D /* ToDoStatus.swift */; }; BA64636A2AC045380080E80D /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463692AC045380080E80D /* Observable.swift */; }; BA64636D2AC045700080E80D /* ViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64636C2AC045700080E80D /* ViewModelProtocol.swift */; }; - BA64636F2AC04A700080E80D /* ToDoListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64636E2AC04A700080E80D /* ToDoListViewModel.swift */; }; - BA6463732AC04F590080E80D /* ToDoListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463722AC04F590080E80D /* ToDoListView.swift */; }; + BA64636F2AC04A700080E80D /* ToDoListBaseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64636E2AC04A700080E80D /* ToDoListBaseViewModel.swift */; }; BA6463752AC04F920080E80D /* ToDoListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463742AC04F920080E80D /* ToDoListHeaderView.swift */; }; BA6463782AC04FE40080E80D /* AlertBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463772AC04FE40080E80D /* AlertBuilder.swift */; }; - BA64637A2AC050AD0080E80D /* ToDoListViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463792AC050AD0080E80D /* ToDoListViewCell.swift */; }; + BA64637A2AC050AD0080E80D /* ToDoListTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463792AC050AD0080E80D /* ToDoListTableViewCell.swift */; }; + BA64637D2AC26F490080E80D /* ToDoListChildViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64637C2AC26F490080E80D /* ToDoListChildViewController.swift */; }; + BA64637F2AC283F70080E80D /* ToDoListChildViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64637E2AC283F70080E80D /* ToDoListChildViewModel.swift */; }; + BA6463812AC289680080E80D /* KeywordArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463802AC289680080E80D /* KeywordArgument.swift */; }; C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0525F51E1D0094C4CF /* AppDelegate.swift */; }; C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0725F51E1D0094C4CF /* SceneDelegate.swift */; }; - C7431F0A25F51E1D0094C4CF /* ToDoListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0925F51E1D0094C4CF /* ToDoListViewController.swift */; }; + C7431F0A25F51E1D0094C4CF /* ToDoListBaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0925F51E1D0094C4CF /* ToDoListBaseViewController.swift */; }; C7431F0F25F51E1E0094C4CF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C7431F0E25F51E1E0094C4CF /* Assets.xcassets */; }; C7431F1225F51E1E0094C4CF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C7431F1025F51E1E0094C4CF /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ @@ -41,15 +43,17 @@ BA6463672AC044D70080E80D /* ToDoStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoStatus.swift; sourceTree = ""; }; BA6463692AC045380080E80D /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; BA64636C2AC045700080E80D /* ViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelProtocol.swift; sourceTree = ""; }; - BA64636E2AC04A700080E80D /* ToDoListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListViewModel.swift; sourceTree = ""; }; - BA6463722AC04F590080E80D /* ToDoListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListView.swift; sourceTree = ""; }; + BA64636E2AC04A700080E80D /* ToDoListBaseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListBaseViewModel.swift; sourceTree = ""; }; BA6463742AC04F920080E80D /* ToDoListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListHeaderView.swift; sourceTree = ""; }; BA6463772AC04FE40080E80D /* AlertBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertBuilder.swift; sourceTree = ""; }; - BA6463792AC050AD0080E80D /* ToDoListViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListViewCell.swift; sourceTree = ""; }; + BA6463792AC050AD0080E80D /* ToDoListTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListTableViewCell.swift; sourceTree = ""; }; + BA64637C2AC26F490080E80D /* ToDoListChildViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListChildViewController.swift; sourceTree = ""; }; + BA64637E2AC283F70080E80D /* ToDoListChildViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListChildViewModel.swift; sourceTree = ""; }; + BA6463802AC289680080E80D /* KeywordArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordArgument.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 = ""; }; - C7431F0925F51E1D0094C4CF /* ToDoListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListViewController.swift; sourceTree = ""; }; + C7431F0925F51E1D0094C4CF /* ToDoListBaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListBaseViewController.swift; sourceTree = ""; }; C7431F0E25F51E1E0094C4CF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C7431F1125F51E1E0094C4CF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; C7431F1325F51E1E0094C4CF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -77,6 +81,7 @@ BA6463622AC043D30080E80D /* CoreData */, BA6463672AC044D70080E80D /* ToDoStatus.swift */, BA6463692AC045380080E80D /* Observable.swift */, + BA6463802AC289680080E80D /* KeywordArgument.swift */, ); path = Model; sourceTree = ""; @@ -92,22 +97,12 @@ path = CoreData; sourceTree = ""; }; - BA64636B2AC0455B0080E80D /* ViewModel */ = { - isa = PBXGroup; - children = ( - BA64636C2AC045700080E80D /* ViewModelProtocol.swift */, - BA64636E2AC04A700080E80D /* ToDoListViewModel.swift */, - ); - path = ViewModel; - sourceTree = ""; - }; BA6463702AC04AAB0080E80D /* List */ = { isa = PBXGroup; children = ( - BA64636B2AC0455B0080E80D /* ViewModel */, BA64637B2AC0613D0080E80D /* Cell */, - C7431F0925F51E1D0094C4CF /* ToDoListViewController.swift */, - BA6463722AC04F590080E80D /* ToDoListView.swift */, + BA6463822AC29D180080E80D /* BaseView */, + BA6463832AC29D1D0080E80D /* ChildView */, ); path = List; sourceTree = ""; @@ -133,11 +128,38 @@ isa = PBXGroup; children = ( BA6463742AC04F920080E80D /* ToDoListHeaderView.swift */, - BA6463792AC050AD0080E80D /* ToDoListViewCell.swift */, + BA6463792AC050AD0080E80D /* ToDoListTableViewCell.swift */, ); path = Cell; sourceTree = ""; }; + BA6463822AC29D180080E80D /* BaseView */ = { + isa = PBXGroup; + children = ( + BA64636E2AC04A700080E80D /* ToDoListBaseViewModel.swift */, + C7431F0925F51E1D0094C4CF /* ToDoListBaseViewController.swift */, + ); + path = BaseView; + sourceTree = ""; + }; + BA6463832AC29D1D0080E80D /* ChildView */ = { + isa = PBXGroup; + children = ( + BA64637E2AC283F70080E80D /* ToDoListChildViewModel.swift */, + BA64637C2AC26F490080E80D /* ToDoListChildViewController.swift */, + ); + path = ChildView; + sourceTree = ""; + }; + BA6463842AC2A3610080E80D /* View */ = { + isa = PBXGroup; + children = ( + BA64636C2AC045700080E80D /* ViewModelProtocol.swift */, + BA6463702AC04AAB0080E80D /* List */, + ); + path = View; + sourceTree = ""; + }; C7431EF925F51E1D0094C4CF = { isa = PBXGroup; children = ( @@ -160,7 +182,7 @@ BA6463712AC04AC50080E80D /* App */, BA6463762AC04FDA0080E80D /* Builder */, BA6463612AC043CE0080E80D /* Model */, - BA6463702AC04AAB0080E80D /* List */, + BA6463842AC2A3610080E80D /* View */, C7431F0E25F51E1E0094C4CF /* Assets.xcassets */, C7431F1025F51E1E0094C4CF /* LaunchScreen.storyboard */, C7431F1325F51E1E0094C4CF /* Info.plist */, @@ -248,21 +270,23 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C7431F0A25F51E1D0094C4CF /* ToDoListViewController.swift in Sources */, - BA64637A2AC050AD0080E80D /* ToDoListViewCell.swift in Sources */, + BA64637F2AC283F70080E80D /* ToDoListChildViewModel.swift in Sources */, + BA6463812AC289680080E80D /* KeywordArgument.swift in Sources */, + C7431F0A25F51E1D0094C4CF /* ToDoListBaseViewController.swift in Sources */, + BA64637A2AC050AD0080E80D /* ToDoListTableViewCell.swift in Sources */, C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */, C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */, BA6463752AC04F920080E80D /* ToDoListHeaderView.swift in Sources */, BA64636D2AC045700080E80D /* ViewModelProtocol.swift in Sources */, - BA64636F2AC04A700080E80D /* ToDoListViewModel.swift in Sources */, + BA64636F2AC04A700080E80D /* ToDoListBaseViewModel.swift in Sources */, BA6463662AC0440E0080E80D /* CoreDataError.swift in Sources */, BA6463602AC043680080E80D /* ToDo+CoreDataProperties.swift in Sources */, BA6463682AC044D70080E80D /* ToDoStatus.swift in Sources */, BA6463782AC04FE40080E80D /* AlertBuilder.swift in Sources */, - BA6463732AC04F590080E80D /* ToDoListView.swift in Sources */, BA64636A2AC045380080E80D /* Observable.swift in Sources */, BA64635F2AC043680080E80D /* ToDo+CoreDataClass.swift in Sources */, BA6463642AC043F50080E80D /* CoreDataManager.swift in Sources */, + BA64637D2AC26F490080E80D /* ToDoListChildViewController.swift in Sources */, BA6463582AC042A90080E80D /* ToDo.xcdatamodeld in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ProjectManager/ProjectManager/App/SceneDelegate.swift b/ProjectManager/ProjectManager/App/SceneDelegate.swift index f389d1cc9..964745e03 100644 --- a/ProjectManager/ProjectManager/App/SceneDelegate.swift +++ b/ProjectManager/ProjectManager/App/SceneDelegate.swift @@ -16,8 +16,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window = UIWindow(windowScene: windowScene) let coreDataManager = CoreDataManager() - let toDoViewModel = ToDoListViewModel(dataManager: coreDataManager) - let baseViewController = ToDoListViewController(toDoViewModel) + let toDoViewModel = ToDoListBaseViewModel(dataManager: coreDataManager) + let baseViewController = ToDoListBaseViewController(toDoViewModel) let navigationViewController = UINavigationController(rootViewController: baseViewController) window?.rootViewController = navigationViewController window?.makeKeyAndVisible() diff --git a/ProjectManager/ProjectManager/List/ToDoListViewController.swift b/ProjectManager/ProjectManager/List/ToDoListViewController.swift deleted file mode 100644 index 2b1a6e03d..000000000 --- a/ProjectManager/ProjectManager/List/ToDoListViewController.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// ProjectManager - ToDoListViewController.swift -// Created by yagom. -// Copyright © yagom. All rights reserved. -// Last modified by Max. - -import UIKit - -class ToDoListViewController: UIViewController { - var viewModel: ViewModelProtocol - - private let stackView: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .horizontal - stackView.distribution = .fillEqually - stackView.alignment = .top - stackView.spacing = 10 - return stackView - }() - - private let toDoView: ToDoListView - private let doingView: ToDoListView - private let doneView: ToDoListView - - init(_ viewModel: ViewModelProtocol) { - self.viewModel = viewModel - self.toDoView = ToDoListView(.toDo, viewModel: viewModel) - self.doingView = ToDoListView(.doing, viewModel: viewModel) - self.doneView = ToDoListView(.done, viewModel: viewModel) - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewWillAppear(_ animated: Bool) { - readData() - } - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .systemBackground - setupUI() - setupNavigationBar() - setupBinding() - } - - private func setupUI() { - let safeArea = view.safeAreaLayoutGuide - view.backgroundColor = .systemBackground - stackView.backgroundColor = UIColor(red: 0.8, green: 0.8, blue: 0.8, alpha: 1) - - stackView.addArrangedSubview(toDoView) - stackView.addArrangedSubview(doingView) - stackView.addArrangedSubview(doneView) - view.addSubview(stackView) - - NSLayoutConstraint.activate([ - stackView.widthAnchor.constraint(equalTo: safeArea.widthAnchor), - stackView.heightAnchor.constraint(equalTo: safeArea.heightAnchor), - stackView.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor), - stackView.centerYAnchor.constraint(equalTo: safeArea.centerYAnchor), - toDoView.heightAnchor.constraint(equalTo: stackView.heightAnchor), - toDoView.centerYAnchor.constraint(equalTo: stackView.centerYAnchor), - doingView.heightAnchor.constraint(equalTo: stackView.heightAnchor), - doingView.centerYAnchor.constraint(equalTo: stackView.centerYAnchor), - doneView.heightAnchor.constraint(equalTo: stackView.heightAnchor), - doneView.centerYAnchor.constraint(equalTo: stackView.centerYAnchor) - ]) - } - - private func setupNavigationBar() { - self.title = "Project Manager" - let addToDo = UIAction(image: UIImage(systemName: "plus")) { _ in } - navigationItem.rightBarButtonItem = UIBarButtonItem(primaryAction: addToDo) - } - - private func readData() { - ToDoStatus.allCases.forEach { viewModel.fetchData($0) } - } - - private func setupBinding() { - viewModel.toDoList.bind { [weak self] _ in - guard let self else { return } - self.toDoView.reloadTableView() - } - - viewModel.doingList.bind { [weak self] _ in - guard let self else { return } - self.doingView.reloadTableView() - } - - viewModel.doneList.bind { [weak self] _ in - guard let self else { return } - self.doneView.reloadTableView() - } - - viewModel.error.bind { [weak self] _ in - guard let self, - let error = viewModel.error.value else { return } - let alertBuilder = AlertBuilder(viewController: self, prefferedStyle: .alert) - alertBuilder.setControllerTitle(title: error.alertTitle) - alertBuilder.setControllerMessage(message: error.alertMessage) - alertBuilder.addAction(.confirm) - let alertController = alertBuilder.makeAlertController() - present(alertController, animated: true) - } - } -} - diff --git a/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift b/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift deleted file mode 100644 index 09417857d..000000000 --- a/ProjectManager/ProjectManager/List/ViewModel/ToDoListViewModel.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// ToDoViewModel.swift -// ProjectManager -// -// Created by Max on 2023/09/24. -// - -import CoreData - -final class ToDoListViewModel: ViewModelProtocol { - var toDoList: Observable<[ToDo]> = Observable([]) - var doingList: Observable<[ToDo]> = Observable([]) - var doneList: Observable<[ToDo]> = Observable([]) - var errorMessage: Observable = Observable(nil) - var error: Observable = Observable(nil) - - let coreDataManager: CoreDataManager - - init(dataManager: CoreDataManager) { - coreDataManager = dataManager - -#if DEBUG - addTestData() -#endif - } - - func fetchData(_ status: ToDoStatus) { - do { - let predicated = NSPredicate(format: "status == %@", status.rawValue) - let filtered = try coreDataManager.fetchData(entityName:"ToDo", predicate: predicated, sort: "createdAt") - - guard let result = filtered as? [ToDo] else { return } - - switch status { - case .toDo: - toDoList.value = result - case .doing: - doingList.value = result - case .done: - doneList.value = result - } - } catch(let error) { - handle(error: error) - } - } - - func createData(title: String?, body: String?, dueDate: Date?) { - guard let title, let body, let dueDate else { return } - let values: [CoreDataManager.Value] = [ - CoreDataManager.Value(key: "id", value: UUID()), - CoreDataManager.Value(key: "title", value: title), - CoreDataManager.Value(key: "body", value: body), - CoreDataManager.Value(key: "dueDate", value: dueDate), - CoreDataManager.Value(key: "createdAt", value: Date()), - CoreDataManager.Value(key: "status", value: ToDoStatus.toDo.rawValue) - ] - - do { - try coreDataManager.createData(type: ToDo.self, values: values) - fetchData(.toDo) - } catch(let error) { - handle(error: error) - } - } - - func updateData(_ entity: ToDo, title: String?, body: String?, dueDate: Date?) { - guard let status = ToDoStatus(rawValue: entity.status) else { return } - let values: [CoreDataManager.Value] = [ - CoreDataManager.Value(key: "title", value: title), - CoreDataManager.Value(key: "body", value: body), - CoreDataManager.Value(key: "dueDate", value: dueDate) - ] - do { - try coreDataManager.updateData(entity: entity, values: values) - fetchData(status) - } catch(let error) { - handle(error: error) - } - } - - func deleteData(_ entity: ToDo) { - guard let status = ToDoStatus(rawValue: entity.status) else { return } - - do { - try coreDataManager.deleteData(entity: entity) - fetchData(status) - } catch(let error) { - handle(error: error) - } - } -} - -extension ToDoListViewModel { - func handle(error: Error) { - if let coreDataError = error as? CoreDataError { - self.setError(coreDataError) - } else { - self.setError(CoreDataError.unknown) - } - } - - func setError(_ error: CoreDataError) { - self.errorMessage = Observable(error.alertMessage) - self.error = Observable(error) - } -} - -extension ToDoListViewModel { - func addTestData() { - // 불러오기 테스트용 - do { - let toDoValues: [CoreDataManager.Value] = [ - CoreDataManager.Value(key: "id", value: UUID()), - CoreDataManager.Value(key: "title", value: "테스트"), - CoreDataManager.Value(key: "body", value: "테스트용입니다"), - CoreDataManager.Value(key: "dueDate", value: Date()), - CoreDataManager.Value(key: "createdAt", value: Date()), - CoreDataManager.Value(key: "status", value: ToDoStatus.toDo.rawValue) - ] - - let doingValues: [CoreDataManager.Value] = [ - CoreDataManager.Value(key: "id", value: UUID()), - CoreDataManager.Value(key: "title", value: "테스트2"), - CoreDataManager.Value(key: "body", value: "테스트용입니다2"), - CoreDataManager.Value(key: "dueDate", value: Date()), - CoreDataManager.Value(key: "createdAt", value: Date()), - CoreDataManager.Value(key: "status", value: ToDoStatus.doing.rawValue) - ] - - let doneValues: [CoreDataManager.Value] = [ - CoreDataManager.Value(key: "id", value: UUID()), - CoreDataManager.Value(key: "title", value: "테스트3"), - CoreDataManager.Value(key: "body", value: "테스트용입니다3"), - CoreDataManager.Value(key: "dueDate", value: Date()), - CoreDataManager.Value(key: "createdAt", value: Date()), - CoreDataManager.Value(key: "status", value: ToDoStatus.done.rawValue) - ] - - let doneValues2: [CoreDataManager.Value] = [ - CoreDataManager.Value(key: "id", value: UUID()), - CoreDataManager.Value(key: "title", value: "테스트4"), - CoreDataManager.Value(key: "body", value: "테스트용입니다4"), - CoreDataManager.Value(key: "dueDate", value: Date()), - CoreDataManager.Value(key: "createdAt", value: Date()), - CoreDataManager.Value(key: "status", value: ToDoStatus.done.rawValue) - ] - - try coreDataManager.createData(type: ToDo.self, values: toDoValues) - try coreDataManager.createData(type: ToDo.self, values: doingValues) - try coreDataManager.createData(type: ToDo.self, values: doneValues) - try coreDataManager.createData(type: ToDo.self, values: doneValues2) - - let toDoPredicated = NSPredicate(format: "status == %@", ToDoStatus.toDo.rawValue) - let toDofiltered = try coreDataManager.fetchData(entityName:"ToDo", predicate: toDoPredicated, sort: "createdAt") - let doingPredicated = NSPredicate(format: "status == %@", ToDoStatus.doing.rawValue) - let doingfiltered = try coreDataManager.fetchData(entityName:"ToDo", predicate: doingPredicated, sort: "createdAt") - let donePredicated = NSPredicate(format: "status == %@", ToDoStatus.done.rawValue) - let donefiltered = try coreDataManager.fetchData(entityName:"ToDo", predicate: donePredicated, sort: "createdAt") - - guard let toDoResult = toDofiltered as? [ToDo], - let doingResult = doingfiltered as? [ToDo], - let doneResult = donefiltered as? [ToDo] else { return } - - toDoList.value = toDoResult - doingList.value = doingResult - doneList.value = doneResult - } catch { - } - } -} diff --git a/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift b/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift deleted file mode 100644 index 5451e38f3..000000000 --- a/ProjectManager/ProjectManager/List/ViewModel/ViewModelProtocol.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ViewModel.swift -// ProjectManager -// -// Created by Max on 2023/09/24. -// - -import Foundation - -protocol ViewModelProtocol { - var toDoList: Observable<[ToDo]> { get set } - var doingList: Observable<[ToDo]> { get set } - var doneList: Observable<[ToDo]>{ get set } - var errorMessage: Observable { get set } - var error: Observable { get set } - - func fetchData(_ status: ToDoStatus) - func createData(title: String?, body: String?, dueDate: Date?) - func updateData(_ entity: ToDo, title: String?, body: String?, dueDate: Date?) - func deleteData(_ entity: ToDo) -} diff --git a/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift b/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift index 60331f279..d86e73bd5 100644 --- a/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift +++ b/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift @@ -32,13 +32,13 @@ struct CoreDataManager { } @discardableResult - func createData(type: T.Type, values: [Value]) throws -> T { + func createData(type: T.Type, values: [KeywordArgument]) throws -> T { let newData = T(context: persistentContainer.viewContext) return try updateData(entity: newData, values: values) } @discardableResult - func updateData(entity: T, values: [Value]) throws -> T { + func updateData(entity: T, values: [KeywordArgument]) throws -> T { values.forEach { entity.setValue($0.value, forKey: $0.key) } try saveContext() return entity @@ -51,15 +51,3 @@ struct CoreDataManager { func saveContext() throws {} } - -extension CoreDataManager { - struct Value { - let key: String - let value: Any? - - init(key: String, value: Any?) { - self.key = key - self.value = value - } - } -} diff --git a/ProjectManager/ProjectManager/Model/KeywordArgument.swift b/ProjectManager/ProjectManager/Model/KeywordArgument.swift new file mode 100644 index 000000000..64f9156b6 --- /dev/null +++ b/ProjectManager/ProjectManager/Model/KeywordArgument.swift @@ -0,0 +1,19 @@ +// +// Argument.swift +// ProjectManager +// +// Created by Max on 2023/09/26. +// + +import Foundation + +final class KeywordArgument { + let key: String + let value: Any? + + init(key: String, value: Any?) { + self.key = key + self.value = value + } + +} diff --git a/ProjectManager/ProjectManager/Model/Observable.swift b/ProjectManager/ProjectManager/Model/Observable.swift index 4967fc4a5..aacc1da73 100644 --- a/ProjectManager/ProjectManager/Model/Observable.swift +++ b/ProjectManager/ProjectManager/Model/Observable.swift @@ -5,7 +5,7 @@ // Created by Max on 2023/09/24. // -class Observable { +final class Observable { private var listener: ((T) -> Void)? var value: T { diff --git a/ProjectManager/ProjectManager/View/List/BaseView/ToDoListBaseViewController.swift b/ProjectManager/ProjectManager/View/List/BaseView/ToDoListBaseViewController.swift new file mode 100644 index 000000000..10bf75b93 --- /dev/null +++ b/ProjectManager/ProjectManager/View/List/BaseView/ToDoListBaseViewController.swift @@ -0,0 +1,105 @@ +// +// ProjectManager - ToDoListViewController.swift +// Created by yagom. +// Copyright © yagom. All rights reserved. +// Last modified by Max. + +import UIKit + +class ToDoListBaseViewController: UIViewController { + private let viewModel: ToDoListBaseViewModel + + private let stackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.alignment = .top + stackView.spacing = 10 + return stackView + }() + + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .long + formatter.timeStyle = .none + return formatter + }() + + init(_ viewModel: ToDoListBaseViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewWillAppear(_ animated: Bool) { + readData() + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + addChildren() + setupUI() + setupNavigationBar() + setupBinding() + } + + private func addChildren() { + ToDoStatus.allCases.forEach { status in + let childViewModel = viewModel.addChildModel(status: status) + let childViewController = ToDoListChildViewController(status, + viewModel: childViewModel, + dateFormatter: dateFormatter) + self.addChild(childViewController) + stackView.addArrangedSubview(childViewController.view) + + NSLayoutConstraint.activate([ + childViewController.view.heightAnchor.constraint(equalTo: stackView.heightAnchor), + childViewController.view.centerYAnchor.constraint(equalTo: stackView.centerYAnchor) + ]) + } + } + + private func setupUI() { + let safeArea = view.safeAreaLayoutGuide + view.backgroundColor = .systemBackground + stackView.backgroundColor = UIColor(red: 0.8, green: 0.8, blue: 0.8, alpha: 1) + + view.addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.widthAnchor.constraint(equalTo: safeArea.widthAnchor), + stackView.heightAnchor.constraint(equalTo: safeArea.heightAnchor), + stackView.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: safeArea.centerYAnchor) + ]) + } + + private func setupNavigationBar() { + self.title = "Project Manager" + let addToDo = UIAction(image: UIImage(systemName: "plus")) { _ in } + navigationItem.rightBarButtonItem = UIBarButtonItem(primaryAction: addToDo) + } + + private func readData() { + viewModel.fetchData() + } + + private func setupBinding() { + viewModel.error.bind { [weak self] _ in + guard let self, + let error = viewModel.error.value else { return } + let alertBuilder = AlertBuilder(viewController: self, prefferedStyle: .alert) + alertBuilder.setControllerTitle(title: error.alertTitle) + alertBuilder.setControllerMessage(message: error.alertMessage) + alertBuilder.addAction(.confirm) + let alertController = alertBuilder.makeAlertController() + present(alertController, animated: true) + } + } +} + diff --git a/ProjectManager/ProjectManager/View/List/BaseView/ToDoListBaseViewModel.swift b/ProjectManager/ProjectManager/View/List/BaseView/ToDoListBaseViewModel.swift new file mode 100644 index 000000000..78ec65a84 --- /dev/null +++ b/ProjectManager/ProjectManager/View/List/BaseView/ToDoListBaseViewModel.swift @@ -0,0 +1,113 @@ +// +// ToDoViewModel.swift +// ProjectManager +// +// Created by Max on 2023/09/24. +// + +import CoreData + +protocol ToDoListBaseViewModelDelegate: AnyObject { + func fetchData() + func fetchDataByStatus(for status: ToDoStatus) + func createData(values: [KeywordArgument]) + func updateData(_ entity: ToDo, values: [KeywordArgument]) + func deleteData(_ entity: ToDo) +} + +final class ToDoListBaseViewModel: ViewModelProtocol, ToDoListBaseViewModelDelegate { + private let coreDataManager: CoreDataManager + private var children: [ToDoStatus: ToDoListChildViewModel] = [:] + + var entityList: Observable<[ToDo]> = Observable([]) + var errorMessage: Observable = Observable(nil) + var error: Observable = Observable(nil) + + init(dataManager: CoreDataManager) { + coreDataManager = dataManager + } + + func fetchData() { + ToDoStatus.allCases.forEach { fetchDataByStatus(for: $0) } + } + + func fetchDataByStatus(for status: ToDoStatus) { + do { + let predicated = NSPredicate(format: "status == %@", status.rawValue) + let filtered = try coreDataManager.fetchData(entityName:"ToDo", predicate: predicated, sort: "createdAt") + + guard let result = filtered as? [ToDo] else { return } + + children[status]?.entityList.value = result + + } catch(let error) { + handle(error: error) + } + } + + func createData(values: [KeywordArgument]) { + var values = values + + if values.filter({ $0.key == "id" }).isEmpty { + values.append(KeywordArgument(key: "id", value: UUID())) + } + + if values.filter({ $0.key == "createdAt" }).isEmpty { + values.append(KeywordArgument(key: "createdAt", value: Date())) + } + + if values.filter({ $0.key == "status" }).isEmpty { + values.append(KeywordArgument(key: "status", value: ToDoStatus.toDo.rawValue)) + } + + do { + try coreDataManager.createData(type: ToDo.self, values: values) + fetchDataByStatus(for: .toDo) + } catch(let error) { + handle(error: error) + } + } + + func updateData(_ entity: ToDo, values: [KeywordArgument]) { + do { + try coreDataManager.updateData(entity: entity, values: values) + } catch(let error) { + handle(error: error) + } + } + + func deleteData(_ entity: ToDo) { + do { + try coreDataManager.deleteData(entity: entity) + } catch(let error) { + handle(error: error) + } + } +} + +extension ToDoListBaseViewModel { + func addChildModel(status: ToDoStatus) -> ToDoListChildViewModel { + let child = ToDoListChildViewModel(status: status) + children[status] = child + child.delegate = self +#if DEBUG + child.addTestData() +#endif + return child + } +} + +extension ToDoListBaseViewModel { + func handle(error: Error) { + if let coreDataError = error as? CoreDataError { + self.setError(coreDataError) + } else { + self.setError(CoreDataError.unknown) + } + } + + func setError(_ error: CoreDataError) { + self.errorMessage = Observable(error.alertMessage) + self.error = Observable(error) + } +} diff --git a/ProjectManager/ProjectManager/List/Cell/ToDoListHeaderView.swift b/ProjectManager/ProjectManager/View/List/Cell/ToDoListHeaderView.swift similarity index 100% rename from ProjectManager/ProjectManager/List/Cell/ToDoListHeaderView.swift rename to ProjectManager/ProjectManager/View/List/Cell/ToDoListHeaderView.swift diff --git a/ProjectManager/ProjectManager/List/Cell/ToDoListViewCell.swift b/ProjectManager/ProjectManager/View/List/Cell/ToDoListTableViewCell.swift similarity index 97% rename from ProjectManager/ProjectManager/List/Cell/ToDoListViewCell.swift rename to ProjectManager/ProjectManager/View/List/Cell/ToDoListTableViewCell.swift index a7180f088..2a68c78f0 100644 --- a/ProjectManager/ProjectManager/List/Cell/ToDoListViewCell.swift +++ b/ProjectManager/ProjectManager/View/List/Cell/ToDoListTableViewCell.swift @@ -7,7 +7,7 @@ import UIKit -class ToDoListViewCell: UITableViewCell { +class ToDoListTableViewCell: UITableViewCell { private let titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false diff --git a/ProjectManager/ProjectManager/View/List/ChildView/ToDoListChildViewController.swift b/ProjectManager/ProjectManager/View/List/ChildView/ToDoListChildViewController.swift new file mode 100644 index 000000000..f62118ac8 --- /dev/null +++ b/ProjectManager/ProjectManager/View/List/ChildView/ToDoListChildViewController.swift @@ -0,0 +1,129 @@ +// +// ToDoViewController.swift +// ProjectManager +// +// Created by Max on 2023/09/26. +// + +import UIKit + +class ToDoListChildViewController: UIViewController { + private let status: ToDoStatus + private let headerView: ToDoListHeaderView + private let viewModel: ToDoListChildViewModel + + let today = Date().timeIntervalSinceReferenceDate + private let dateFormatter: DateFormatter + + private let tableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.tag = 0 + tableView.backgroundColor = UIColor(red: 0.95, green: 0.95, blue: 0.98, alpha: 1) + return tableView + }() + + init(_ status: ToDoStatus, viewModel: ToDoListChildViewModel, dateFormatter: DateFormatter) { + self.status = status + self.headerView = ToDoListHeaderView(status) + self.dateFormatter = dateFormatter + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupTableView() + setupBinding() + } + + private func setupUI() { + let safeArea = view.safeAreaLayoutGuide + view.backgroundColor = .systemBackground + view.addSubview(tableView) + + NSLayoutConstraint.activate([ + tableView.widthAnchor.constraint(equalTo: safeArea.widthAnchor), + tableView.heightAnchor.constraint(equalTo: safeArea.heightAnchor), + tableView.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor), + tableView.centerYAnchor.constraint(equalTo: safeArea.centerYAnchor) + ]) + } + + private func setupTableView() { + tableView.dataSource = self + tableView.delegate = self + tableView.register(ToDoListTableViewCell.self, forCellReuseIdentifier: status.rawValue) + } + + func reloadTableView() { + tableView.reloadData() + headerView.setupTotalCount(viewModel.entityList.value.count) + } +} + +extension ToDoListChildViewController: UITableViewDelegate, UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + viewModel.entityList.value.count + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return headerView + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: status.rawValue, + for: indexPath) as? + ToDoListTableViewCell else { return UITableViewCell() } + + let toDoEntity = viewModel.entityList.value[indexPath.row] + let isDone = toDoEntity.status == ToDoStatus.done.rawValue + let isPast = floor(today/86400) > floor(toDoEntity.dueDate.timeIntervalSinceReferenceDate/86400) && !isDone + let date = dateFormatter.string(from: toDoEntity.dueDate) + + cell.setupUI() + cell.setModel(title: toDoEntity.title, date: date, body: toDoEntity.body, isPast: isPast) + + return cell + } + + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> + UISwipeActionsConfiguration? { + let delete = UIContextualAction(style: .normal, title: "") { (_, _, success: @escaping (Bool) -> Void) in + let selectedEntity = self.viewModel.entityList.value[indexPath.row] + self.viewModel.deleteData(selectedEntity) + } + + delete.backgroundColor = .systemRed + delete.title = "Delete" + + return UISwipeActionsConfiguration(actions: [delete]) + } +} + +extension ToDoListChildViewController { + private func setupBinding() { + viewModel.entityList.bind { [weak self] _ in + guard let self else { return } + self.reloadTableView() + } + + viewModel.error.bind { [weak self] _ in + guard let self, + let error = viewModel.error.value else { return } + let alertBuilder = AlertBuilder(viewController: self, prefferedStyle: .alert) + alertBuilder.setControllerTitle(title: error.alertTitle) + alertBuilder.setControllerMessage(message: error.alertMessage) + alertBuilder.addAction(.confirm) + let alertController = alertBuilder.makeAlertController() + present(alertController, animated: true) + } + } + +} diff --git a/ProjectManager/ProjectManager/View/List/ChildView/ToDoListChildViewModel.swift b/ProjectManager/ProjectManager/View/List/ChildView/ToDoListChildViewModel.swift new file mode 100644 index 000000000..765929472 --- /dev/null +++ b/ProjectManager/ProjectManager/View/List/ChildView/ToDoListChildViewModel.swift @@ -0,0 +1,83 @@ +// +// ToDoViewModelProtocol.swift +// ProjectManager +// +// Created by Max on 2023/09/26. +// + +import Foundation + +final class ToDoListChildViewModel: ViewModelProtocol { + private let status: ToDoStatus + var entityList: Observable<[ToDo]> = Observable([]) + var errorMessage: Observable = Observable(nil) + var error: Observable = Observable(nil) + weak var delegate: ToDoListBaseViewModelDelegate? + + init(status: ToDoStatus) { + self.status = status + } + + func fetchData() { + delegate?.fetchDataByStatus(for: status) + } + + func createData(values: [KeywordArgument]) { + delegate?.createData(values: values) + } + + func updateData(_ entity: ToDo, values: [KeywordArgument]) { + delegate?.updateData(entity, values: values) + fetchData() + } + + func deleteData(_ entity: ToDo) { + delegate?.deleteData(entity) + fetchData() + } +} + +extension ToDoListChildViewModel { + func handle(error: Error) { + if let coreDataError = error as? CoreDataError { + self.setError(coreDataError) + } else { + self.setError(CoreDataError.unknown) + } + } + + func setError(_ error: CoreDataError) { + self.errorMessage = Observable(error.alertMessage) + self.error = Observable(error) + } +} + +#if DEBUG +extension ToDoListChildViewModel { + func addTestData() { + // 불러오기 테스트용 + let values: [KeywordArgument] = [ + KeywordArgument(key: "id", value: UUID()), + KeywordArgument(key: "title", value: "\(status.rawValue) 테스트1"), + KeywordArgument(key: "body", value: "테스트용입니다"), + KeywordArgument(key: "dueDate", value: Date()), + KeywordArgument(key: "createdAt", value: Date()), + KeywordArgument(key: "status", value: status.rawValue) + ] + + let values2: [KeywordArgument] = [ + KeywordArgument(key: "id", value: UUID()), + KeywordArgument(key: "title", value: "\(status.rawValue) 테스트2"), + KeywordArgument(key: "body", value: "테스트용입니다2"), + KeywordArgument(key: "dueDate", value: Date()), + KeywordArgument(key: "createdAt", value: Date()), + KeywordArgument(key: "status", value: status.rawValue) + ] + + delegate?.createData(values: values) + delegate?.createData(values: values2) + + fetchData() + } +} +#endif diff --git a/ProjectManager/ProjectManager/List/ToDoListView.swift b/ProjectManager/ProjectManager/View/List/ToDoListViewController.swift similarity index 81% rename from ProjectManager/ProjectManager/List/ToDoListView.swift rename to ProjectManager/ProjectManager/View/List/ToDoListViewController.swift index f35353999..6c37ce2da 100644 --- a/ProjectManager/ProjectManager/List/ToDoListView.swift +++ b/ProjectManager/ProjectManager/View/List/ToDoListViewController.swift @@ -1,14 +1,13 @@ // -// ToDoListView.swift +// ToDoViewController.swift // ProjectManager // -// Created by Max on 2023/09/24. +// Created by Max on 2023/09/26. // import UIKit -class ToDoListView: UIView { - private let viewModel: ViewModelProtocol +class ChildListViewController: BaseListViewController { private let status: ToDoStatus private let headerView: ToDoListHeaderView let today = Date().timeIntervalSinceReferenceDate @@ -41,27 +40,31 @@ class ToDoListView: UIView { init(_ status: ToDoStatus, viewModel: ViewModelProtocol) { self.status = status - self.viewModel = viewModel self.headerView = ToDoListHeaderView(status) - super.init(frame: .init()) - - setupUI() - setupTableView() + super.init(viewModel) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupTableView() + } + + private func setupUI() { - self.backgroundColor = .systemBackground - self.addSubview(tableView) + let safeArea = view.safeAreaLayoutGuide + view.backgroundColor = .systemBackground + view.addSubview(tableView) NSLayoutConstraint.activate([ - tableView.widthAnchor.constraint(equalTo: self.widthAnchor), - tableView.heightAnchor.constraint(equalTo: self.heightAnchor), - tableView.centerXAnchor.constraint(equalTo: self.centerXAnchor), - tableView.centerYAnchor.constraint(equalTo: self.centerYAnchor) + tableView.widthAnchor.constraint(equalTo: safeArea.widthAnchor), + tableView.heightAnchor.constraint(equalTo: safeArea.heightAnchor), + tableView.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor), + tableView.centerYAnchor.constraint(equalTo: safeArea.centerYAnchor) ]) } @@ -77,7 +80,7 @@ class ToDoListView: UIView { } } -extension ToDoListView: UITableViewDelegate, UITableViewDataSource { +extension ChildListViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { toDoEntities.count } @@ -115,3 +118,4 @@ extension ToDoListView: UITableViewDelegate, UITableViewDataSource { return UISwipeActionsConfiguration(actions: [delete]) } } + diff --git a/ProjectManager/ProjectManager/View/ViewModelProtocol.swift b/ProjectManager/ProjectManager/View/ViewModelProtocol.swift new file mode 100644 index 000000000..43bdd26d4 --- /dev/null +++ b/ProjectManager/ProjectManager/View/ViewModelProtocol.swift @@ -0,0 +1,24 @@ +// +// ViewModel.swift +// ProjectManager +// +// Created by Max on 2023/09/24. +// + +import Foundation + +protocol ViewModelProtocol { + associatedtype Entity + associatedtype ViewModelError + + var entityList: Observable<[Entity]> { get set } + var errorMessage: Observable { get set } + var error: Observable { get set } + + func fetchData() + func createData(values: [KeywordArgument]) + func updateData(_ entity: Entity, values: [KeywordArgument]) + func deleteData(_ entity: Entity) + func handle(error: Error) + func setError(_ error: ViewModelError) +} From 75f82431036801775d021690758eeedfad481790 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Tue, 3 Oct 2023 12:31:47 +0900 Subject: [PATCH 16/20] =?UTF-8?q?feat:=20=EB=B0=B0=EC=B9=98=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectManager.xcodeproj/project.pbxproj | 32 +++++-- .../ProjectManager/Model/Action.swift | 22 +++++ .../CoreData/ToDo+CoreDataProperties.swift | 2 +- .../Model/KeywordArgument.swift | 2 - .../ToDo.xcdatamodel/contents | 2 +- .../{ => View}/Builder/AlertBuilder.swift | 2 +- .../BaseView/ToDoListBaseViewController.swift | 30 ++++--- .../BaseView/ToDoListBaseViewModel.swift | 84 ++++++++++--------- .../Cell/ToDoListHeaderView.swift | 2 +- .../Cell/ToDoListTableViewCell.swift | 2 +- .../ToDoListChildViewController.swift | 39 +++++---- .../ChildView/ToDoListChildViewModel.swift | 53 +++++------- .../ToDoListViewController.swift | 0 .../ToDoListBaseViewModelDelegate.swift | 13 +++ .../View/Protocol/ViewModelType.swift | 17 ++++ .../View/ViewModelProtocol.swift | 24 ------ 16 files changed, 193 insertions(+), 133 deletions(-) create mode 100644 ProjectManager/ProjectManager/Model/Action.swift rename ProjectManager/ProjectManager/{ => View}/Builder/AlertBuilder.swift (95%) rename ProjectManager/ProjectManager/View/{List => ListView}/BaseView/ToDoListBaseViewController.swift (78%) rename ProjectManager/ProjectManager/View/{List => ListView}/BaseView/ToDoListBaseViewModel.swift (57%) rename ProjectManager/ProjectManager/View/{List => ListView}/Cell/ToDoListHeaderView.swift (97%) rename ProjectManager/ProjectManager/View/{List => ListView}/Cell/ToDoListTableViewCell.swift (97%) rename ProjectManager/ProjectManager/View/{List => ListView}/ChildView/ToDoListChildViewController.swift (76%) rename ProjectManager/ProjectManager/View/{List => ListView}/ChildView/ToDoListChildViewModel.swift (71%) rename ProjectManager/ProjectManager/View/{List => ListView}/ToDoListViewController.swift (100%) create mode 100644 ProjectManager/ProjectManager/View/Protocol/ToDoListBaseViewModelDelegate.swift create mode 100644 ProjectManager/ProjectManager/View/Protocol/ViewModelType.swift delete mode 100644 ProjectManager/ProjectManager/View/ViewModelProtocol.swift diff --git a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj index 793181e4a..4a70fcb1e 100644 --- a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj +++ b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj @@ -19,7 +19,7 @@ BA6463662AC0440E0080E80D /* CoreDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463652AC0440E0080E80D /* CoreDataError.swift */; }; BA6463682AC044D70080E80D /* ToDoStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463672AC044D70080E80D /* ToDoStatus.swift */; }; BA64636A2AC045380080E80D /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463692AC045380080E80D /* Observable.swift */; }; - BA64636D2AC045700080E80D /* ViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64636C2AC045700080E80D /* ViewModelProtocol.swift */; }; + BA64636D2AC045700080E80D /* ViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64636C2AC045700080E80D /* ViewModelType.swift */; }; BA64636F2AC04A700080E80D /* ToDoListBaseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64636E2AC04A700080E80D /* ToDoListBaseViewModel.swift */; }; BA6463752AC04F920080E80D /* ToDoListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463742AC04F920080E80D /* ToDoListHeaderView.swift */; }; BA6463782AC04FE40080E80D /* AlertBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463772AC04FE40080E80D /* AlertBuilder.swift */; }; @@ -27,6 +27,8 @@ BA64637D2AC26F490080E80D /* ToDoListChildViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64637C2AC26F490080E80D /* ToDoListChildViewController.swift */; }; BA64637F2AC283F70080E80D /* ToDoListChildViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64637E2AC283F70080E80D /* ToDoListChildViewModel.swift */; }; BA6463812AC289680080E80D /* KeywordArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463802AC289680080E80D /* KeywordArgument.swift */; }; + BAD471D62ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD471D52ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift */; }; + BAD471D82ACBA4820021323A /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD471D72ACBA4820021323A /* Action.swift */; }; C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0525F51E1D0094C4CF /* AppDelegate.swift */; }; C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0725F51E1D0094C4CF /* SceneDelegate.swift */; }; C7431F0A25F51E1D0094C4CF /* ToDoListBaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0925F51E1D0094C4CF /* ToDoListBaseViewController.swift */; }; @@ -42,7 +44,7 @@ BA6463652AC0440E0080E80D /* CoreDataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataError.swift; sourceTree = ""; }; BA6463672AC044D70080E80D /* ToDoStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoStatus.swift; sourceTree = ""; }; BA6463692AC045380080E80D /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; - BA64636C2AC045700080E80D /* ViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelProtocol.swift; sourceTree = ""; }; + BA64636C2AC045700080E80D /* ViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelType.swift; sourceTree = ""; }; BA64636E2AC04A700080E80D /* ToDoListBaseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListBaseViewModel.swift; sourceTree = ""; }; BA6463742AC04F920080E80D /* ToDoListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListHeaderView.swift; sourceTree = ""; }; BA6463772AC04FE40080E80D /* AlertBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertBuilder.swift; sourceTree = ""; }; @@ -50,6 +52,8 @@ BA64637C2AC26F490080E80D /* ToDoListChildViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListChildViewController.swift; sourceTree = ""; }; BA64637E2AC283F70080E80D /* ToDoListChildViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListChildViewModel.swift; sourceTree = ""; }; BA6463802AC289680080E80D /* KeywordArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordArgument.swift; sourceTree = ""; }; + BAD471D52ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListBaseViewModelDelegate.swift; sourceTree = ""; }; + BAD471D72ACBA4820021323A /* Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Action.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 = ""; }; @@ -82,6 +86,7 @@ BA6463672AC044D70080E80D /* ToDoStatus.swift */, BA6463692AC045380080E80D /* Observable.swift */, BA6463802AC289680080E80D /* KeywordArgument.swift */, + BAD471D72ACBA4820021323A /* Action.swift */, ); path = Model; sourceTree = ""; @@ -97,14 +102,14 @@ path = CoreData; sourceTree = ""; }; - BA6463702AC04AAB0080E80D /* List */ = { + BA6463702AC04AAB0080E80D /* ListView */ = { isa = PBXGroup; children = ( BA64637B2AC0613D0080E80D /* Cell */, BA6463822AC29D180080E80D /* BaseView */, BA6463832AC29D1D0080E80D /* ChildView */, ); - path = List; + path = ListView; sourceTree = ""; }; BA6463712AC04AC50080E80D /* App */ = { @@ -154,12 +159,22 @@ BA6463842AC2A3610080E80D /* View */ = { isa = PBXGroup; children = ( - BA64636C2AC045700080E80D /* ViewModelProtocol.swift */, - BA6463702AC04AAB0080E80D /* List */, + BA6463762AC04FDA0080E80D /* Builder */, + BAD471D42ACBA2B80021323A /* Protocol */, + BA6463702AC04AAB0080E80D /* ListView */, ); path = View; sourceTree = ""; }; + BAD471D42ACBA2B80021323A /* Protocol */ = { + isa = PBXGroup; + children = ( + BA64636C2AC045700080E80D /* ViewModelType.swift */, + BAD471D52ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift */, + ); + path = Protocol; + sourceTree = ""; + }; C7431EF925F51E1D0094C4CF = { isa = PBXGroup; children = ( @@ -180,7 +195,6 @@ isa = PBXGroup; children = ( BA6463712AC04AC50080E80D /* App */, - BA6463762AC04FDA0080E80D /* Builder */, BA6463612AC043CE0080E80D /* Model */, BA6463842AC2A3610080E80D /* View */, C7431F0E25F51E1E0094C4CF /* Assets.xcassets */, @@ -277,11 +291,13 @@ C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */, C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */, BA6463752AC04F920080E80D /* ToDoListHeaderView.swift in Sources */, - BA64636D2AC045700080E80D /* ViewModelProtocol.swift in Sources */, + BA64636D2AC045700080E80D /* ViewModelType.swift in Sources */, BA64636F2AC04A700080E80D /* ToDoListBaseViewModel.swift in Sources */, BA6463662AC0440E0080E80D /* CoreDataError.swift in Sources */, BA6463602AC043680080E80D /* ToDo+CoreDataProperties.swift in Sources */, + BAD471D62ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift in Sources */, BA6463682AC044D70080E80D /* ToDoStatus.swift in Sources */, + BAD471D82ACBA4820021323A /* Action.swift in Sources */, BA6463782AC04FE40080E80D /* AlertBuilder.swift in Sources */, BA64636A2AC045380080E80D /* Observable.swift in Sources */, BA64635F2AC043680080E80D /* ToDo+CoreDataClass.swift in Sources */, diff --git a/ProjectManager/ProjectManager/Model/Action.swift b/ProjectManager/ProjectManager/Model/Action.swift new file mode 100644 index 000000000..fe90824d3 --- /dev/null +++ b/ProjectManager/ProjectManager/Model/Action.swift @@ -0,0 +1,22 @@ +// +// Action.swift +// ProjectManager +// +// Created by Max on 2023/10/03. +// + +struct Action { + let type: ActionType + let extraInformation: [KeywordArgument] + + init(type: ActionType, extraInformation: [KeywordArgument] = []) { + self.type = type + self.extraInformation = extraInformation + } + + enum ActionType { + case create + case update + case delete + } +} diff --git a/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift b/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift index 4788fca0a..b93da530a 100644 --- a/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift +++ b/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift @@ -19,7 +19,7 @@ extension ToDo { @NSManaged public var title: String @NSManaged public var dueDate: Date @NSManaged public var body: String - @NSManaged public var createdAt: Date + @NSManaged public var modifiedAt: Date @NSManaged public var status: String } diff --git a/ProjectManager/ProjectManager/Model/KeywordArgument.swift b/ProjectManager/ProjectManager/Model/KeywordArgument.swift index 64f9156b6..3420d5280 100644 --- a/ProjectManager/ProjectManager/Model/KeywordArgument.swift +++ b/ProjectManager/ProjectManager/Model/KeywordArgument.swift @@ -5,8 +5,6 @@ // Created by Max on 2023/09/26. // -import Foundation - final class KeywordArgument { let key: String let value: Any? diff --git a/ProjectManager/ProjectManager/ToDo.xcdatamodeld/ToDo.xcdatamodel/contents b/ProjectManager/ProjectManager/ToDo.xcdatamodeld/ToDo.xcdatamodel/contents index 58fa67964..3f0484eb0 100644 --- a/ProjectManager/ProjectManager/ToDo.xcdatamodeld/ToDo.xcdatamodel/contents +++ b/ProjectManager/ProjectManager/ToDo.xcdatamodeld/ToDo.xcdatamodel/contents @@ -2,9 +2,9 @@ - + diff --git a/ProjectManager/ProjectManager/Builder/AlertBuilder.swift b/ProjectManager/ProjectManager/View/Builder/AlertBuilder.swift similarity index 95% rename from ProjectManager/ProjectManager/Builder/AlertBuilder.swift rename to ProjectManager/ProjectManager/View/Builder/AlertBuilder.swift index fdddaa515..8c9337d6b 100644 --- a/ProjectManager/ProjectManager/Builder/AlertBuilder.swift +++ b/ProjectManager/ProjectManager/View/Builder/AlertBuilder.swift @@ -13,7 +13,7 @@ final class AlertBuilder { private var controllerMessage: String = "" private var alertActions: [UIAlertAction] = [] - init(viewController: UIViewController, prefferedStyle: UIAlertController.Style) { + init(prefferedStyle: UIAlertController.Style) { self.alertController = UIAlertController(title: nil, message: nil, preferredStyle: prefferedStyle) } diff --git a/ProjectManager/ProjectManager/View/List/BaseView/ToDoListBaseViewController.swift b/ProjectManager/ProjectManager/View/ListView/BaseView/ToDoListBaseViewController.swift similarity index 78% rename from ProjectManager/ProjectManager/View/List/BaseView/ToDoListBaseViewController.swift rename to ProjectManager/ProjectManager/View/ListView/BaseView/ToDoListBaseViewController.swift index 10bf75b93..b1d9fe5e4 100644 --- a/ProjectManager/ProjectManager/View/List/BaseView/ToDoListBaseViewController.swift +++ b/ProjectManager/ProjectManager/View/ListView/BaseView/ToDoListBaseViewController.swift @@ -6,7 +6,7 @@ import UIKit -class ToDoListBaseViewController: UIViewController { +final class ToDoListBaseViewController: UIViewController { private let viewModel: ToDoListBaseViewModel private let stackView: UIStackView = { @@ -36,7 +36,7 @@ class ToDoListBaseViewController: UIViewController { } override func viewWillAppear(_ animated: Bool) { - readData() + viewModel.fetchAllData() } override func viewDidLoad() { @@ -50,7 +50,7 @@ class ToDoListBaseViewController: UIViewController { private func addChildren() { ToDoStatus.allCases.forEach { status in - let childViewModel = viewModel.addChildModel(status: status) + let childViewModel = viewModel.addChild(status) let childViewController = ToDoListChildViewController(status, viewModel: childViewModel, dateFormatter: dateFormatter) @@ -81,19 +81,27 @@ class ToDoListBaseViewController: UIViewController { private func setupNavigationBar() { self.title = "Project Manager" - let addToDo = UIAction(image: UIImage(systemName: "plus")) { _ in } + let addToDo = UIAction(image: UIImage(systemName: "plus")) { [weak self] _ in +#if DEBUG + let testValue: [KeywordArgument] = [ + KeywordArgument(key: "id", value: UUID()), + KeywordArgument(key: "title", value: "추가 테스트"), + KeywordArgument(key: "body", value: "테스트용입니다"), + KeywordArgument(key: "dueDate", value: Date()), + KeywordArgument(key: "modifiedAt", value: Date()), + KeywordArgument(key: "status", value: ToDoStatus.toDo.rawValue) + ] + self?.viewModel.createData(values: testValue) +#endif + } navigationItem.rightBarButtonItem = UIBarButtonItem(primaryAction: addToDo) } - private func readData() { - viewModel.fetchData() - } - private func setupBinding() { - viewModel.error.bind { [weak self] _ in + viewModel.error.bind { [weak self] error in guard let self, - let error = viewModel.error.value else { return } - let alertBuilder = AlertBuilder(viewController: self, prefferedStyle: .alert) + let error else { return } + let alertBuilder = AlertBuilder(prefferedStyle: .alert) alertBuilder.setControllerTitle(title: error.alertTitle) alertBuilder.setControllerMessage(message: error.alertMessage) alertBuilder.addAction(.confirm) diff --git a/ProjectManager/ProjectManager/View/List/BaseView/ToDoListBaseViewModel.swift b/ProjectManager/ProjectManager/View/ListView/BaseView/ToDoListBaseViewModel.swift similarity index 57% rename from ProjectManager/ProjectManager/View/List/BaseView/ToDoListBaseViewModel.swift rename to ProjectManager/ProjectManager/View/ListView/BaseView/ToDoListBaseViewModel.swift index 78ec65a84..477c61505 100644 --- a/ProjectManager/ProjectManager/View/List/BaseView/ToDoListBaseViewModel.swift +++ b/ProjectManager/ProjectManager/View/ListView/BaseView/ToDoListBaseViewModel.swift @@ -7,39 +7,43 @@ import CoreData -protocol ToDoListBaseViewModelDelegate: AnyObject { - func fetchData() - func fetchDataByStatus(for status: ToDoStatus) - func createData(values: [KeywordArgument]) - func updateData(_ entity: ToDo, values: [KeywordArgument]) - func deleteData(_ entity: ToDo) -} - -final class ToDoListBaseViewModel: ViewModelProtocol, ToDoListBaseViewModelDelegate { +final class ToDoListBaseViewModel: ViewModelType { private let coreDataManager: CoreDataManager private var children: [ToDoStatus: ToDoListChildViewModel] = [:] - - var entityList: Observable<[ToDo]> = Observable([]) - var errorMessage: Observable = Observable(nil) + var error: Observable = Observable(nil) init(dataManager: CoreDataManager) { coreDataManager = dataManager } - func fetchData() { + func handle(error: Error) { + if let coreDataError = error as? CoreDataError { + self.setError(coreDataError) + } else { + self.setError(CoreDataError.unknown) + } + } + + func setError(_ error: CoreDataError) { + self.error = Observable(error) + } +} + +extension ToDoListBaseViewModel: ToDoListBaseViewModelDelegate { + func fetchAllData() { ToDoStatus.allCases.forEach { fetchDataByStatus(for: $0) } } func fetchDataByStatus(for status: ToDoStatus) { do { let predicated = NSPredicate(format: "status == %@", status.rawValue) - let filtered = try coreDataManager.fetchData(entityName:"ToDo", predicate: predicated, sort: "createdAt") - + let filtered = try coreDataManager.fetchData(entityName:"ToDo", predicate: predicated, sort: "modifiedAt") + guard let result = filtered as? [ToDo] else { return } - children[status]?.entityList.value = result - + children[status]?.entityList = result + } catch(let error) { handle(error: error) } @@ -52,62 +56,66 @@ final class ToDoListBaseViewModel: ViewModelProtocol, ToDoListBaseViewModelDeleg values.append(KeywordArgument(key: "id", value: UUID())) } - if values.filter({ $0.key == "createdAt" }).isEmpty { - values.append(KeywordArgument(key: "createdAt", value: Date())) + if values.filter({ $0.key == "modifiedAt" }).isEmpty { + values.append(KeywordArgument(key: "modifiedAt", value: Date())) } if values.filter({ $0.key == "status" }).isEmpty { values.append(KeywordArgument(key: "status", value: ToDoStatus.toDo.rawValue)) } - + do { try coreDataManager.createData(type: ToDo.self, values: values) - fetchDataByStatus(for: .toDo) + updateChild(.toDo, action: Action(type: .create)) } catch(let error) { handle(error: error) } } - func updateData(_ entity: ToDo, values: [KeywordArgument]) { + func updateData(_ entity: ToDo, values: [KeywordArgument], from childKey: ToDoStatus) { do { try coreDataManager.updateData(entity: entity, values: values) + updateChild(childKey, action: Action(type: .update)) } catch(let error) { handle(error: error) } } - func deleteData(_ entity: ToDo) { + func deleteData(_ entity: ToDo, at index: Int, from childKey: ToDoStatus) { do { try coreDataManager.deleteData(entity: entity) + updateChild(childKey, action: Action(type: .delete, extraInformation: [KeywordArgument(key: "index", value: index)])) } catch(let error) { handle(error: error) } } -} + func changeStatus(_ entity: ToDo, at index: Int, from oldStatus: ToDoStatus, to newStatus: ToDoStatus) { + do { + try coreDataManager.updateData(entity: entity, values: [KeywordArgument(key: "status", value: newStatus.rawValue)]) + updateChild(oldStatus, action: Action(type: .delete, extraInformation: [KeywordArgument(key: "index", value: index)])) + updateChild(newStatus, action: Action(type: .create)) + } catch(let error) { + handle(error: error) + } + } +} + extension ToDoListBaseViewModel { - func addChildModel(status: ToDoStatus) -> ToDoListChildViewModel { + func addChild(_ status: ToDoStatus) -> ToDoListChildViewModel { let child = ToDoListChildViewModel(status: status) children[status] = child child.delegate = self #if DEBUG child.addTestData() + updateChild(status, action: Action(type: .create)) #endif return child } -} - -extension ToDoListBaseViewModel { - func handle(error: Error) { - if let coreDataError = error as? CoreDataError { - self.setError(coreDataError) - } else { - self.setError(CoreDataError.unknown) - } - } - func setError(_ error: CoreDataError) { - self.errorMessage = Observable(error.alertMessage) - self.error = Observable(error) + func updateChild(_ status: ToDoStatus, action: Action) { + fetchDataByStatus(for: status) + children[status]?.action.value = action } } + diff --git a/ProjectManager/ProjectManager/View/List/Cell/ToDoListHeaderView.swift b/ProjectManager/ProjectManager/View/ListView/Cell/ToDoListHeaderView.swift similarity index 97% rename from ProjectManager/ProjectManager/View/List/Cell/ToDoListHeaderView.swift rename to ProjectManager/ProjectManager/View/ListView/Cell/ToDoListHeaderView.swift index 5c568d6fb..de515cf2b 100644 --- a/ProjectManager/ProjectManager/View/List/Cell/ToDoListHeaderView.swift +++ b/ProjectManager/ProjectManager/View/ListView/Cell/ToDoListHeaderView.swift @@ -7,7 +7,7 @@ import UIKit -class ToDoListHeaderView: UIView { +final class ToDoListHeaderView: UIView { private let status: ToDoStatus private let headerTitleLabel: UILabel = { diff --git a/ProjectManager/ProjectManager/View/List/Cell/ToDoListTableViewCell.swift b/ProjectManager/ProjectManager/View/ListView/Cell/ToDoListTableViewCell.swift similarity index 97% rename from ProjectManager/ProjectManager/View/List/Cell/ToDoListTableViewCell.swift rename to ProjectManager/ProjectManager/View/ListView/Cell/ToDoListTableViewCell.swift index 2a68c78f0..521fdd9dd 100644 --- a/ProjectManager/ProjectManager/View/List/Cell/ToDoListTableViewCell.swift +++ b/ProjectManager/ProjectManager/View/ListView/Cell/ToDoListTableViewCell.swift @@ -7,7 +7,7 @@ import UIKit -class ToDoListTableViewCell: UITableViewCell { +final class ToDoListTableViewCell: UITableViewCell { private let titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false diff --git a/ProjectManager/ProjectManager/View/List/ChildView/ToDoListChildViewController.swift b/ProjectManager/ProjectManager/View/ListView/ChildView/ToDoListChildViewController.swift similarity index 76% rename from ProjectManager/ProjectManager/View/List/ChildView/ToDoListChildViewController.swift rename to ProjectManager/ProjectManager/View/ListView/ChildView/ToDoListChildViewController.swift index f62118ac8..6460d8d8d 100644 --- a/ProjectManager/ProjectManager/View/List/ChildView/ToDoListChildViewController.swift +++ b/ProjectManager/ProjectManager/View/ListView/ChildView/ToDoListChildViewController.swift @@ -7,7 +7,7 @@ import UIKit -class ToDoListChildViewController: UIViewController { +final class ToDoListChildViewController: UIViewController { private let status: ToDoStatus private let headerView: ToDoListHeaderView private let viewModel: ToDoListChildViewModel @@ -61,16 +61,11 @@ class ToDoListChildViewController: UIViewController { tableView.delegate = self tableView.register(ToDoListTableViewCell.self, forCellReuseIdentifier: status.rawValue) } - - func reloadTableView() { - tableView.reloadData() - headerView.setupTotalCount(viewModel.entityList.value.count) - } } extension ToDoListChildViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - viewModel.entityList.value.count + viewModel.entityList.count } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { @@ -82,7 +77,7 @@ extension ToDoListChildViewController: UITableViewDelegate, UITableViewDataSourc for: indexPath) as? ToDoListTableViewCell else { return UITableViewCell() } - let toDoEntity = viewModel.entityList.value[indexPath.row] + let toDoEntity = viewModel.entityList[indexPath.row] let isDone = toDoEntity.status == ToDoStatus.done.rawValue let isPast = floor(today/86400) > floor(toDoEntity.dueDate.timeIntervalSinceReferenceDate/86400) && !isDone let date = dateFormatter.string(from: toDoEntity.dueDate) @@ -96,7 +91,7 @@ extension ToDoListChildViewController: UITableViewDelegate, UITableViewDataSourc func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let delete = UIContextualAction(style: .normal, title: "") { (_, _, success: @escaping (Bool) -> Void) in - let selectedEntity = self.viewModel.entityList.value[indexPath.row] + let selectedEntity = self.viewModel.entityList[indexPath.row] self.viewModel.deleteData(selectedEntity) } @@ -109,15 +104,29 @@ extension ToDoListChildViewController: UITableViewDelegate, UITableViewDataSourc extension ToDoListChildViewController { private func setupBinding() { - viewModel.entityList.bind { [weak self] _ in - guard let self else { return } - self.reloadTableView() + viewModel.action.bind { [weak self] action in + guard let self, + let action else { return } + + switch action.type { + case .create: + let index = self.viewModel.entityList.count - 1 + self.tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .fade) + case .update: + self.tableView.reloadData() + case .delete: + guard let indexInformation = action.extraInformation.filter({ $0.key == "index" }).first, + let index = indexInformation.value as? Int else { return } + self.tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .fade) + } + + self.headerView.setupTotalCount(viewModel.entityList.count) } - viewModel.error.bind { [weak self] _ in + viewModel.error.bind { [weak self] error in guard let self, - let error = viewModel.error.value else { return } - let alertBuilder = AlertBuilder(viewController: self, prefferedStyle: .alert) + let error else { return } + let alertBuilder = AlertBuilder(prefferedStyle: .alert) alertBuilder.setControllerTitle(title: error.alertTitle) alertBuilder.setControllerMessage(message: error.alertMessage) alertBuilder.addAction(.confirm) diff --git a/ProjectManager/ProjectManager/View/List/ChildView/ToDoListChildViewModel.swift b/ProjectManager/ProjectManager/View/ListView/ChildView/ToDoListChildViewModel.swift similarity index 71% rename from ProjectManager/ProjectManager/View/List/ChildView/ToDoListChildViewModel.swift rename to ProjectManager/ProjectManager/View/ListView/ChildView/ToDoListChildViewModel.swift index 765929472..34cd2fb0d 100644 --- a/ProjectManager/ProjectManager/View/List/ChildView/ToDoListChildViewModel.swift +++ b/ProjectManager/ProjectManager/View/ListView/ChildView/ToDoListChildViewModel.swift @@ -7,37 +7,18 @@ import Foundation -final class ToDoListChildViewModel: ViewModelProtocol { +final class ToDoListChildViewModel: ViewModelType { private let status: ToDoStatus - var entityList: Observable<[ToDo]> = Observable([]) - var errorMessage: Observable = Observable(nil) - var error: Observable = Observable(nil) + var entityList: [ToDo] = [] + var action: Observable = Observable(nil) weak var delegate: ToDoListBaseViewModelDelegate? + var error: Observable = Observable(nil) + init(status: ToDoStatus) { self.status = status } - func fetchData() { - delegate?.fetchDataByStatus(for: status) - } - - func createData(values: [KeywordArgument]) { - delegate?.createData(values: values) - } - - func updateData(_ entity: ToDo, values: [KeywordArgument]) { - delegate?.updateData(entity, values: values) - fetchData() - } - - func deleteData(_ entity: ToDo) { - delegate?.deleteData(entity) - fetchData() - } -} - -extension ToDoListChildViewModel { func handle(error: Error) { if let coreDataError = error as? CoreDataError { self.setError(coreDataError) @@ -47,21 +28,35 @@ extension ToDoListChildViewModel { } func setError(_ error: CoreDataError) { - self.errorMessage = Observable(error.alertMessage) self.error = Observable(error) } } +extension ToDoListChildViewModel { + func updateData(_ entity: ToDo, values: [KeywordArgument]) { + delegate?.updateData(entity, values: values, from: status) + } + + func deleteData(_ entity: ToDo) { + guard let index = entityList.firstIndex(of: entity) else { return } + delegate?.deleteData(entity, at: index, from: status) + } + + func changeStatus(_ entity: ToDo, to newStatus: ToDoStatus) { + guard let index = entityList.firstIndex(of: entity) else { return } + delegate?.changeStatus(entity, at: index, from: status, to: newStatus) + } +} + #if DEBUG extension ToDoListChildViewModel { func addTestData() { - // 불러오기 테스트용 let values: [KeywordArgument] = [ KeywordArgument(key: "id", value: UUID()), KeywordArgument(key: "title", value: "\(status.rawValue) 테스트1"), KeywordArgument(key: "body", value: "테스트용입니다"), KeywordArgument(key: "dueDate", value: Date()), - KeywordArgument(key: "createdAt", value: Date()), + KeywordArgument(key: "modifiedAt", value: Date()), KeywordArgument(key: "status", value: status.rawValue) ] @@ -70,14 +65,12 @@ extension ToDoListChildViewModel { KeywordArgument(key: "title", value: "\(status.rawValue) 테스트2"), KeywordArgument(key: "body", value: "테스트용입니다2"), KeywordArgument(key: "dueDate", value: Date()), - KeywordArgument(key: "createdAt", value: Date()), + KeywordArgument(key: "modifiedAt", value: Date()), KeywordArgument(key: "status", value: status.rawValue) ] delegate?.createData(values: values) delegate?.createData(values: values2) - - fetchData() } } #endif diff --git a/ProjectManager/ProjectManager/View/List/ToDoListViewController.swift b/ProjectManager/ProjectManager/View/ListView/ToDoListViewController.swift similarity index 100% rename from ProjectManager/ProjectManager/View/List/ToDoListViewController.swift rename to ProjectManager/ProjectManager/View/ListView/ToDoListViewController.swift diff --git a/ProjectManager/ProjectManager/View/Protocol/ToDoListBaseViewModelDelegate.swift b/ProjectManager/ProjectManager/View/Protocol/ToDoListBaseViewModelDelegate.swift new file mode 100644 index 000000000..165355e9e --- /dev/null +++ b/ProjectManager/ProjectManager/View/Protocol/ToDoListBaseViewModelDelegate.swift @@ -0,0 +1,13 @@ +// +// ToDoListViewModelDelegate.swift +// ProjectManager +// +// Created by Max on 2023/10/03. +// + +protocol ToDoListBaseViewModelDelegate: AnyObject { + func createData(values: [KeywordArgument]) + func updateData(_ entity: ToDo, values: [KeywordArgument], from childKey: ToDoStatus) + func deleteData(_ entity: ToDo, at index: Int, from childKey: ToDoStatus) + func changeStatus(_ entity: ToDo, at index: Int, from oldStatus: ToDoStatus, to newStatus: ToDoStatus) +} diff --git a/ProjectManager/ProjectManager/View/Protocol/ViewModelType.swift b/ProjectManager/ProjectManager/View/Protocol/ViewModelType.swift new file mode 100644 index 000000000..c10b774df --- /dev/null +++ b/ProjectManager/ProjectManager/View/Protocol/ViewModelType.swift @@ -0,0 +1,17 @@ +// +// ViewModel.swift +// ProjectManager +// +// Created by Max on 2023/09/24. +// + +import Foundation + +protocol ViewModelType { + associatedtype ViewModelError + + var error: Observable { get set } + + func handle(error: Error) + func setError(_ error: ViewModelError) +} diff --git a/ProjectManager/ProjectManager/View/ViewModelProtocol.swift b/ProjectManager/ProjectManager/View/ViewModelProtocol.swift deleted file mode 100644 index 43bdd26d4..000000000 --- a/ProjectManager/ProjectManager/View/ViewModelProtocol.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ViewModel.swift -// ProjectManager -// -// Created by Max on 2023/09/24. -// - -import Foundation - -protocol ViewModelProtocol { - associatedtype Entity - associatedtype ViewModelError - - var entityList: Observable<[Entity]> { get set } - var errorMessage: Observable { get set } - var error: Observable { get set } - - func fetchData() - func createData(values: [KeywordArgument]) - func updateData(_ entity: Entity, values: [KeywordArgument]) - func deleteData(_ entity: Entity) - func handle(error: Error) - func setError(_ error: ViewModelError) -} From 829791e5ba94662a29a0aa2a911e8613b92b6a3b Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Tue, 3 Oct 2023 12:46:03 +0900 Subject: [PATCH 17/20] =?UTF-8?q?chore:=20=ED=8C=8C=EC=9D=BC=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectManager.xcodeproj/project.pbxproj | 60 ++++++++++++++----- .../CoreData/ToDo+CoreDataClass.swift | 0 .../CoreData/ToDo+CoreDataProperties.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../{Model => Utility}/Action.swift | 0 .../Builder/AlertBuilder.swift | 0 .../Error}/CoreDataError.swift | 0 .../{Model => Utility}/KeywordArgument.swift | 0 .../Manager}/CoreDataManager.swift | 0 .../{Model => Utility}/Observable.swift | 0 .../ToDoListBaseViewModelDelegate.swift | 0 .../Protocol/ViewModelType.swift | 0 .../{Model => Utility}/ToDoStatus.swift | 0 16 files changed, 46 insertions(+), 14 deletions(-) rename ProjectManager/ProjectManager/{Model => Entity}/CoreData/ToDo+CoreDataClass.swift (100%) rename ProjectManager/ProjectManager/{Model => Entity}/CoreData/ToDo+CoreDataProperties.swift (100%) rename ProjectManager/ProjectManager/{ => Resource}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename ProjectManager/ProjectManager/{ => Resource}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename ProjectManager/ProjectManager/{ => Resource}/Assets.xcassets/Contents.json (100%) rename ProjectManager/ProjectManager/{ => Resource}/Base.lproj/LaunchScreen.storyboard (100%) rename ProjectManager/ProjectManager/{Model => Utility}/Action.swift (100%) rename ProjectManager/ProjectManager/{View => Utility}/Builder/AlertBuilder.swift (100%) rename ProjectManager/ProjectManager/{Model/CoreData => Utility/Error}/CoreDataError.swift (100%) rename ProjectManager/ProjectManager/{Model => Utility}/KeywordArgument.swift (100%) rename ProjectManager/ProjectManager/{Model/CoreData => Utility/Manager}/CoreDataManager.swift (100%) rename ProjectManager/ProjectManager/{Model => Utility}/Observable.swift (100%) rename ProjectManager/ProjectManager/{View => Utility}/Protocol/ToDoListBaseViewModelDelegate.swift (100%) rename ProjectManager/ProjectManager/{View => Utility}/Protocol/ViewModelType.swift (100%) rename ProjectManager/ProjectManager/{Model => Utility}/ToDoStatus.swift (100%) diff --git a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj index 4a70fcb1e..6fa9b31c9 100644 --- a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj +++ b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj @@ -79,16 +79,12 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - BA6463612AC043CE0080E80D /* Model */ = { + BA6463612AC043CE0080E80D /* Entity */ = { isa = PBXGroup; children = ( BA6463622AC043D30080E80D /* CoreData */, - BA6463672AC044D70080E80D /* ToDoStatus.swift */, - BA6463692AC045380080E80D /* Observable.swift */, - BA6463802AC289680080E80D /* KeywordArgument.swift */, - BAD471D72ACBA4820021323A /* Action.swift */, ); - path = Model; + path = Entity; sourceTree = ""; }; BA6463622AC043D30080E80D /* CoreData */ = { @@ -96,8 +92,6 @@ children = ( BA64635D2AC043680080E80D /* ToDo+CoreDataClass.swift */, BA64635E2AC043680080E80D /* ToDo+CoreDataProperties.swift */, - BA6463632AC043F50080E80D /* CoreDataManager.swift */, - BA6463652AC0440E0080E80D /* CoreDataError.swift */, ); path = CoreData; sourceTree = ""; @@ -159,8 +153,6 @@ BA6463842AC2A3610080E80D /* View */ = { isa = PBXGroup; children = ( - BA6463762AC04FDA0080E80D /* Builder */, - BAD471D42ACBA2B80021323A /* Protocol */, BA6463702AC04AAB0080E80D /* ListView */, ); path = View; @@ -175,6 +167,46 @@ path = Protocol; sourceTree = ""; }; + BAD471D92ACBC3A80021323A /* Utility */ = { + isa = PBXGroup; + children = ( + BAD471D42ACBA2B80021323A /* Protocol */, + BA6463762AC04FDA0080E80D /* Builder */, + BAD471DD2ACBC5220021323A /* Manager */, + BAD471DC2ACBC5080021323A /* Error */, + BA6463672AC044D70080E80D /* ToDoStatus.swift */, + BA6463692AC045380080E80D /* Observable.swift */, + BA6463802AC289680080E80D /* KeywordArgument.swift */, + BAD471D72ACBA4820021323A /* Action.swift */, + ); + path = Utility; + sourceTree = ""; + }; + BAD471DA2ACBC4550021323A /* Resource */ = { + isa = PBXGroup; + children = ( + C7431F1025F51E1E0094C4CF /* LaunchScreen.storyboard */, + C7431F0E25F51E1E0094C4CF /* Assets.xcassets */, + ); + path = Resource; + sourceTree = ""; + }; + BAD471DC2ACBC5080021323A /* Error */ = { + isa = PBXGroup; + children = ( + BA6463652AC0440E0080E80D /* CoreDataError.swift */, + ); + path = Error; + sourceTree = ""; + }; + BAD471DD2ACBC5220021323A /* Manager */ = { + isa = PBXGroup; + children = ( + BA6463632AC043F50080E80D /* CoreDataManager.swift */, + ); + path = Manager; + sourceTree = ""; + }; C7431EF925F51E1D0094C4CF = { isa = PBXGroup; children = ( @@ -195,12 +227,12 @@ isa = PBXGroup; children = ( BA6463712AC04AC50080E80D /* App */, - BA6463612AC043CE0080E80D /* Model */, + BA6463612AC043CE0080E80D /* Entity */, + BAD471D92ACBC3A80021323A /* Utility */, BA6463842AC2A3610080E80D /* View */, - C7431F0E25F51E1E0094C4CF /* Assets.xcassets */, - C7431F1025F51E1E0094C4CF /* LaunchScreen.storyboard */, - C7431F1325F51E1E0094C4CF /* Info.plist */, + BAD471DA2ACBC4550021323A /* Resource */, BA6463562AC042A90080E80D /* ToDo.xcdatamodeld */, + C7431F1325F51E1E0094C4CF /* Info.plist */, ); path = ProjectManager; sourceTree = ""; diff --git a/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataClass.swift b/ProjectManager/ProjectManager/Entity/CoreData/ToDo+CoreDataClass.swift similarity index 100% rename from ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataClass.swift rename to ProjectManager/ProjectManager/Entity/CoreData/ToDo+CoreDataClass.swift diff --git a/ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift b/ProjectManager/ProjectManager/Entity/CoreData/ToDo+CoreDataProperties.swift similarity index 100% rename from ProjectManager/ProjectManager/Model/CoreData/ToDo+CoreDataProperties.swift rename to ProjectManager/ProjectManager/Entity/CoreData/ToDo+CoreDataProperties.swift diff --git a/ProjectManager/ProjectManager/Assets.xcassets/AccentColor.colorset/Contents.json b/ProjectManager/ProjectManager/Resource/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from ProjectManager/ProjectManager/Assets.xcassets/AccentColor.colorset/Contents.json rename to ProjectManager/ProjectManager/Resource/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/ProjectManager/ProjectManager/Assets.xcassets/AppIcon.appiconset/Contents.json b/ProjectManager/ProjectManager/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from ProjectManager/ProjectManager/Assets.xcassets/AppIcon.appiconset/Contents.json rename to ProjectManager/ProjectManager/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/ProjectManager/ProjectManager/Assets.xcassets/Contents.json b/ProjectManager/ProjectManager/Resource/Assets.xcassets/Contents.json similarity index 100% rename from ProjectManager/ProjectManager/Assets.xcassets/Contents.json rename to ProjectManager/ProjectManager/Resource/Assets.xcassets/Contents.json diff --git a/ProjectManager/ProjectManager/Base.lproj/LaunchScreen.storyboard b/ProjectManager/ProjectManager/Resource/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from ProjectManager/ProjectManager/Base.lproj/LaunchScreen.storyboard rename to ProjectManager/ProjectManager/Resource/Base.lproj/LaunchScreen.storyboard diff --git a/ProjectManager/ProjectManager/Model/Action.swift b/ProjectManager/ProjectManager/Utility/Action.swift similarity index 100% rename from ProjectManager/ProjectManager/Model/Action.swift rename to ProjectManager/ProjectManager/Utility/Action.swift diff --git a/ProjectManager/ProjectManager/View/Builder/AlertBuilder.swift b/ProjectManager/ProjectManager/Utility/Builder/AlertBuilder.swift similarity index 100% rename from ProjectManager/ProjectManager/View/Builder/AlertBuilder.swift rename to ProjectManager/ProjectManager/Utility/Builder/AlertBuilder.swift diff --git a/ProjectManager/ProjectManager/Model/CoreData/CoreDataError.swift b/ProjectManager/ProjectManager/Utility/Error/CoreDataError.swift similarity index 100% rename from ProjectManager/ProjectManager/Model/CoreData/CoreDataError.swift rename to ProjectManager/ProjectManager/Utility/Error/CoreDataError.swift diff --git a/ProjectManager/ProjectManager/Model/KeywordArgument.swift b/ProjectManager/ProjectManager/Utility/KeywordArgument.swift similarity index 100% rename from ProjectManager/ProjectManager/Model/KeywordArgument.swift rename to ProjectManager/ProjectManager/Utility/KeywordArgument.swift diff --git a/ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift b/ProjectManager/ProjectManager/Utility/Manager/CoreDataManager.swift similarity index 100% rename from ProjectManager/ProjectManager/Model/CoreData/CoreDataManager.swift rename to ProjectManager/ProjectManager/Utility/Manager/CoreDataManager.swift diff --git a/ProjectManager/ProjectManager/Model/Observable.swift b/ProjectManager/ProjectManager/Utility/Observable.swift similarity index 100% rename from ProjectManager/ProjectManager/Model/Observable.swift rename to ProjectManager/ProjectManager/Utility/Observable.swift diff --git a/ProjectManager/ProjectManager/View/Protocol/ToDoListBaseViewModelDelegate.swift b/ProjectManager/ProjectManager/Utility/Protocol/ToDoListBaseViewModelDelegate.swift similarity index 100% rename from ProjectManager/ProjectManager/View/Protocol/ToDoListBaseViewModelDelegate.swift rename to ProjectManager/ProjectManager/Utility/Protocol/ToDoListBaseViewModelDelegate.swift diff --git a/ProjectManager/ProjectManager/View/Protocol/ViewModelType.swift b/ProjectManager/ProjectManager/Utility/Protocol/ViewModelType.swift similarity index 100% rename from ProjectManager/ProjectManager/View/Protocol/ViewModelType.swift rename to ProjectManager/ProjectManager/Utility/Protocol/ViewModelType.swift diff --git a/ProjectManager/ProjectManager/Model/ToDoStatus.swift b/ProjectManager/ProjectManager/Utility/ToDoStatus.swift similarity index 100% rename from ProjectManager/ProjectManager/Model/ToDoStatus.swift rename to ProjectManager/ProjectManager/Utility/ToDoStatus.swift From bfd087b518772a5430451a8aa1f7b8c084f4e7f2 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Thu, 5 Oct 2023 23:25:37 +0900 Subject: [PATCH 18/20] =?UTF-8?q?refactor:=20Input/Output=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20ViewModel=20=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectManager.xcodeproj/project.pbxproj | 68 +++++----- .../ProjectManager/App/SceneDelegate.swift | 3 +- .../Manager => Domain}/CoreDataManager.swift | 0 .../Entity/CoreData/ToDo+CoreDataClass.swift | 0 .../CoreData/ToDo+CoreDataProperties.swift | 0 .../ProjectManager/Domain/ToDoUseCase.swift | 51 ++++++++ .../BaseView/ToDoListBaseViewController.swift | 14 +- .../BaseView/ToDoListBaseViewModel.swift | 73 +++++++++++ .../ListView/Cell/ToDoListHeaderView.swift | 0 .../ListView/Cell/ToDoListTableViewCell.swift | 0 .../ToDoListChildViewController.swift | 27 ++-- .../ChildView/ToDoListChildViewModel.swift | 48 +++++-- .../ListView/ToDoListViewController.swift | 0 .../ToDoListBaseViewModelDelegate.swift | 13 ++ .../ViewModelProtocol/ViewModelType.swift | 47 +++++++ .../Utility/{Builder => }/AlertBuilder.swift | 0 .../Utility/{Error => }/CoreDataError.swift | 0 .../Utility/{Action.swift => Output.swift} | 3 +- .../ToDoListBaseViewModelDelegate.swift | 13 -- .../Utility/Protocol/ViewModelType.swift | 17 --- .../BaseView/ToDoListBaseViewModel.swift | 121 ------------------ 21 files changed, 278 insertions(+), 220 deletions(-) rename ProjectManager/ProjectManager/{Utility/Manager => Domain}/CoreDataManager.swift (100%) rename ProjectManager/ProjectManager/{ => Domain}/Entity/CoreData/ToDo+CoreDataClass.swift (100%) rename ProjectManager/ProjectManager/{ => Domain}/Entity/CoreData/ToDo+CoreDataProperties.swift (100%) create mode 100644 ProjectManager/ProjectManager/Domain/ToDoUseCase.swift rename ProjectManager/ProjectManager/{ => Presentation}/View/ListView/BaseView/ToDoListBaseViewController.swift (91%) create mode 100644 ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewModel.swift rename ProjectManager/ProjectManager/{ => Presentation}/View/ListView/Cell/ToDoListHeaderView.swift (100%) rename ProjectManager/ProjectManager/{ => Presentation}/View/ListView/Cell/ToDoListTableViewCell.swift (100%) rename ProjectManager/ProjectManager/{ => Presentation}/View/ListView/ChildView/ToDoListChildViewController.swift (84%) rename ProjectManager/ProjectManager/{ => Presentation}/View/ListView/ChildView/ToDoListChildViewModel.swift (58%) rename ProjectManager/ProjectManager/{ => Presentation}/View/ListView/ToDoListViewController.swift (100%) create mode 100644 ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ToDoListBaseViewModelDelegate.swift create mode 100644 ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ViewModelType.swift rename ProjectManager/ProjectManager/Utility/{Builder => }/AlertBuilder.swift (100%) rename ProjectManager/ProjectManager/Utility/{Error => }/CoreDataError.swift (100%) rename ProjectManager/ProjectManager/Utility/{Action.swift => Output.swift} (92%) delete mode 100644 ProjectManager/ProjectManager/Utility/Protocol/ToDoListBaseViewModelDelegate.swift delete mode 100644 ProjectManager/ProjectManager/Utility/Protocol/ViewModelType.swift delete mode 100644 ProjectManager/ProjectManager/View/ListView/BaseView/ToDoListBaseViewModel.swift diff --git a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj index 6fa9b31c9..b2a4e15b5 100644 --- a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj +++ b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj @@ -28,7 +28,8 @@ BA64637F2AC283F70080E80D /* ToDoListChildViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64637E2AC283F70080E80D /* ToDoListChildViewModel.swift */; }; BA6463812AC289680080E80D /* KeywordArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463802AC289680080E80D /* KeywordArgument.swift */; }; BAD471D62ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD471D52ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift */; }; - BAD471D82ACBA4820021323A /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD471D72ACBA4820021323A /* Action.swift */; }; + BAD471D82ACBA4820021323A /* Output.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD471D72ACBA4820021323A /* Output.swift */; }; + BAD471DF2ACF19EA0021323A /* ToDoUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD471DE2ACF19EA0021323A /* ToDoUseCase.swift */; }; C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0525F51E1D0094C4CF /* AppDelegate.swift */; }; C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0725F51E1D0094C4CF /* SceneDelegate.swift */; }; C7431F0A25F51E1D0094C4CF /* ToDoListBaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0925F51E1D0094C4CF /* ToDoListBaseViewController.swift */; }; @@ -53,7 +54,8 @@ BA64637E2AC283F70080E80D /* ToDoListChildViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListChildViewModel.swift; sourceTree = ""; }; BA6463802AC289680080E80D /* KeywordArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordArgument.swift; sourceTree = ""; }; BAD471D52ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListBaseViewModelDelegate.swift; sourceTree = ""; }; - BAD471D72ACBA4820021323A /* Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = ""; }; + BAD471D72ACBA4820021323A /* Output.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Output.swift; sourceTree = ""; }; + BAD471DE2ACF19EA0021323A /* ToDoUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoUseCase.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 = ""; }; @@ -115,14 +117,6 @@ path = App; sourceTree = ""; }; - BA6463762AC04FDA0080E80D /* Builder */ = { - isa = PBXGroup; - children = ( - BA6463772AC04FE40080E80D /* AlertBuilder.swift */, - ); - path = Builder; - sourceTree = ""; - }; BA64637B2AC0613D0080E80D /* Cell */ = { isa = PBXGroup; children = ( @@ -158,53 +152,54 @@ path = View; sourceTree = ""; }; - BAD471D42ACBA2B80021323A /* Protocol */ = { + BAD471D42ACBA2B80021323A /* ViewModelProtocol */ = { isa = PBXGroup; children = ( BA64636C2AC045700080E80D /* ViewModelType.swift */, BAD471D52ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift */, ); - path = Protocol; + path = ViewModelProtocol; sourceTree = ""; }; - BAD471D92ACBC3A80021323A /* Utility */ = { + BAD471DA2ACBC4550021323A /* Resource */ = { isa = PBXGroup; children = ( - BAD471D42ACBA2B80021323A /* Protocol */, - BA6463762AC04FDA0080E80D /* Builder */, - BAD471DD2ACBC5220021323A /* Manager */, - BAD471DC2ACBC5080021323A /* Error */, - BA6463672AC044D70080E80D /* ToDoStatus.swift */, - BA6463692AC045380080E80D /* Observable.swift */, - BA6463802AC289680080E80D /* KeywordArgument.swift */, - BAD471D72ACBA4820021323A /* Action.swift */, + C7431F1025F51E1E0094C4CF /* LaunchScreen.storyboard */, + C7431F0E25F51E1E0094C4CF /* Assets.xcassets */, ); - path = Utility; + path = Resource; sourceTree = ""; }; - BAD471DA2ACBC4550021323A /* Resource */ = { + BAD471E12ACF280B0021323A /* Domain */ = { isa = PBXGroup; children = ( - C7431F1025F51E1E0094C4CF /* LaunchScreen.storyboard */, - C7431F0E25F51E1E0094C4CF /* Assets.xcassets */, + BA6463612AC043CE0080E80D /* Entity */, + BA6463632AC043F50080E80D /* CoreDataManager.swift */, + BAD471DE2ACF19EA0021323A /* ToDoUseCase.swift */, ); - path = Resource; + path = Domain; sourceTree = ""; }; - BAD471DC2ACBC5080021323A /* Error */ = { + BAD471E22ACF28150021323A /* Presentation */ = { isa = PBXGroup; children = ( - BA6463652AC0440E0080E80D /* CoreDataError.swift */, + BAD471D42ACBA2B80021323A /* ViewModelProtocol */, + BA6463842AC2A3610080E80D /* View */, ); - path = Error; + path = Presentation; sourceTree = ""; }; - BAD471DD2ACBC5220021323A /* Manager */ = { + BAD471E32ACF28CC0021323A /* Utility */ = { isa = PBXGroup; children = ( - BA6463632AC043F50080E80D /* CoreDataManager.swift */, + BA6463652AC0440E0080E80D /* CoreDataError.swift */, + BA6463672AC044D70080E80D /* ToDoStatus.swift */, + BA6463802AC289680080E80D /* KeywordArgument.swift */, + BA6463692AC045380080E80D /* Observable.swift */, + BAD471D72ACBA4820021323A /* Output.swift */, + BA6463772AC04FE40080E80D /* AlertBuilder.swift */, ); - path = Manager; + path = Utility; sourceTree = ""; }; C7431EF925F51E1D0094C4CF = { @@ -227,9 +222,9 @@ isa = PBXGroup; children = ( BA6463712AC04AC50080E80D /* App */, - BA6463612AC043CE0080E80D /* Entity */, - BAD471D92ACBC3A80021323A /* Utility */, - BA6463842AC2A3610080E80D /* View */, + BAD471E12ACF280B0021323A /* Domain */, + BAD471E32ACF28CC0021323A /* Utility */, + BAD471E22ACF28150021323A /* Presentation */, BAD471DA2ACBC4550021323A /* Resource */, BA6463562AC042A90080E80D /* ToDo.xcdatamodeld */, C7431F1325F51E1E0094C4CF /* Info.plist */, @@ -329,8 +324,9 @@ BA6463602AC043680080E80D /* ToDo+CoreDataProperties.swift in Sources */, BAD471D62ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift in Sources */, BA6463682AC044D70080E80D /* ToDoStatus.swift in Sources */, - BAD471D82ACBA4820021323A /* Action.swift in Sources */, + BAD471D82ACBA4820021323A /* Output.swift in Sources */, BA6463782AC04FE40080E80D /* AlertBuilder.swift in Sources */, + BAD471DF2ACF19EA0021323A /* ToDoUseCase.swift in Sources */, BA64636A2AC045380080E80D /* Observable.swift in Sources */, BA64635F2AC043680080E80D /* ToDo+CoreDataClass.swift in Sources */, BA6463642AC043F50080E80D /* CoreDataManager.swift in Sources */, diff --git a/ProjectManager/ProjectManager/App/SceneDelegate.swift b/ProjectManager/ProjectManager/App/SceneDelegate.swift index 964745e03..c586ffa57 100644 --- a/ProjectManager/ProjectManager/App/SceneDelegate.swift +++ b/ProjectManager/ProjectManager/App/SceneDelegate.swift @@ -16,7 +16,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window = UIWindow(windowScene: windowScene) let coreDataManager = CoreDataManager() - let toDoViewModel = ToDoListBaseViewModel(dataManager: coreDataManager) + let useCase = ToDoUseCase(dataManager: coreDataManager) + let toDoViewModel = ToDoListBaseViewModel(dataManager: coreDataManager, useCase: useCase) let baseViewController = ToDoListBaseViewController(toDoViewModel) let navigationViewController = UINavigationController(rootViewController: baseViewController) window?.rootViewController = navigationViewController diff --git a/ProjectManager/ProjectManager/Utility/Manager/CoreDataManager.swift b/ProjectManager/ProjectManager/Domain/CoreDataManager.swift similarity index 100% rename from ProjectManager/ProjectManager/Utility/Manager/CoreDataManager.swift rename to ProjectManager/ProjectManager/Domain/CoreDataManager.swift diff --git a/ProjectManager/ProjectManager/Entity/CoreData/ToDo+CoreDataClass.swift b/ProjectManager/ProjectManager/Domain/Entity/CoreData/ToDo+CoreDataClass.swift similarity index 100% rename from ProjectManager/ProjectManager/Entity/CoreData/ToDo+CoreDataClass.swift rename to ProjectManager/ProjectManager/Domain/Entity/CoreData/ToDo+CoreDataClass.swift diff --git a/ProjectManager/ProjectManager/Entity/CoreData/ToDo+CoreDataProperties.swift b/ProjectManager/ProjectManager/Domain/Entity/CoreData/ToDo+CoreDataProperties.swift similarity index 100% rename from ProjectManager/ProjectManager/Entity/CoreData/ToDo+CoreDataProperties.swift rename to ProjectManager/ProjectManager/Domain/Entity/CoreData/ToDo+CoreDataProperties.swift diff --git a/ProjectManager/ProjectManager/Domain/ToDoUseCase.swift b/ProjectManager/ProjectManager/Domain/ToDoUseCase.swift new file mode 100644 index 000000000..6dc5f86f0 --- /dev/null +++ b/ProjectManager/ProjectManager/Domain/ToDoUseCase.swift @@ -0,0 +1,51 @@ +// +// ToDoUseCase.swift +// ProjectManager +// +// Created by Max on 2023/10/06. +// + +import CoreData + +struct ToDoUseCase { + private let coreDataManager: CoreDataManager + + init(dataManager: CoreDataManager) { + coreDataManager = dataManager + } + + func fetchDataByStatus(for status: ToDoStatus) throws -> [ToDo] { + let predicated = NSPredicate(format: "status == %@", status.rawValue) + let filtered = try coreDataManager.fetchData(entityName:"ToDo", predicate: predicated, sort: "modifiedAt") + + guard let result = filtered as? [ToDo] else { + throw CoreDataError.unknown + } + return result + } + + func createData(values: [KeywordArgument]) throws { + var values = values + + if values.filter({ $0.key == "id" }).isEmpty { + values.append(KeywordArgument(key: "id", value: UUID())) + } + + if values.filter({ $0.key == "modifiedAt" }).isEmpty { + values.append(KeywordArgument(key: "modifiedAt", value: Date())) + } + + if values.filter({ $0.key == "status" }).isEmpty { + values.append(KeywordArgument(key: "status", value: ToDoStatus.toDo.rawValue)) + } + try coreDataManager.createData(type: ToDo.self, values: values) + } + + func updateData(_ entity: ToDo, values: [KeywordArgument]) throws { + try coreDataManager.updateData(entity: entity, values: values) + } + + func deleteData(_ entity: ToDo) throws { + try coreDataManager.deleteData(entity: entity) + } +} diff --git a/ProjectManager/ProjectManager/View/ListView/BaseView/ToDoListBaseViewController.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewController.swift similarity index 91% rename from ProjectManager/ProjectManager/View/ListView/BaseView/ToDoListBaseViewController.swift rename to ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewController.swift index b1d9fe5e4..3b83bd4fe 100644 --- a/ProjectManager/ProjectManager/View/ListView/BaseView/ToDoListBaseViewController.swift +++ b/ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewController.swift @@ -7,7 +7,7 @@ import UIKit final class ToDoListBaseViewController: UIViewController { - private let viewModel: ToDoListBaseViewModel + private let viewModel: ToDoBaseViewModelType private let stackView: UIStackView = { let stackView = UIStackView() @@ -26,7 +26,7 @@ final class ToDoListBaseViewController: UIViewController { return formatter }() - init(_ viewModel: ToDoListBaseViewModel) { + init(_ viewModel: ToDoBaseViewModelType) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } @@ -35,10 +35,6 @@ final class ToDoListBaseViewController: UIViewController { fatalError("init(coder:) has not been implemented") } - override func viewWillAppear(_ animated: Bool) { - viewModel.fetchAllData() - } - override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground @@ -50,7 +46,7 @@ final class ToDoListBaseViewController: UIViewController { private func addChildren() { ToDoStatus.allCases.forEach { status in - let childViewModel = viewModel.addChild(status) + let childViewModel = viewModel.inputs.addChild(status) let childViewController = ToDoListChildViewController(status, viewModel: childViewModel, dateFormatter: dateFormatter) @@ -91,14 +87,14 @@ final class ToDoListBaseViewController: UIViewController { KeywordArgument(key: "modifiedAt", value: Date()), KeywordArgument(key: "status", value: ToDoStatus.toDo.rawValue) ] - self?.viewModel.createData(values: testValue) + self?.viewModel.inputs.createData(values: testValue) #endif } navigationItem.rightBarButtonItem = UIBarButtonItem(primaryAction: addToDo) } private func setupBinding() { - viewModel.error.bind { [weak self] error in + viewModel.outputs.error.bind { [weak self] error in guard let self, let error else { return } let alertBuilder = AlertBuilder(prefferedStyle: .alert) diff --git a/ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewModel.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewModel.swift new file mode 100644 index 000000000..fab7d63f6 --- /dev/null +++ b/ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewModel.swift @@ -0,0 +1,73 @@ +// +// ToDoViewModel.swift +// ProjectManager +// +// Created by Max on 2023/09/24. +// + +import CoreData + +final class ToDoListBaseViewModel: ToDoBaseViewModelType, ToDoBaseViewModelOutputsType { + private let coreDataManager: CoreDataManager + private let useCase: ToDoUseCase + private var children: [ToDoStatus: ToDoListChildViewModel] = [:] + + var inputs: ToDoBaseViewModelInputsType { return self } + var outputs: ToDoBaseViewModelOutputsType { return self } + + var error: Observable = Observable(nil) + + init(dataManager: CoreDataManager, useCase: ToDoUseCase) { + coreDataManager = dataManager + self.useCase = useCase + } +} + +extension ToDoListBaseViewModel: ViewModelTypeWithError { + func handle(error: Error) { + if let coreDataError = error as? CoreDataError { + self.setError(coreDataError) + } else { + self.setError(CoreDataError.unknown) + } + } + + func setError(_ error: CoreDataError) { + self.error = Observable(error) + } +} + +extension ToDoListBaseViewModel: ToDoListBaseViewModelDelegate { + func createData(values: [KeywordArgument]) { + do { + try useCase.createData(values: values) + try updateChild(.toDo, action: Output(type: .create)) + } catch(let error) { + handle(error: error) + } + } + + func updateChild(_ status: ToDoStatus, action: Output) throws { + children[status]?.entityList = try useCase.fetchDataByStatus(for: status) + children[status]?.action.value = action + } +} + +extension ToDoListBaseViewModel: ToDoBaseViewModelInputsType { + func addChild(_ status: ToDoStatus) -> ToDoListChildViewModel { + let child = ToDoListChildViewModel(status: status, useCase: useCase) + children[status] = child + child.delegate = self +#if DEBUG + child.addTestData() + do { + try updateChild(status, action: Output(type: .create)) + } catch(let error) { + handle(error: error) + } +#endif + return child + } +} + + diff --git a/ProjectManager/ProjectManager/View/ListView/Cell/ToDoListHeaderView.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/Cell/ToDoListHeaderView.swift similarity index 100% rename from ProjectManager/ProjectManager/View/ListView/Cell/ToDoListHeaderView.swift rename to ProjectManager/ProjectManager/Presentation/View/ListView/Cell/ToDoListHeaderView.swift diff --git a/ProjectManager/ProjectManager/View/ListView/Cell/ToDoListTableViewCell.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/Cell/ToDoListTableViewCell.swift similarity index 100% rename from ProjectManager/ProjectManager/View/ListView/Cell/ToDoListTableViewCell.swift rename to ProjectManager/ProjectManager/Presentation/View/ListView/Cell/ToDoListTableViewCell.swift diff --git a/ProjectManager/ProjectManager/View/ListView/ChildView/ToDoListChildViewController.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewController.swift similarity index 84% rename from ProjectManager/ProjectManager/View/ListView/ChildView/ToDoListChildViewController.swift rename to ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewController.swift index 6460d8d8d..d47ee18ad 100644 --- a/ProjectManager/ProjectManager/View/ListView/ChildView/ToDoListChildViewController.swift +++ b/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewController.swift @@ -10,7 +10,7 @@ import UIKit final class ToDoListChildViewController: UIViewController { private let status: ToDoStatus private let headerView: ToDoListHeaderView - private let viewModel: ToDoListChildViewModel + private let viewModel: ToDoChildViewModelType let today = Date().timeIntervalSinceReferenceDate private let dateFormatter: DateFormatter @@ -23,7 +23,7 @@ final class ToDoListChildViewController: UIViewController { return tableView }() - init(_ status: ToDoStatus, viewModel: ToDoListChildViewModel, dateFormatter: DateFormatter) { + init(_ status: ToDoStatus, viewModel: ToDoChildViewModelType, dateFormatter: DateFormatter) { self.status = status self.headerView = ToDoListHeaderView(status) self.dateFormatter = dateFormatter @@ -35,6 +35,11 @@ final class ToDoListChildViewController: UIViewController { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + viewModel.inputs.viewWillAppear() + } override func viewDidLoad() { super.viewDidLoad() @@ -65,7 +70,7 @@ final class ToDoListChildViewController: UIViewController { extension ToDoListChildViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - viewModel.entityList.count + viewModel.outputs.entityList.count } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { @@ -77,7 +82,7 @@ extension ToDoListChildViewController: UITableViewDelegate, UITableViewDataSourc for: indexPath) as? ToDoListTableViewCell else { return UITableViewCell() } - let toDoEntity = viewModel.entityList[indexPath.row] + let toDoEntity = viewModel.outputs.entityList[indexPath.row] let isDone = toDoEntity.status == ToDoStatus.done.rawValue let isPast = floor(today/86400) > floor(toDoEntity.dueDate.timeIntervalSinceReferenceDate/86400) && !isDone let date = dateFormatter.string(from: toDoEntity.dueDate) @@ -91,8 +96,8 @@ extension ToDoListChildViewController: UITableViewDelegate, UITableViewDataSourc func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let delete = UIContextualAction(style: .normal, title: "") { (_, _, success: @escaping (Bool) -> Void) in - let selectedEntity = self.viewModel.entityList[indexPath.row] - self.viewModel.deleteData(selectedEntity) + let selectedEntity = self.viewModel.outputs.entityList[indexPath.row] + self.viewModel.inputs.swipeToDelete(selectedEntity) } delete.backgroundColor = .systemRed @@ -104,15 +109,15 @@ extension ToDoListChildViewController: UITableViewDelegate, UITableViewDataSourc extension ToDoListChildViewController { private func setupBinding() { - viewModel.action.bind { [weak self] action in + viewModel.outputs.action.bind { [weak self] action in guard let self, let action else { return } switch action.type { case .create: - let index = self.viewModel.entityList.count - 1 + let index = self.viewModel.outputs.entityList.count - 1 self.tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .fade) - case .update: + case .read, .update: self.tableView.reloadData() case .delete: guard let indexInformation = action.extraInformation.filter({ $0.key == "index" }).first, @@ -120,10 +125,10 @@ extension ToDoListChildViewController { self.tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .fade) } - self.headerView.setupTotalCount(viewModel.entityList.count) + self.headerView.setupTotalCount(viewModel.outputs.entityList.count) } - viewModel.error.bind { [weak self] error in + viewModel.outputs.error.bind { [weak self] error in guard let self, let error else { return } let alertBuilder = AlertBuilder(prefferedStyle: .alert) diff --git a/ProjectManager/ProjectManager/View/ListView/ChildView/ToDoListChildViewModel.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewModel.swift similarity index 58% rename from ProjectManager/ProjectManager/View/ListView/ChildView/ToDoListChildViewModel.swift rename to ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewModel.swift index 34cd2fb0d..684f6c71a 100644 --- a/ProjectManager/ProjectManager/View/ListView/ChildView/ToDoListChildViewModel.swift +++ b/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewModel.swift @@ -7,16 +7,22 @@ import Foundation -final class ToDoListChildViewModel: ViewModelType { +final class ToDoListChildViewModel: ToDoChildViewModelType, ViewModelTypeWithError, ToDoChildViewModelOutputsType { + private let useCase: ToDoUseCase + + var inputs: ToDoChildViewModelInputsType { return self } + var outputs: ToDoChildViewModelOutputsType { return self } + private let status: ToDoStatus var entityList: [ToDo] = [] - var action: Observable = Observable(nil) + var action: Observable = Observable(nil) weak var delegate: ToDoListBaseViewModelDelegate? var error: Observable = Observable(nil) - init(status: ToDoStatus) { + init(status: ToDoStatus, useCase: ToDoUseCase) { self.status = status + self.useCase = useCase } func handle(error: Error) { @@ -33,18 +39,38 @@ final class ToDoListChildViewModel: ViewModelType { } extension ToDoListChildViewModel { - func updateData(_ entity: ToDo, values: [KeywordArgument]) { - delegate?.updateData(entity, values: values, from: status) - } - - func deleteData(_ entity: ToDo) { + func changeStatus(_ entity: ToDo, to newStatus: ToDoStatus) { guard let index = entityList.firstIndex(of: entity) else { return } - delegate?.deleteData(entity, at: index, from: status) + do { + try useCase.updateData(entity, values: [KeywordArgument(key: "status", value: newStatus.rawValue)]) + + action.value = Output(type: .delete, extraInformation: [KeywordArgument(key: "index", value: index)]) + try delegate?.updateChild(newStatus, action: Output(type: .create)) + } catch(let error) { + handle(error: error) + } + } +} + +extension ToDoListChildViewModel: ToDoChildViewModelInputsType { + func viewWillAppear() { + do { + entityList = try useCase.fetchDataByStatus(for: status) + action.value = Output(type: .read) + } catch(let error) { + handle(error: error) + } } - func changeStatus(_ entity: ToDo, to newStatus: ToDoStatus) { + func swipeToDelete(_ entity: ToDo) { guard let index = entityList.firstIndex(of: entity) else { return } - delegate?.changeStatus(entity, at: index, from: status, to: newStatus) + do { + try useCase.deleteData(entity) + entityList = try useCase.fetchDataByStatus(for: status) + action.value = Output(type: .delete, extraInformation: [KeywordArgument(key: "index", value: index)]) + } catch(let error) { + handle(error: error) + } } } diff --git a/ProjectManager/ProjectManager/View/ListView/ToDoListViewController.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/ToDoListViewController.swift similarity index 100% rename from ProjectManager/ProjectManager/View/ListView/ToDoListViewController.swift rename to ProjectManager/ProjectManager/Presentation/View/ListView/ToDoListViewController.swift diff --git a/ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ToDoListBaseViewModelDelegate.swift b/ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ToDoListBaseViewModelDelegate.swift new file mode 100644 index 000000000..9510df4ab --- /dev/null +++ b/ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ToDoListBaseViewModelDelegate.swift @@ -0,0 +1,13 @@ +// +// ToDoListViewModelDelegate.swift +// ProjectManager +// +// Created by Max on 2023/10/03. +// + +protocol ToDoListBaseViewModelDelegate: AnyObject { + func updateChild(_ status: ToDoStatus, action: Output) throws +#if DEBUG + func createData(values: [KeywordArgument]) +#endif +} diff --git a/ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ViewModelType.swift b/ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ViewModelType.swift new file mode 100644 index 000000000..b231f76c5 --- /dev/null +++ b/ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ViewModelType.swift @@ -0,0 +1,47 @@ +// +// ViewModel.swift +// ProjectManager +// +// Created by Max on 2023/09/24. +// + +protocol ViewModelTypeWithError { + associatedtype ViewModelError + + var error: Observable { get set } + + func handle(error: Error) + func setError(_ error: ViewModelError) +} + +protocol ToDoBaseViewModelType { + var inputs: ToDoBaseViewModelInputsType { get } + var outputs: ToDoBaseViewModelOutputsType { get } +} + +protocol ToDoBaseViewModelInputsType { + func addChild(_ status: ToDoStatus) -> ToDoListChildViewModel + func createData(values: [KeywordArgument]) +} + +protocol ToDoBaseViewModelOutputsType { + var error: Observable { get set } +} + +protocol ToDoChildViewModelType { + var inputs: ToDoChildViewModelInputsType { get } + var outputs: ToDoChildViewModelOutputsType { get } +} + +protocol ToDoChildViewModelInputsType { + func viewWillAppear() + func swipeToDelete(_ entity: ToDo) +} + +protocol ToDoChildViewModelOutputsType { + var action: Observable { get } + var entityList: [ToDo] { get } + var error: Observable { get set } +} + + diff --git a/ProjectManager/ProjectManager/Utility/Builder/AlertBuilder.swift b/ProjectManager/ProjectManager/Utility/AlertBuilder.swift similarity index 100% rename from ProjectManager/ProjectManager/Utility/Builder/AlertBuilder.swift rename to ProjectManager/ProjectManager/Utility/AlertBuilder.swift diff --git a/ProjectManager/ProjectManager/Utility/Error/CoreDataError.swift b/ProjectManager/ProjectManager/Utility/CoreDataError.swift similarity index 100% rename from ProjectManager/ProjectManager/Utility/Error/CoreDataError.swift rename to ProjectManager/ProjectManager/Utility/CoreDataError.swift diff --git a/ProjectManager/ProjectManager/Utility/Action.swift b/ProjectManager/ProjectManager/Utility/Output.swift similarity index 92% rename from ProjectManager/ProjectManager/Utility/Action.swift rename to ProjectManager/ProjectManager/Utility/Output.swift index fe90824d3..089a46fcd 100644 --- a/ProjectManager/ProjectManager/Utility/Action.swift +++ b/ProjectManager/ProjectManager/Utility/Output.swift @@ -5,7 +5,7 @@ // Created by Max on 2023/10/03. // -struct Action { +struct Output { let type: ActionType let extraInformation: [KeywordArgument] @@ -16,6 +16,7 @@ struct Action { enum ActionType { case create + case read case update case delete } diff --git a/ProjectManager/ProjectManager/Utility/Protocol/ToDoListBaseViewModelDelegate.swift b/ProjectManager/ProjectManager/Utility/Protocol/ToDoListBaseViewModelDelegate.swift deleted file mode 100644 index 165355e9e..000000000 --- a/ProjectManager/ProjectManager/Utility/Protocol/ToDoListBaseViewModelDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// ToDoListViewModelDelegate.swift -// ProjectManager -// -// Created by Max on 2023/10/03. -// - -protocol ToDoListBaseViewModelDelegate: AnyObject { - func createData(values: [KeywordArgument]) - func updateData(_ entity: ToDo, values: [KeywordArgument], from childKey: ToDoStatus) - func deleteData(_ entity: ToDo, at index: Int, from childKey: ToDoStatus) - func changeStatus(_ entity: ToDo, at index: Int, from oldStatus: ToDoStatus, to newStatus: ToDoStatus) -} diff --git a/ProjectManager/ProjectManager/Utility/Protocol/ViewModelType.swift b/ProjectManager/ProjectManager/Utility/Protocol/ViewModelType.swift deleted file mode 100644 index c10b774df..000000000 --- a/ProjectManager/ProjectManager/Utility/Protocol/ViewModelType.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ViewModel.swift -// ProjectManager -// -// Created by Max on 2023/09/24. -// - -import Foundation - -protocol ViewModelType { - associatedtype ViewModelError - - var error: Observable { get set } - - func handle(error: Error) - func setError(_ error: ViewModelError) -} diff --git a/ProjectManager/ProjectManager/View/ListView/BaseView/ToDoListBaseViewModel.swift b/ProjectManager/ProjectManager/View/ListView/BaseView/ToDoListBaseViewModel.swift deleted file mode 100644 index 477c61505..000000000 --- a/ProjectManager/ProjectManager/View/ListView/BaseView/ToDoListBaseViewModel.swift +++ /dev/null @@ -1,121 +0,0 @@ -// -// ToDoViewModel.swift -// ProjectManager -// -// Created by Max on 2023/09/24. -// - -import CoreData - -final class ToDoListBaseViewModel: ViewModelType { - private let coreDataManager: CoreDataManager - private var children: [ToDoStatus: ToDoListChildViewModel] = [:] - - var error: Observable = Observable(nil) - - init(dataManager: CoreDataManager) { - coreDataManager = dataManager - } - - func handle(error: Error) { - if let coreDataError = error as? CoreDataError { - self.setError(coreDataError) - } else { - self.setError(CoreDataError.unknown) - } - } - - func setError(_ error: CoreDataError) { - self.error = Observable(error) - } -} - -extension ToDoListBaseViewModel: ToDoListBaseViewModelDelegate { - func fetchAllData() { - ToDoStatus.allCases.forEach { fetchDataByStatus(for: $0) } - } - - func fetchDataByStatus(for status: ToDoStatus) { - do { - let predicated = NSPredicate(format: "status == %@", status.rawValue) - let filtered = try coreDataManager.fetchData(entityName:"ToDo", predicate: predicated, sort: "modifiedAt") - - guard let result = filtered as? [ToDo] else { return } - - children[status]?.entityList = result - - } catch(let error) { - handle(error: error) - } - } - - func createData(values: [KeywordArgument]) { - var values = values - - if values.filter({ $0.key == "id" }).isEmpty { - values.append(KeywordArgument(key: "id", value: UUID())) - } - - if values.filter({ $0.key == "modifiedAt" }).isEmpty { - values.append(KeywordArgument(key: "modifiedAt", value: Date())) - } - - if values.filter({ $0.key == "status" }).isEmpty { - values.append(KeywordArgument(key: "status", value: ToDoStatus.toDo.rawValue)) - } - - do { - try coreDataManager.createData(type: ToDo.self, values: values) - updateChild(.toDo, action: Action(type: .create)) - } catch(let error) { - handle(error: error) - } - } - - func updateData(_ entity: ToDo, values: [KeywordArgument], from childKey: ToDoStatus) { - do { - try coreDataManager.updateData(entity: entity, values: values) - updateChild(childKey, action: Action(type: .update)) - } catch(let error) { - handle(error: error) - } - } - - func deleteData(_ entity: ToDo, at index: Int, from childKey: ToDoStatus) { - do { - try coreDataManager.deleteData(entity: entity) - updateChild(childKey, action: Action(type: .delete, extraInformation: [KeywordArgument(key: "index", value: index)])) - } catch(let error) { - handle(error: error) - } - } - - func changeStatus(_ entity: ToDo, at index: Int, from oldStatus: ToDoStatus, to newStatus: ToDoStatus) { - do { - try coreDataManager.updateData(entity: entity, values: [KeywordArgument(key: "status", value: newStatus.rawValue)]) - updateChild(oldStatus, action: Action(type: .delete, extraInformation: [KeywordArgument(key: "index", value: index)])) - updateChild(newStatus, action: Action(type: .create)) - } catch(let error) { - handle(error: error) - } - } -} - -extension ToDoListBaseViewModel { - func addChild(_ status: ToDoStatus) -> ToDoListChildViewModel { - let child = ToDoListChildViewModel(status: status) - children[status] = child - child.delegate = self -#if DEBUG - child.addTestData() - updateChild(status, action: Action(type: .create)) -#endif - return child - } - - func updateChild(_ status: ToDoStatus, action: Action) { - fetchDataByStatus(for: status) - children[status]?.action.value = action - } -} - From 3e68a43ca48a96caa8ef5fc7414bff05dc999ea9 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Fri, 6 Oct 2023 03:30:21 +0900 Subject: [PATCH 19/20] =?UTF-8?q?refactor:=20UseCase=20=EB=B6=84=EB=A6=AC,?= =?UTF-8?q?=20PopOverView=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectManager.xcodeproj/project.pbxproj | 28 ++++++-- .../ProjectManager/App/SceneDelegate.swift | 3 +- .../ProjectManager/Domain/ToDoUseCase.swift | 4 ++ .../BaseView/ToDoListBaseViewModel.swift | 38 +++++------ .../ListView/Cell/ToDoListTableViewCell.swift | 3 + .../ToDoListChildViewController.swift | 26 +++++++- .../ChildView/ToDoListChildViewModel.swift | 38 ++++++----- .../ChangeStatusViewController.swift | 65 +++++++++++++++++++ .../PopOverView/ChangeStatusViewModel.swift | 37 +++++++++++ .../ListView/PopOverView/PopOverButton.swift | 29 +++++++++ ...Delegate.swift => ViewModelDelegate.swift} | 4 ++ .../ViewModelProtocol/ViewModelType.swift | 11 ++++ 12 files changed, 241 insertions(+), 45 deletions(-) create mode 100644 ProjectManager/ProjectManager/Presentation/View/ListView/PopOverView/ChangeStatusViewController.swift create mode 100644 ProjectManager/ProjectManager/Presentation/View/ListView/PopOverView/ChangeStatusViewModel.swift create mode 100644 ProjectManager/ProjectManager/Presentation/View/ListView/PopOverView/PopOverButton.swift rename ProjectManager/ProjectManager/Presentation/ViewModelProtocol/{ToDoListBaseViewModelDelegate.swift => ViewModelDelegate.swift} (70%) diff --git a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj index b2a4e15b5..4dcfbd599 100644 --- a/ProjectManager/ProjectManager.xcodeproj/project.pbxproj +++ b/ProjectManager/ProjectManager.xcodeproj/project.pbxproj @@ -27,9 +27,12 @@ BA64637D2AC26F490080E80D /* ToDoListChildViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64637C2AC26F490080E80D /* ToDoListChildViewController.swift */; }; BA64637F2AC283F70080E80D /* ToDoListChildViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA64637E2AC283F70080E80D /* ToDoListChildViewModel.swift */; }; BA6463812AC289680080E80D /* KeywordArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6463802AC289680080E80D /* KeywordArgument.swift */; }; - BAD471D62ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD471D52ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift */; }; + BAD471D62ACBA2CB0021323A /* ViewModelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD471D52ACBA2CB0021323A /* ViewModelDelegate.swift */; }; BAD471D82ACBA4820021323A /* Output.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD471D72ACBA4820021323A /* Output.swift */; }; BAD471DF2ACF19EA0021323A /* ToDoUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD471DE2ACF19EA0021323A /* ToDoUseCase.swift */; }; + BAD471E62ACF2AC90021323A /* ChangeStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD471E52ACF2AC90021323A /* ChangeStatusViewModel.swift */; }; + BAD471EB2ACF2AF30021323A /* ChangeStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD471EA2ACF2AF30021323A /* ChangeStatusViewController.swift */; }; + BAD471ED2ACF2B080021323A /* PopOverButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD471EC2ACF2B080021323A /* PopOverButton.swift */; }; C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0525F51E1D0094C4CF /* AppDelegate.swift */; }; C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0725F51E1D0094C4CF /* SceneDelegate.swift */; }; C7431F0A25F51E1D0094C4CF /* ToDoListBaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7431F0925F51E1D0094C4CF /* ToDoListBaseViewController.swift */; }; @@ -53,9 +56,12 @@ BA64637C2AC26F490080E80D /* ToDoListChildViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListChildViewController.swift; sourceTree = ""; }; BA64637E2AC283F70080E80D /* ToDoListChildViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListChildViewModel.swift; sourceTree = ""; }; BA6463802AC289680080E80D /* KeywordArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordArgument.swift; sourceTree = ""; }; - BAD471D52ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoListBaseViewModelDelegate.swift; sourceTree = ""; }; + BAD471D52ACBA2CB0021323A /* ViewModelDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelDelegate.swift; sourceTree = ""; }; BAD471D72ACBA4820021323A /* Output.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Output.swift; sourceTree = ""; }; BAD471DE2ACF19EA0021323A /* ToDoUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToDoUseCase.swift; sourceTree = ""; }; + BAD471E52ACF2AC90021323A /* ChangeStatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeStatusViewModel.swift; sourceTree = ""; }; + BAD471EA2ACF2AF30021323A /* ChangeStatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeStatusViewController.swift; sourceTree = ""; }; + BAD471EC2ACF2B080021323A /* PopOverButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopOverButton.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 = ""; }; @@ -104,6 +110,7 @@ BA64637B2AC0613D0080E80D /* Cell */, BA6463822AC29D180080E80D /* BaseView */, BA6463832AC29D1D0080E80D /* ChildView */, + BAD471E42ACF2A9E0021323A /* PopOverView */, ); path = ListView; sourceTree = ""; @@ -156,7 +163,7 @@ isa = PBXGroup; children = ( BA64636C2AC045700080E80D /* ViewModelType.swift */, - BAD471D52ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift */, + BAD471D52ACBA2CB0021323A /* ViewModelDelegate.swift */, ); path = ViewModelProtocol; sourceTree = ""; @@ -202,6 +209,16 @@ path = Utility; sourceTree = ""; }; + BAD471E42ACF2A9E0021323A /* PopOverView */ = { + isa = PBXGroup; + children = ( + BAD471E52ACF2AC90021323A /* ChangeStatusViewModel.swift */, + BAD471EA2ACF2AF30021323A /* ChangeStatusViewController.swift */, + BAD471EC2ACF2B080021323A /* PopOverButton.swift */, + ); + path = PopOverView; + sourceTree = ""; + }; C7431EF925F51E1D0094C4CF = { isa = PBXGroup; children = ( @@ -317,19 +334,22 @@ BA64637A2AC050AD0080E80D /* ToDoListTableViewCell.swift in Sources */, C7431F0625F51E1D0094C4CF /* AppDelegate.swift in Sources */, C7431F0825F51E1D0094C4CF /* SceneDelegate.swift in Sources */, + BAD471E62ACF2AC90021323A /* ChangeStatusViewModel.swift in Sources */, BA6463752AC04F920080E80D /* ToDoListHeaderView.swift in Sources */, BA64636D2AC045700080E80D /* ViewModelType.swift in Sources */, BA64636F2AC04A700080E80D /* ToDoListBaseViewModel.swift in Sources */, BA6463662AC0440E0080E80D /* CoreDataError.swift in Sources */, BA6463602AC043680080E80D /* ToDo+CoreDataProperties.swift in Sources */, - BAD471D62ACBA2CB0021323A /* ToDoListBaseViewModelDelegate.swift in Sources */, + BAD471D62ACBA2CB0021323A /* ViewModelDelegate.swift in Sources */, BA6463682AC044D70080E80D /* ToDoStatus.swift in Sources */, BAD471D82ACBA4820021323A /* Output.swift in Sources */, BA6463782AC04FE40080E80D /* AlertBuilder.swift in Sources */, BAD471DF2ACF19EA0021323A /* ToDoUseCase.swift in Sources */, BA64636A2AC045380080E80D /* Observable.swift in Sources */, BA64635F2AC043680080E80D /* ToDo+CoreDataClass.swift in Sources */, + BAD471EB2ACF2AF30021323A /* ChangeStatusViewController.swift in Sources */, BA6463642AC043F50080E80D /* CoreDataManager.swift in Sources */, + BAD471ED2ACF2B080021323A /* PopOverButton.swift in Sources */, BA64637D2AC26F490080E80D /* ToDoListChildViewController.swift in Sources */, BA6463582AC042A90080E80D /* ToDo.xcdatamodeld in Sources */, ); diff --git a/ProjectManager/ProjectManager/App/SceneDelegate.swift b/ProjectManager/ProjectManager/App/SceneDelegate.swift index c586ffa57..77497d2b7 100644 --- a/ProjectManager/ProjectManager/App/SceneDelegate.swift +++ b/ProjectManager/ProjectManager/App/SceneDelegate.swift @@ -7,7 +7,6 @@ import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? @@ -17,7 +16,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window = UIWindow(windowScene: windowScene) let coreDataManager = CoreDataManager() let useCase = ToDoUseCase(dataManager: coreDataManager) - let toDoViewModel = ToDoListBaseViewModel(dataManager: coreDataManager, useCase: useCase) + let toDoViewModel = ToDoListBaseViewModel(useCase: useCase) let baseViewController = ToDoListBaseViewController(toDoViewModel) let navigationViewController = UINavigationController(rootViewController: baseViewController) window?.rootViewController = navigationViewController diff --git a/ProjectManager/ProjectManager/Domain/ToDoUseCase.swift b/ProjectManager/ProjectManager/Domain/ToDoUseCase.swift index 6dc5f86f0..762427b10 100644 --- a/ProjectManager/ProjectManager/Domain/ToDoUseCase.swift +++ b/ProjectManager/ProjectManager/Domain/ToDoUseCase.swift @@ -42,6 +42,10 @@ struct ToDoUseCase { } func updateData(_ entity: ToDo, values: [KeywordArgument]) throws { + var values = values + if values.filter({ $0.key == "modifiedAt" }).isEmpty { + values.append(KeywordArgument(key: "modifiedAt", value: Date())) + } try coreDataManager.updateData(entity: entity, values: values) } diff --git a/ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewModel.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewModel.swift index fab7d63f6..562f26f58 100644 --- a/ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewModel.swift +++ b/ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewModel.swift @@ -8,7 +8,6 @@ import CoreData final class ToDoListBaseViewModel: ToDoBaseViewModelType, ToDoBaseViewModelOutputsType { - private let coreDataManager: CoreDataManager private let useCase: ToDoUseCase private var children: [ToDoStatus: ToDoListChildViewModel] = [:] @@ -17,8 +16,7 @@ final class ToDoListBaseViewModel: ToDoBaseViewModelType, ToDoBaseViewModelOutpu var error: Observable = Observable(nil) - init(dataManager: CoreDataManager, useCase: ToDoUseCase) { - coreDataManager = dataManager + init(useCase: ToDoUseCase) { self.useCase = useCase } } @@ -36,28 +34,12 @@ extension ToDoListBaseViewModel: ViewModelTypeWithError { self.error = Observable(error) } } - -extension ToDoListBaseViewModel: ToDoListBaseViewModelDelegate { - func createData(values: [KeywordArgument]) { - do { - try useCase.createData(values: values) - try updateChild(.toDo, action: Output(type: .create)) - } catch(let error) { - handle(error: error) - } - } - - func updateChild(_ status: ToDoStatus, action: Output) throws { - children[status]?.entityList = try useCase.fetchDataByStatus(for: status) - children[status]?.action.value = action - } -} extension ToDoListBaseViewModel: ToDoBaseViewModelInputsType { func addChild(_ status: ToDoStatus) -> ToDoListChildViewModel { let child = ToDoListChildViewModel(status: status, useCase: useCase) - children[status] = child child.delegate = self + children[status] = child #if DEBUG child.addTestData() do { @@ -70,4 +52,20 @@ extension ToDoListBaseViewModel: ToDoBaseViewModelInputsType { } } +extension ToDoListBaseViewModel { + func createData(values: [KeywordArgument]) { + do { + try useCase.createData(values: values) + try updateChild(.toDo, action: Output(type: .create)) + } catch(let error) { + handle(error: error) + } + } +} +extension ToDoListBaseViewModel: ToDoListBaseViewModelDelegate { + func updateChild(_ status: ToDoStatus, action: Output) throws { + children[status]?.entityList = try useCase.fetchDataByStatus(for: status) + children[status]?.action.value = action + } +} diff --git a/ProjectManager/ProjectManager/Presentation/View/ListView/Cell/ToDoListTableViewCell.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/Cell/ToDoListTableViewCell.swift index 521fdd9dd..fed3ac9e5 100644 --- a/ProjectManager/ProjectManager/Presentation/View/ListView/Cell/ToDoListTableViewCell.swift +++ b/ProjectManager/ProjectManager/Presentation/View/ListView/Cell/ToDoListTableViewCell.swift @@ -62,6 +62,9 @@ final class ToDoListTableViewCell: UITableViewCell { titleLabel.textColor = .black bodyLabel.textColor = .black dateLabel.textColor = .black + titleLabel.text = "" + bodyLabel.text = "" + dateLabel.text = "" } } diff --git a/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewController.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewController.swift index d47ee18ad..bceb45766 100644 --- a/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewController.swift +++ b/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewController.swift @@ -53,6 +53,8 @@ final class ToDoListChildViewController: UIViewController { view.backgroundColor = .systemBackground view.addSubview(tableView) + tableView.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(longPressEvent))) + NSLayoutConstraint.activate([ tableView.widthAnchor.constraint(equalTo: safeArea.widthAnchor), tableView.heightAnchor.constraint(equalTo: safeArea.heightAnchor), @@ -139,5 +141,27 @@ extension ToDoListChildViewController { present(alertController, animated: true) } } - } + +extension ToDoListChildViewController { + @objc private func longPressEvent(sender: UILongPressGestureRecognizer) { + let point = sender.location(in: tableView) + + guard let indexPath = self.tableView.indexPathForRow(at: point), + let viewModelDelegate = viewModel as? ToDoListChildViewModelDelegate else { return } + let entity = viewModel.outputs.entityList[indexPath.row] + let changeStatusViewModel = ChangeStatusViewModel() + changeStatusViewModel.delegate = viewModelDelegate + let changeStatusViewController = ChangeStatusViewController(entity, status: status, viewModel: changeStatusViewModel) + changeStatusViewController.modalPresentationStyle = .popover + changeStatusViewController.preferredContentSize = CGSize(width: 300, height: 150) + + let popOverController = changeStatusViewController.popoverPresentationController + popOverController?.sourceView = tableView + popOverController?.sourceRect = CGRect(x: point.x, y: point.y, width: 0, height: 0) + popOverController?.permittedArrowDirections = .up + present(changeStatusViewController, animated: true) + + } +} + diff --git a/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewModel.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewModel.swift index 684f6c71a..ec2bce0c7 100644 --- a/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewModel.swift +++ b/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewModel.swift @@ -7,8 +7,9 @@ import Foundation -final class ToDoListChildViewModel: ToDoChildViewModelType, ViewModelTypeWithError, ToDoChildViewModelOutputsType { - private let useCase: ToDoUseCase +final class ToDoListChildViewModel: ToDoChildViewModelType, ToDoChildViewModelOutputsType { + var useCase: ToDoUseCase + weak var delegate: ToDoListBaseViewModelDelegate? var inputs: ToDoChildViewModelInputsType { return self } var outputs: ToDoChildViewModelOutputsType { return self } @@ -16,7 +17,6 @@ final class ToDoListChildViewModel: ToDoChildViewModelType, ViewModelTypeWithErr private let status: ToDoStatus var entityList: [ToDo] = [] var action: Observable = Observable(nil) - weak var delegate: ToDoListBaseViewModelDelegate? var error: Observable = Observable(nil) @@ -24,7 +24,9 @@ final class ToDoListChildViewModel: ToDoChildViewModelType, ViewModelTypeWithErr self.status = status self.useCase = useCase } - +} + +extension ToDoListChildViewModel: ViewModelTypeWithError { func handle(error: Error) { if let coreDataError = error as? CoreDataError { self.setError(coreDataError) @@ -38,20 +40,6 @@ final class ToDoListChildViewModel: ToDoChildViewModelType, ViewModelTypeWithErr } } -extension ToDoListChildViewModel { - func changeStatus(_ entity: ToDo, to newStatus: ToDoStatus) { - guard let index = entityList.firstIndex(of: entity) else { return } - do { - try useCase.updateData(entity, values: [KeywordArgument(key: "status", value: newStatus.rawValue)]) - - action.value = Output(type: .delete, extraInformation: [KeywordArgument(key: "index", value: index)]) - try delegate?.updateChild(newStatus, action: Output(type: .create)) - } catch(let error) { - handle(error: error) - } - } -} - extension ToDoListChildViewModel: ToDoChildViewModelInputsType { func viewWillAppear() { do { @@ -74,6 +62,20 @@ extension ToDoListChildViewModel: ToDoChildViewModelInputsType { } } +extension ToDoListChildViewModel: ToDoListChildViewModelDelegate { + func changeStatus(_ entity: ToDo, to newStatus: ToDoStatus) { + guard let index = entityList.firstIndex(of: entity) else { return } + do { + try useCase.updateData(entity, values: [KeywordArgument(key: "status", value: newStatus.rawValue)]) + entityList = try useCase.fetchDataByStatus(for: status) + action.value = Output(type: .delete, extraInformation: [KeywordArgument(key: "index", value: index)]) + try delegate?.updateChild(newStatus, action: Output(type: .update)) + } catch(let error) { + handle(error: error) + } + } +} + #if DEBUG extension ToDoListChildViewModel { func addTestData() { diff --git a/ProjectManager/ProjectManager/Presentation/View/ListView/PopOverView/ChangeStatusViewController.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/PopOverView/ChangeStatusViewController.swift new file mode 100644 index 000000000..6d54dfa32 --- /dev/null +++ b/ProjectManager/ProjectManager/Presentation/View/ListView/PopOverView/ChangeStatusViewController.swift @@ -0,0 +1,65 @@ +// +// PopOverViewController.swift +// ProjectManager +// +// Created by Min Hyun on 2023/10/06. +// + +import UIKit + +class ChangeStatusViewController: UIViewController { + let entity: ToDo + let currentStatus: ToDoStatus + let viewModel: ToDoChangeStatusViewModelType + + let stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.distribution = .fillEqually + stackView.spacing = 10 + return stackView + }() + + init(_ entity: ToDo, status: ToDoStatus, viewModel: ToDoChangeStatusViewModelType) { + self.entity = entity + self.viewModel = viewModel + currentStatus = status + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + makeButtons() + } + + func setupUI() { + let safeArea = view.safeAreaLayoutGuide + view.addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.widthAnchor.constraint(equalTo: safeArea.widthAnchor, constant: -30), + stackView.heightAnchor.constraint(equalTo: safeArea.heightAnchor, constant: -30), + stackView.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: safeArea.centerYAnchor), + ]) + } + + func makeButtons() { + ToDoStatus.allCases.filter({ $0 != currentStatus }).forEach { status in + let button = ChangeStatusButton(status: status) + button.addTarget(self, action: #selector(moveStatus(_:)), for: .touchUpInside) + stackView.addArrangedSubview(button) + } + } + + @objc func moveStatus(_ sender: ChangeStatusButton) throws { + viewModel.inputs.touchUpButton(entity, status: sender.status) + self.dismiss(animated: true) + } +} diff --git a/ProjectManager/ProjectManager/Presentation/View/ListView/PopOverView/ChangeStatusViewModel.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/PopOverView/ChangeStatusViewModel.swift new file mode 100644 index 000000000..a1675559e --- /dev/null +++ b/ProjectManager/ProjectManager/Presentation/View/ListView/PopOverView/ChangeStatusViewModel.swift @@ -0,0 +1,37 @@ +// +// ChangeStatusViewModel.swift +// ProjectManager +// +// Created by Max on 2023/10/06. +// + +final class ChangeStatusViewModel: ToDoChangeStatusViewModelType, ToDoChangeStatusViewModelOutputsType { + weak var delegate: ToDoListChildViewModelDelegate? + + var errorMessage: Observable = Observable(nil) + var error: Observable = Observable(nil) + + var inputs: ToDoChangeStatusViewModelInputsType { return self } + var outputs: ToDoChangeStatusViewModelOutputsType { return self } +} + +extension ChangeStatusViewModel: ViewModelTypeWithError { + func handle(error: Error) { + if let coreDataError = error as? CoreDataError { + self.setError(coreDataError) + } else { + self.setError(CoreDataError.unknown) + } + } + + func setError(_ error: CoreDataError) { + self.errorMessage = Observable(error.alertMessage) + self.error = Observable(error) + } +} + +extension ChangeStatusViewModel: ToDoChangeStatusViewModelInputsType { + func touchUpButton(_ entity: ToDo, status: ToDoStatus) { + delegate?.changeStatus(entity, to: status) + } +} diff --git a/ProjectManager/ProjectManager/Presentation/View/ListView/PopOverView/PopOverButton.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/PopOverView/PopOverButton.swift new file mode 100644 index 000000000..3bac438c3 --- /dev/null +++ b/ProjectManager/ProjectManager/Presentation/View/ListView/PopOverView/PopOverButton.swift @@ -0,0 +1,29 @@ +// +// PopOverButton.swift +// ProjectManager +// +// Created by Max on 2023/10/06. +// + +import UIKit + +final class ChangeStatusButton: UIButton { + let status: ToDoStatus + + init(status: ToDoStatus) { + self.status = status + super.init(frame: .init()) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupUI() { + self.setTitle("Move to \(status.rawValue)", for: .normal) + self.backgroundColor = .white + self.setTitleColor(.systemBlue, for: .normal) + } + +} diff --git a/ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ToDoListBaseViewModelDelegate.swift b/ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ViewModelDelegate.swift similarity index 70% rename from ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ToDoListBaseViewModelDelegate.swift rename to ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ViewModelDelegate.swift index 9510df4ab..9a3a2ddb9 100644 --- a/ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ToDoListBaseViewModelDelegate.swift +++ b/ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ViewModelDelegate.swift @@ -11,3 +11,7 @@ protocol ToDoListBaseViewModelDelegate: AnyObject { func createData(values: [KeywordArgument]) #endif } + +protocol ToDoListChildViewModelDelegate: AnyObject { + func changeStatus(_ entity: ToDo, to newStatus: ToDoStatus) +} diff --git a/ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ViewModelType.swift b/ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ViewModelType.swift index b231f76c5..1c4c2a1ab 100644 --- a/ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ViewModelType.swift +++ b/ProjectManager/ProjectManager/Presentation/ViewModelProtocol/ViewModelType.swift @@ -44,4 +44,15 @@ protocol ToDoChildViewModelOutputsType { var error: Observable { get set } } +protocol ToDoChangeStatusViewModelType { + var inputs: ToDoChangeStatusViewModelInputsType { get } + var outputs: ToDoChangeStatusViewModelOutputsType { get } +} + +protocol ToDoChangeStatusViewModelInputsType { + func touchUpButton(_ entity: ToDo, status: ToDoStatus) +} +protocol ToDoChangeStatusViewModelOutputsType { + var error: Observable { get set } +} From 6876bb6d168a95dad6fbd8c92df3ac4abed3e65e Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Fri, 6 Oct 2023 09:30:47 +0900 Subject: [PATCH 20/20] =?UTF-8?q?refactor:=20Alert=20Builder=20struct?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BaseView/ToDoListBaseViewController.swift | 10 ++-- .../ToDoListChildViewController.swift | 10 ++-- .../ProjectManager/Utility/AlertBuilder.swift | 50 ++++++++++++------- .../Utility/KeywordArgument.swift | 2 +- 4 files changed, 44 insertions(+), 28 deletions(-) diff --git a/ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewController.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewController.swift index 3b83bd4fe..349447916 100644 --- a/ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewController.swift +++ b/ProjectManager/ProjectManager/Presentation/View/ListView/BaseView/ToDoListBaseViewController.swift @@ -98,11 +98,11 @@ final class ToDoListBaseViewController: UIViewController { guard let self, let error else { return } let alertBuilder = AlertBuilder(prefferedStyle: .alert) - alertBuilder.setControllerTitle(title: error.alertTitle) - alertBuilder.setControllerMessage(message: error.alertMessage) - alertBuilder.addAction(.confirm) - let alertController = alertBuilder.makeAlertController() - present(alertController, animated: true) + .setTitle(error.alertTitle) + .setMessage(error.alertMessage) + .addAction(.confirm) + .build() + present(alertBuilder, animated: true) } } } diff --git a/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewController.swift b/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewController.swift index bceb45766..c9cb100cd 100644 --- a/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewController.swift +++ b/ProjectManager/ProjectManager/Presentation/View/ListView/ChildView/ToDoListChildViewController.swift @@ -134,11 +134,11 @@ extension ToDoListChildViewController { guard let self, let error else { return } let alertBuilder = AlertBuilder(prefferedStyle: .alert) - alertBuilder.setControllerTitle(title: error.alertTitle) - alertBuilder.setControllerMessage(message: error.alertMessage) - alertBuilder.addAction(.confirm) - let alertController = alertBuilder.makeAlertController() - present(alertController, animated: true) + .setTitle(error.alertTitle) + .setMessage(error.alertMessage) + .addAction(.confirm) + .build() + present(alertBuilder, animated: true) } } } diff --git a/ProjectManager/ProjectManager/Utility/AlertBuilder.swift b/ProjectManager/ProjectManager/Utility/AlertBuilder.swift index 8c9337d6b..bb45329ee 100644 --- a/ProjectManager/ProjectManager/Utility/AlertBuilder.swift +++ b/ProjectManager/ProjectManager/Utility/AlertBuilder.swift @@ -7,38 +7,54 @@ import UIKit -final class AlertBuilder { - let alertController: UIAlertController - private var controllerTitle: String = "" - private var controllerMessage: String = "" - private var alertActions: [UIAlertAction] = [] +struct AlertBuilder { + let configuration: AlertConfiguration init(prefferedStyle: UIAlertController.Style) { - self.alertController = UIAlertController(title: nil, message: nil, preferredStyle: prefferedStyle) + self.configuration = AlertConfiguration(prefferedStyle: prefferedStyle) } - func setControllerTitle(title: String) { - self.controllerTitle = title + @discardableResult + func setTitle(_ title: String) -> Self { + configuration.title = title + return self } - func setControllerMessage(message: String) { - self.controllerMessage = message + @discardableResult + func setMessage(_ message: String) -> Self { + configuration.message = message + return self } - func addAction(_ actionType: AlertActionType, action: ((UIAlertAction) -> Void)? = nil) { + @discardableResult + func addAction(_ actionType: AlertActionType, action: ((UIAlertAction) -> Void)? = nil) -> Self { let action = UIAlertAction(title: actionType.title, style: actionType.style, handler: action) - alertActions.append(action) + configuration.actions.append(action) + return self } - func makeAlertController() -> UIAlertController { - alertController.title = controllerTitle - alertController.message = controllerMessage - alertActions.forEach { alertController.addAction($0) } - + func build() -> UIAlertController { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: configuration.prefferedStyle) + alertController.title = configuration.title + alertController.message = configuration.message + configuration.actions.forEach { alertController.addAction($0) } return alertController } } +extension AlertBuilder { + final class AlertConfiguration { + let prefferedStyle: UIAlertController.Style + var title: String = "" + var message: String = "" + var actions: [UIAlertAction] = [] + + init(prefferedStyle: UIAlertController.Style) { + self.prefferedStyle = prefferedStyle + } + } +} + extension AlertBuilder { enum AlertActionType { case confirm diff --git a/ProjectManager/ProjectManager/Utility/KeywordArgument.swift b/ProjectManager/ProjectManager/Utility/KeywordArgument.swift index 3420d5280..452fa5b76 100644 --- a/ProjectManager/ProjectManager/Utility/KeywordArgument.swift +++ b/ProjectManager/ProjectManager/Utility/KeywordArgument.swift @@ -5,7 +5,7 @@ // Created by Max on 2023/09/26. // -final class KeywordArgument { +struct KeywordArgument { let key: String let value: Any?