diff --git a/.swiftlint.yml b/.swiftlint.yml index 1b601d3fa..32e5c4a92 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -2,3 +2,4 @@ disabled_rules: - line_length - vertical_whitespace - trailing_whitespace +- type_name diff --git a/Diary.xcodeproj/project.pbxproj b/Diary.xcodeproj/project.pbxproj index 3baf42a20..a492b6ad4 100644 --- a/Diary.xcodeproj/project.pbxproj +++ b/Diary.xcodeproj/project.pbxproj @@ -7,35 +7,47 @@ objects = { /* Begin PBXBuildFile section */ + 5B2E75E42AB322D7004D828F /* Diary.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5B2E75E22AB322D7004D828F /* Diary.xcdatamodeld */; }; + 5B2E75EB2AB323F3004D828F /* DiaryEntity+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2E75E92AB323F3004D828F /* DiaryEntity+CoreDataClass.swift */; }; + 5B2E75EC2AB323F3004D828F /* DiaryEntity+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2E75EA2AB323F3004D828F /* DiaryEntity+CoreDataProperties.swift */; }; 5BBC44392A9F036000C528DD /* MainTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBC44382A9F036000C528DD /* MainTableViewCell.swift */; }; - 5BBC443B2A9F276100C528DD /* DiarySample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBC443A2A9F276100C528DD /* DiarySample.swift */; }; 5BBC443D2A9F30ED00C528DD /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 5BBC443C2A9F30ED00C528DD /* .swiftlint.yml */; }; + 5BF1B2D22AAF422300FDC1A5 /* AlertControllerShowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF1B2D12AAF422300FDC1A5 /* AlertControllerShowable.swift */; }; + 5BF1B2D42AAF42FB00FDC1A5 /* ActivityViewControllerShowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF1B2D32AAF42FB00FDC1A5 /* ActivityViewControllerShowable.swift */; }; + 5BF1B2DA2AB2D9E000FDC1A5 /* MainViewControllerUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF1B2D92AB2D9E000FDC1A5 /* MainViewControllerUseCase.swift */; }; + 5BF1B2DE2AB2DA8800FDC1A5 /* DiaryContentDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF1B2DD2AB2DA8800FDC1A5 /* DiaryContentDTO.swift */; }; + 5BF1B2E02AB2EAC800FDC1A5 /* DiaryDetailViewControllerUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF1B2DF2AB2EAC800FDC1A5 /* DiaryDetailViewControllerUseCase.swift */; }; C739AE25284DF28600741E8F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE24284DF28600741E8F /* AppDelegate.swift */; }; C739AE27284DF28600741E8F /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE26284DF28600741E8F /* SceneDelegate.swift */; }; C739AE29284DF28600741E8F /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE28284DF28600741E8F /* MainViewController.swift */; }; - C739AE2F284DF28600741E8F /* Diary.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C739AE2D284DF28600741E8F /* Diary.xcdatamodeld */; }; C739AE31284DF28600741E8F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C739AE30284DF28600741E8F /* Assets.xcassets */; }; C739AE34284DF28600741E8F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C739AE32284DF28600741E8F /* LaunchScreen.storyboard */; }; - D2C1FBEF2A9F058000526DA5 /* DiaryContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C1FBEE2A9F058000526DA5 /* DiaryContent.swift */; }; - D2C1FBF32A9F0A0900526DA5 /* AddDiaryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C1FBF22A9F0A0900526DA5 /* AddDiaryViewController.swift */; }; + D2BCA2B52AAF3B1500D55F4C /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BCA2B42AAF3B1500D55F4C /* CoreDataManager.swift */; }; + D2C1FBF32A9F0A0900526DA5 /* DiaryDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C1FBF22A9F0A0900526DA5 /* DiaryDetailViewController.swift */; }; D2C1FBF62A9F1EC100526DA5 /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C1FBF52A9F1EC100526DA5 /* AppManager.swift */; }; D2DB46872AA1A83B009D1926 /* ReuseIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DB46862AA1A83B009D1926 /* ReuseIdentifiable.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 5B2E75E32AB322D7004D828F /* Diary.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Diary.xcdatamodel; sourceTree = ""; }; + 5B2E75E92AB323F3004D828F /* DiaryEntity+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiaryEntity+CoreDataClass.swift"; sourceTree = ""; }; + 5B2E75EA2AB323F3004D828F /* DiaryEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiaryEntity+CoreDataProperties.swift"; sourceTree = ""; }; 5BBC44382A9F036000C528DD /* MainTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTableViewCell.swift; sourceTree = ""; }; - 5BBC443A2A9F276100C528DD /* DiarySample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiarySample.swift; sourceTree = ""; }; 5BBC443C2A9F30ED00C528DD /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; + 5BF1B2D12AAF422300FDC1A5 /* AlertControllerShowable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertControllerShowable.swift; sourceTree = ""; }; + 5BF1B2D32AAF42FB00FDC1A5 /* ActivityViewControllerShowable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityViewControllerShowable.swift; sourceTree = ""; }; + 5BF1B2D92AB2D9E000FDC1A5 /* MainViewControllerUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewControllerUseCase.swift; sourceTree = ""; }; + 5BF1B2DD2AB2DA8800FDC1A5 /* DiaryContentDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryContentDTO.swift; sourceTree = ""; }; + 5BF1B2DF2AB2EAC800FDC1A5 /* DiaryDetailViewControllerUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryDetailViewControllerUseCase.swift; sourceTree = ""; }; C739AE21284DF28600741E8F /* Diary.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Diary.app; sourceTree = BUILT_PRODUCTS_DIR; }; C739AE24284DF28600741E8F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C739AE26284DF28600741E8F /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; C739AE28284DF28600741E8F /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; - C739AE2E284DF28600741E8F /* Diary.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Diary.xcdatamodel; sourceTree = ""; }; C739AE30284DF28600741E8F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C739AE33284DF28600741E8F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; C739AE35284DF28600741E8F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D2C1FBEE2A9F058000526DA5 /* DiaryContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryContent.swift; sourceTree = ""; }; - D2C1FBF22A9F0A0900526DA5 /* AddDiaryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddDiaryViewController.swift; sourceTree = ""; }; + D2BCA2B42AAF3B1500D55F4C /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; + D2C1FBF22A9F0A0900526DA5 /* DiaryDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryDetailViewController.swift; sourceTree = ""; }; D2C1FBF52A9F1EC100526DA5 /* AppManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = ""; }; D2DB46862AA1A83B009D1926 /* ReuseIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReuseIdentifiable.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -64,7 +76,7 @@ isa = PBXGroup; children = ( C739AE28284DF28600741E8F /* MainViewController.swift */, - D2C1FBF22A9F0A0900526DA5 /* AddDiaryViewController.swift */, + D2C1FBF22A9F0A0900526DA5 /* DiaryDetailViewController.swift */, ); path = Controller; sourceTree = ""; @@ -81,9 +93,8 @@ 5BBC44362A9F025500C528DD /* Model */ = { isa = PBXGroup; children = ( - C739AE2D284DF28600741E8F /* Diary.xcdatamodeld */, - D2C1FBEE2A9F058000526DA5 /* DiaryContent.swift */, - 5BBC443A2A9F276100C528DD /* DiarySample.swift */, + 5BF1B2DC2AB2DA7D00FDC1A5 /* DTO */, + D2BCA2B22AAF3A8700D55F4C /* CoreData */, ); path = Model; sourceTree = ""; @@ -96,6 +107,23 @@ path = Resource; sourceTree = ""; }; + 5BF1B2DB2AB2D9E500FDC1A5 /* UseCase */ = { + isa = PBXGroup; + children = ( + 5BF1B2D92AB2D9E000FDC1A5 /* MainViewControllerUseCase.swift */, + 5BF1B2DF2AB2EAC800FDC1A5 /* DiaryDetailViewControllerUseCase.swift */, + ); + path = UseCase; + sourceTree = ""; + }; + 5BF1B2DC2AB2DA7D00FDC1A5 /* DTO */ = { + isa = PBXGroup; + children = ( + 5BF1B2DD2AB2DA8800FDC1A5 /* DiaryContentDTO.swift */, + ); + path = DTO; + sourceTree = ""; + }; C739AE18284DF28600741E8F = { isa = PBXGroup; children = ( @@ -120,6 +148,7 @@ D2DB46852AA1A7BC009D1926 /* Protocol */, 5BBC44372A9F025A00C528DD /* Resource */, 5BBC44362A9F025500C528DD /* Model */, + 5BF1B2DB2AB2D9E500FDC1A5 /* UseCase */, 5BBC44352A9F025000C528DD /* View */, 5BBC44342A9F024B00C528DD /* Controller */, D2C1FBF42A9F1EA400526DA5 /* Manager */, @@ -128,10 +157,21 @@ path = Diary; sourceTree = ""; }; + D2BCA2B22AAF3A8700D55F4C /* CoreData */ = { + isa = PBXGroup; + children = ( + 5B2E75E22AB322D7004D828F /* Diary.xcdatamodeld */, + 5B2E75E92AB323F3004D828F /* DiaryEntity+CoreDataClass.swift */, + 5B2E75EA2AB323F3004D828F /* DiaryEntity+CoreDataProperties.swift */, + ); + path = CoreData; + sourceTree = ""; + }; D2C1FBF42A9F1EA400526DA5 /* Manager */ = { isa = PBXGroup; children = ( D2C1FBF52A9F1EC100526DA5 /* AppManager.swift */, + D2BCA2B42AAF3B1500D55F4C /* CoreDataManager.swift */, ); path = Manager; sourceTree = ""; @@ -140,6 +180,8 @@ isa = PBXGroup; children = ( D2DB46862AA1A83B009D1926 /* ReuseIdentifiable.swift */, + 5BF1B2D12AAF422300FDC1A5 /* AlertControllerShowable.swift */, + 5BF1B2D32AAF42FB00FDC1A5 /* ActivityViewControllerShowable.swift */, ); path = Protocol; sourceTree = ""; @@ -237,16 +279,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5B2E75EC2AB323F3004D828F /* DiaryEntity+CoreDataProperties.swift in Sources */, + 5BF1B2D42AAF42FB00FDC1A5 /* ActivityViewControllerShowable.swift in Sources */, + 5BF1B2D22AAF422300FDC1A5 /* AlertControllerShowable.swift in Sources */, C739AE29284DF28600741E8F /* MainViewController.swift in Sources */, + 5B2E75EB2AB323F3004D828F /* DiaryEntity+CoreDataClass.swift in Sources */, C739AE25284DF28600741E8F /* AppDelegate.swift in Sources */, + 5BF1B2E02AB2EAC800FDC1A5 /* DiaryDetailViewControllerUseCase.swift in Sources */, C739AE27284DF28600741E8F /* SceneDelegate.swift in Sources */, - D2C1FBF32A9F0A0900526DA5 /* AddDiaryViewController.swift in Sources */, - D2C1FBEF2A9F058000526DA5 /* DiaryContent.swift in Sources */, - C739AE2F284DF28600741E8F /* Diary.xcdatamodeld in Sources */, + D2C1FBF32A9F0A0900526DA5 /* DiaryDetailViewController.swift in Sources */, + D2BCA2B52AAF3B1500D55F4C /* CoreDataManager.swift in Sources */, 5BBC44392A9F036000C528DD /* MainTableViewCell.swift in Sources */, + 5BF1B2DE2AB2DA8800FDC1A5 /* DiaryContentDTO.swift in Sources */, D2C1FBF62A9F1EC100526DA5 /* AppManager.swift in Sources */, - 5BBC443B2A9F276100C528DD /* DiarySample.swift in Sources */, + 5B2E75E42AB322D7004D828F /* Diary.xcdatamodeld in Sources */, D2DB46872AA1A83B009D1926 /* ReuseIdentifiable.swift in Sources */, + 5BF1B2DA2AB2D9E000FDC1A5 /* MainViewControllerUseCase.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -458,12 +506,12 @@ /* End XCConfigurationList section */ /* Begin XCVersionGroup section */ - C739AE2D284DF28600741E8F /* Diary.xcdatamodeld */ = { + 5B2E75E22AB322D7004D828F /* Diary.xcdatamodeld */ = { isa = XCVersionGroup; children = ( - C739AE2E284DF28600741E8F /* Diary.xcdatamodel */, + 5B2E75E32AB322D7004D828F /* Diary.xcdatamodel */, ); - currentVersion = C739AE2E284DF28600741E8F /* Diary.xcdatamodel */; + currentVersion = 5B2E75E32AB322D7004D828F /* Diary.xcdatamodel */; path = Diary.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Diary.xcodeproj/xcshareddata/xcschemes/Diary.xcscheme b/Diary.xcodeproj/xcshareddata/xcschemes/Diary.xcscheme new file mode 100644 index 000000000..90943b4f2 --- /dev/null +++ b/Diary.xcodeproj/xcshareddata/xcschemes/Diary.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Diary/App/AppDelegate.swift b/Diary/App/AppDelegate.swift index 4614bc7fb..645027cad 100644 --- a/Diary/App/AppDelegate.swift +++ b/Diary/App/AppDelegate.swift @@ -9,16 +9,12 @@ import CoreData @main class AppDelegate: UIResponder, UIApplicationDelegate { - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true } - + // MARK: UISceneSession Lifecycle - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. @@ -30,49 +26,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } - - // MARK: - Core Data stack - - lazy var persistentContainer: NSPersistentContainer = { - /* - The persistent container for the application. This implementation - creates and returns a container, having loaded the store for the - application to it. This property is optional since there are legitimate - error conditions that could cause the creation of the store to fail. - */ - let container = NSPersistentContainer(name: "Diary") - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - return container - }() - - // MARK: - Core Data Saving support - - func saveContext () { - let context = persistentContainer.viewContext - if context.hasChanges { - do { - try context.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nserror = error as NSError - fatalError("Unresolved error \(nserror), \(nserror.userInfo)") - } - } - } } diff --git a/Diary/App/SceneDelegate.swift b/Diary/App/SceneDelegate.swift index 94a3163d3..c79764cbd 100644 --- a/Diary/App/SceneDelegate.swift +++ b/Diary/App/SceneDelegate.swift @@ -12,10 +12,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } - let navigationController = UINavigationController() - appManager = AppManager(navigationController: navigationController) + let diaryCoreDataManager: any CoreDataManagerType = CoreDataManager(name: "Diary") + appManager = AppManager(navigationController: navigationController, coreDataManager: diaryCoreDataManager) appManager?.start() window = UIWindow(windowScene: windowScene) window?.rootViewController = navigationController @@ -45,11 +45,5 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - - // Save changes in the application's managed object context when the application transitions to the background. - (UIApplication.shared.delegate as? AppDelegate)?.saveContext() } } diff --git a/Diary/Controller/AddDiaryViewController.swift b/Diary/Controller/AddDiaryViewController.swift deleted file mode 100644 index 53c014fb7..000000000 --- a/Diary/Controller/AddDiaryViewController.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// AddDiaryViewController.swift -// Diary -// -// Created by Zion, Serena on 2023/08/30. -// - -import UIKit - -final class AddDiaryViewController: UIViewController { - private lazy var textView: UITextView = { - let textView = UITextView() - - textView.text = diaryTitle + "\n\n" + diaryDescription - textView.translatesAutoresizingMaskIntoConstraints = false - return textView - }() - - private let todayDate: String - private let diaryTitle: String - private let diaryDescription: String - - init(todayDate: String, diaryTitle: String, diaryDescription: String) { - self.todayDate = todayDate - self.diaryTitle = diaryTitle - self.diaryDescription = diaryDescription - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - configureUI() - setUpConstraints() - setUpViewController() - } - - private func configureUI() { - view.addSubview(textView) - } - - private func setUpConstraints() { - NSLayoutConstraint.activate([ - textView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), - textView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), - textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - textView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor) - ]) - } - - private func setUpViewController() { - view.backgroundColor = .systemBackground - navigationItem.title = todayDate - } -} diff --git a/Diary/Controller/DiaryDetailViewController.swift b/Diary/Controller/DiaryDetailViewController.swift new file mode 100644 index 000000000..c59575232 --- /dev/null +++ b/Diary/Controller/DiaryDetailViewController.swift @@ -0,0 +1,138 @@ +// +// DiaryDetailViewController.swift +// Diary +// +// Created by Zion, Serena on 2023/08/30. +// + +import UIKit + +protocol DiaryDetailViewControllerDelegate: AnyObject { + func popDiaryDetailViewController() +} + +final class DiaryDetailViewController: UIViewController, AlertControllerShowable, ActivityViewControllerShowable { + private lazy var textView: UITextView = { + let textView = UITextView() + + textView.delegate = self + textView.keyboardDismissMode = .onDrag + textView.becomeFirstResponder() + textView.translatesAutoresizingMaskIntoConstraints = false + return textView + }() + + private let date: String + private let useCase: DiaryDetailViewControllerUseCaseType + weak var delegate: DiaryDetailViewControllerDelegate? + + init(date: String, useCase: DiaryDetailViewControllerUseCaseType) { + self.date = date + self.useCase = useCase + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + configureUI() + setUpConstraints() + setUpViewController() + setUpText() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + addObserver() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + removeObserver() + } + + private func configureUI() { + view.addSubview(textView) + } + + private func setUpConstraints() { + NSLayoutConstraint.activate([ + textView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + textView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + textView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor) + ]) + } + + private func setUpText() { + textView.text = useCase.setUpTextFromDiaryContentDTO() + } + + private func setUpViewController() { + view.backgroundColor = .systemBackground + navigationItem.title = date + navigationItem.rightBarButtonItem = .init(title: "더보기", style: .plain, target: self, action: #selector(didTappedMoreButton)) + } + + private func addObserver() { + NotificationCenter.default.addObserver(self, selector: #selector(saveDiaryContents), name: UIApplication.didEnterBackgroundNotification, object: nil) + } + + private func removeObserver() { + NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) + } + + @objc + private func saveDiaryContents() { + useCase.upsert(diaryDetailContent: textView.text) + } +} + +// MARK: - Button Action +extension DiaryDetailViewController { + @objc + private func didTappedMoreButton() { + let shareAction = UIAlertAction(title: "Share...", style: .default) { _ in + self.didTappedShareAction() + } + + let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { _ in + self.didTappedDeleteAction() + } + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) + + showAlertController(actions: [shareAction, deleteAction, cancelAction]) + } + + private func didTappedDeleteAction() { + let cancelAction = UIAlertAction(title: "취소", style: .cancel) + let deleteAction = UIAlertAction(title: "삭제", style: .destructive) { _ in + self.textView.text = "" + self.useCase.deleteDiary() + self.delegate?.popDiaryDetailViewController() + } + + showAlertController(title: "진짜요?", message: "정말로 삭제하시겠어요?", style: .alert, actions: [cancelAction, deleteAction]) + } + + private func didTappedShareAction() { + let sharedItem = self.textView.text + + showActivityViewController(items: [sharedItem as Any]) + } +} + +// MARK: - TextView Delegate +extension DiaryDetailViewController: UITextViewDelegate { + func textViewDidEndEditing(_ textView: UITextView) { + saveDiaryContents() + } +} diff --git a/Diary/Controller/MainViewController.swift b/Diary/Controller/MainViewController.swift index dc7464ba4..3ca2d9bb7 100644 --- a/Diary/Controller/MainViewController.swift +++ b/Diary/Controller/MainViewController.swift @@ -7,31 +7,33 @@ import UIKit protocol MainViewControllerDelegate: AnyObject { - func didTappedRightAddButton() + func didTappedRightAddButton(newDiaryContent: DiaryContentDTO) + func didSelectRowAt(diaryContent: DiaryContentDTO) } -final class MainViewController: UIViewController { +final class MainViewController: UIViewController, AlertControllerShowable, ActivityViewControllerShowable { enum Section { case main } weak var delegate: MainViewControllerDelegate? - private let diaryContents: [DiaryContent] + private var diaryContents: [DiaryContentDTO]? private let dateFormatter: DateFormatter - private var diffableDatasource: UITableViewDiffableDataSource? + private let useCase: MainViewControllerUseCaseType + private var diffableDatasource: UITableViewDiffableDataSource? - private let tableView: UITableView = { + private lazy var tableView: UITableView = { let tableView = UITableView() - tableView.allowsSelection = false + tableView.delegate = self tableView.register(MainTableViewCell.self, forCellReuseIdentifier: MainTableViewCell.indentifier) tableView.translatesAutoresizingMaskIntoConstraints = false return tableView }() - init(diaryContents: [DiaryContent], dateFormatter: DateFormatter) { - self.diaryContents = diaryContents + init(dateFormatter: DateFormatter, useCase: MainViewControllerUseCaseType) { self.dateFormatter = dateFormatter + self.useCase = useCase super.init(nibName: nil, bundle: nil) } @@ -47,9 +49,19 @@ final class MainViewController: UIViewController { setUpConstraints() setUpViewController() setUpTableViewDiffableDataSource() - setUpTableViewDiffableDataSourceSnapShot() } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + setUpDiaryContents() + setUpTableViewDiffableDataSourceSnapShot(animated: false) + } + + private func setUpDiaryContents() { + diaryContents = useCase.fetchDiaryContentDTO() + } + private func configureUI() { view.addSubview(tableView) } @@ -70,26 +82,53 @@ final class MainViewController: UIViewController { } } +// MARK: - TableView Delegate +extension MainViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let diaryContents else { return } + + delegate?.didSelectRowAt(diaryContent: diaryContents[indexPath.row]) + tableView.deselectRow(at: indexPath, animated: true) + } + + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let shareAction: UIContextualAction = .init(style: .normal, title: "Share") { _, _, _ in + self.didTappedShareAction(index: indexPath.row) + } + + let deleteAction: UIContextualAction = .init(style: .destructive, title: "Delete") { _, _, _ in + self.didTappedDeleteAction(index: indexPath.row) + } + + let swipeActionConfiguration: UISwipeActionsConfiguration = .init(actions: [deleteAction, shareAction]) + + swipeActionConfiguration.performsFirstActionWithFullSwipe = true + return swipeActionConfiguration + } +} + // MARK: - TableViewDiffableDataSource extension MainViewController { private func setUpTableViewDiffableDataSource() { - diffableDatasource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, diarySample in + diffableDatasource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, diaryContent in guard let self = self, let cell = tableView.dequeueReusableCell(withIdentifier: MainTableViewCell.indentifier, for: indexPath) as? MainTableViewCell else { return UITableViewCell() } - let date = Date(timeIntervalSince1970: diarySample.date) + let date = Date(timeIntervalSince1970: diaryContent.date) let formattedDate = self.dateFormatter.string(from: date) - cell.setUpContents(title: diarySample.title, date: formattedDate, body: diarySample.body) + + cell.setUpContents(title: diaryContent.title, date: formattedDate, body: diaryContent.body) return cell }) } - private func setUpTableViewDiffableDataSourceSnapShot() { - var snapShot = NSDiffableDataSourceSnapshot() + private func setUpTableViewDiffableDataSourceSnapShot(animated: Bool = true) { + guard let diaryContents else { return } + var snapShot = NSDiffableDataSourceSnapshot() snapShot.appendSections([.main]) snapShot.appendItems(diaryContents) - diffableDatasource?.apply(snapShot) + diffableDatasource?.apply(snapShot, animatingDifferences: animated) } } @@ -97,6 +136,33 @@ extension MainViewController { extension MainViewController { @objc private func didTappedRightAddButton() { - delegate?.didTappedRightAddButton() + let newDiaryContent = useCase.createNewDiary() + + delegate?.didTappedRightAddButton(newDiaryContent: newDiaryContent) + } + + private func didTappedDeleteAction(index: Int) { + let cancelAction = UIAlertAction(title: "취소", style: .cancel) + let deleteAction = UIAlertAction(title: "삭제", style: .destructive) { _ in + guard let deleteDiary = self.diaryContents?[index] else { return } + + self.useCase.deleteDiary(deleteDiaryId: deleteDiary.identifier) + self.diaryContents?.remove(at: index) + self.setUpTableViewDiffableDataSourceSnapShot() + } + + showAlertController(title: "진짜요?", + message: "정말로 삭제하시겠어요?", + style: .alert, + actions: [cancelAction, deleteAction]) + } + + private func didTappedShareAction(index: Int) { + guard let diaryContents else { return } + + let entity = diaryContents[index] + let sharedItem = entity.title + "\n" + entity.body + + self.showActivityViewController(items: [sharedItem as Any]) } } diff --git a/Diary/Diary.xcdatamodeld/Diary.xcdatamodel/contents b/Diary/Diary.xcdatamodeld/Diary.xcdatamodel/contents new file mode 100644 index 000000000..ff04e6f8c --- /dev/null +++ b/Diary/Diary.xcdatamodeld/Diary.xcdatamodel/contents @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Diary/Manager/AppManager.swift b/Diary/Manager/AppManager.swift index a9be3e07a..c9ba3580b 100644 --- a/Diary/Manager/AppManager.swift +++ b/Diary/Manager/AppManager.swift @@ -11,45 +11,54 @@ final class AppManager { private let navigationController: UINavigationController private let dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() - let localeID = Locale.preferredLanguages.first ?? "kr_KR" - let deviceLocale = Locale(identifier: localeID).languageCode ?? "KST" - dateFormatter.locale = Locale(identifier: deviceLocale) + dateFormatter.locale = Locale.current dateFormatter.timeZone = TimeZone.current - dateFormatter.dateFormat = "yyyy년 MM월 dd일" + dateFormatter.dateStyle = .long return dateFormatter }() - init(navigationController: UINavigationController) { + private let coreDataManager: any CoreDataManagerType + + init(navigationController: UINavigationController, coreDataManager: any CoreDataManagerType) { self.navigationController = navigationController + self.coreDataManager = coreDataManager } func start() { - guard let dairyContents = decodeDairySample(fileName: "sample") else { return } - let mainViewController = MainViewController(diaryContents: dairyContents, dateFormatter: dateFormatter) + let useCase: MainViewControllerUseCaseType = MainViewControllerUseCase(coreDataManager: coreDataManager) + let mainViewController = MainViewController(dateFormatter: dateFormatter, useCase: useCase) mainViewController.delegate = self navigationController.viewControllers = [mainViewController] } - - private func decodeDairySample(fileName: String) -> [DiaryContent]? { - let jsonDecoder = JSONDecoder() - guard let asset = NSDataAsset(name: fileName) else { return nil } - guard let data = try? jsonDecoder.decode([DiaryContent].self, from: asset.data) else { return nil } - - return data - } } // MARK: - MainViewControllerDelegate extension AppManager: MainViewControllerDelegate { - func didTappedRightAddButton() { - let diarySample = DiarySample() + func didSelectRowAt(diaryContent: DiaryContentDTO) { + let date = Date(timeIntervalSince1970: diaryContent.date) + let formattedDate = dateFormatter.string(from: date) + let useCase: DiaryDetailViewControllerUseCaseType = DiaryDetailViewControllerUseCase(coreDataManager: coreDataManager, diaryContent: diaryContent) + let diaryDetailViewController = DiaryDetailViewController(date: formattedDate, useCase: useCase) + + diaryDetailViewController.delegate = self + navigationController.pushViewController(diaryDetailViewController, animated: true) + } + + func didTappedRightAddButton(newDiaryContent: DiaryContentDTO) { let todayDate = dateFormatter.string(from: Date()) - let addDiaryViewController = AddDiaryViewController(todayDate: todayDate, - diaryTitle: diarySample.title, - diaryDescription: diarySample.description) + let useCase: DiaryDetailViewControllerUseCaseType = DiaryDetailViewControllerUseCase(coreDataManager: coreDataManager, diaryContent: newDiaryContent) + let diaryDetailViewController = DiaryDetailViewController(date: todayDate, useCase: useCase) - navigationController.pushViewController(addDiaryViewController, animated: true) + diaryDetailViewController.delegate = self + navigationController.pushViewController(diaryDetailViewController, animated: true) + } +} + +// MARK: - DiaryDetailViewControllerDelegate +extension AppManager: DiaryDetailViewControllerDelegate { + func popDiaryDetailViewController() { + navigationController.popViewController(animated: true) } } diff --git a/Diary/Manager/CoreDataManager.swift b/Diary/Manager/CoreDataManager.swift new file mode 100644 index 000000000..3da6df394 --- /dev/null +++ b/Diary/Manager/CoreDataManager.swift @@ -0,0 +1,90 @@ +// +// CoreDataManager.swift +// Diary +// +// Created by Zion, Serena on 2023/09/11. +// + +import CoreData + +protocol CoreDataManagerType { + associatedtype T + + func fetchData(request: NSFetchRequest) throws -> [T] + func insertData(entityName: String, entityProperties: [String: Any]) + func updateData(request: NSFetchRequest, entityProperties: [String: Any]) where T.ID == UUID + func deleteData(request: NSFetchRequest, identifier: UUID) where T.ID == UUID + func isExistData(request: NSFetchRequest, identifier: UUID) -> Bool where T.ID == UUID +} + +final class CoreDataManager: CoreDataManagerType { + private let name: String + private lazy var context = persistentContainer.viewContext + + private lazy var persistentContainer: NSPersistentContainer = { + let container = NSPersistentContainer(name: name) + + container.loadPersistentStores(completionHandler: { _, _ in }) + return container + }() + + init(name: String) { + self.name = name + } + + func fetchData(request: NSFetchRequest) throws -> [T] { + let data = try context.fetch(request) + + return data + } + + func insertData(entityName: String, entityProperties: [String: Any]) { + if let entity = NSEntityDescription.entity(forEntityName: entityName, in: context) { + let managedObject = NSManagedObject(entity: entity, insertInto: context) + + for entityProperty in entityProperties { + managedObject.setValue(entityProperty.value, forKey: entityProperty.key) + } + + saveContext() + } + } + + func updateData(request: NSFetchRequest, entityProperties: [String: Any]) where T.ID == UUID { + guard let dataList = try? context.fetch(request), let id = entityProperties["id"] as? UUID else { return } + guard let updateObject = dataList.filter({ $0.id == id}).first as? NSManagedObject else { return } + + for entityProperty in entityProperties { + updateObject.setValue(entityProperty.value, forKey: entityProperty.key) + } + + saveContext() + } + + func deleteData(request: NSFetchRequest, identifier: UUID) where T.ID == UUID { + guard let dataList = try? context.fetch(request) else { return } + guard let deleteObject = dataList.filter({ $0.id == identifier}).first as? NSManagedObject else { return } + + context.delete(deleteObject) + saveContext() + } + + func isExistData(request: NSFetchRequest, identifier: UUID) -> Bool where T.ID == UUID { + guard let dataList = try? context.fetch(request) else { return false } + guard let data = dataList.filter({ $0.id == identifier}).first as? NSManagedObject else { return false } + + return true + } + + private func saveContext () { + if context.hasChanges { + do { + try context.save() + } catch { + let nserror = error as NSError + + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } + } +} diff --git a/Diary/Model/CoreData/Diary.xcdatamodeld/Diary.xcdatamodel/contents b/Diary/Model/CoreData/Diary.xcdatamodeld/Diary.xcdatamodel/contents new file mode 100644 index 000000000..3436163cf --- /dev/null +++ b/Diary/Model/CoreData/Diary.xcdatamodeld/Diary.xcdatamodel/contents @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Diary/Model/CoreData/DiaryEntity+CoreDataClass.swift b/Diary/Model/CoreData/DiaryEntity+CoreDataClass.swift new file mode 100644 index 000000000..cd99be1e9 --- /dev/null +++ b/Diary/Model/CoreData/DiaryEntity+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// DiaryEntity+CoreDataClass.swift +// Diary +// +// Created by Hyungmin Lee on 2023/09/14. +// +// + +import Foundation +import CoreData + +@objc(DiaryEntity) +public class DiaryEntity: NSManagedObject { + +} diff --git a/Diary/Model/CoreData/DiaryEntity+CoreDataProperties.swift b/Diary/Model/CoreData/DiaryEntity+CoreDataProperties.swift new file mode 100644 index 000000000..c41eb45f5 --- /dev/null +++ b/Diary/Model/CoreData/DiaryEntity+CoreDataProperties.swift @@ -0,0 +1,28 @@ +// +// DiaryEntity+CoreDataProperties.swift +// Diary +// +// Created by Hyungmin Lee on 2023/09/14. +// +// + +import Foundation +import CoreData + + +extension DiaryEntity { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "DiaryEntity") + } + + @NSManaged public var id: UUID + @NSManaged public var title: String + @NSManaged public var body: String + @NSManaged public var date: Double + +} + +extension DiaryEntity : Identifiable { + +} diff --git a/Diary/Model/DTO/DiaryContentDTO.swift b/Diary/Model/DTO/DiaryContentDTO.swift new file mode 100644 index 000000000..e140041b4 --- /dev/null +++ b/Diary/Model/DTO/DiaryContentDTO.swift @@ -0,0 +1,15 @@ +// +// DiaryContentDTO.swift +// Diary +// +// Created by Hyungmin Lee on 2023/09/14. +// + +import Foundation + +struct DiaryContentDTO: Hashable { + var body: String + var date: Double + var title: String + var identifier: UUID +} diff --git a/Diary/Model/Diary.xcdatamodeld/.xccurrentversion b/Diary/Model/Diary.xcdatamodeld/.xccurrentversion deleted file mode 100644 index d49fecccc..000000000 --- a/Diary/Model/Diary.xcdatamodeld/.xccurrentversion +++ /dev/null @@ -1,8 +0,0 @@ - - - - - _XCCurrentVersionName - Diary.xcdatamodel - - diff --git a/Diary/Model/Diary.xcdatamodeld/Diary.xcdatamodel/contents b/Diary/Model/Diary.xcdatamodeld/Diary.xcdatamodel/contents deleted file mode 100644 index 50d2514e8..000000000 --- a/Diary/Model/Diary.xcdatamodeld/Diary.xcdatamodel/contents +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Diary/Model/DiaryContent.swift b/Diary/Model/DiaryContent.swift deleted file mode 100644 index 3e0b4c7b0..000000000 --- a/Diary/Model/DiaryContent.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// DiaryContent.swift -// Diary -// -// Created by Zion, Serena on 2023/08/30. -// - -import Foundation - -struct DiaryContent: Decodable, Hashable { - private let id: UUID = UUID() - let title: String - let body: String - let date: Double - - private enum CodingKeys: String, CodingKey { - case title - case body - case date = "created_at" - } -} diff --git a/Diary/Model/DiarySample.swift b/Diary/Model/DiarySample.swift deleted file mode 100644 index 2141aeeb0..000000000 --- a/Diary/Model/DiarySample.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// DiarySample.swift -// Diary -// -// Created by Zion, Serena on 2023/08/30. -// - -struct DiarySample { - let title = "드라고요롱이마초미미진사오미" - let description = "A wonderful serenity has taken possession of my entire soul, like these sweet mornings of spring which I enjoy with my whole heart. I am alone, and feel the charm of existence in this spot, which was created for the bliss of souls like mine.\n\nI am so happy, my dear friend, so absorbed in the exquisite sense of mere tranquil existence, that I neglect my talents. I should be incapable of drawing a single stroke at the present moment; and yet I feel that I never was a greater artist than now.\n\nWhen, while the lovely valley teems with vapour around me, and the meridian sun strikes the upper surface of the impenetrable foliage of my trees, and but a few stray gleams steal into the inner sanctuary, I throw myself down among the tall grass by the trickling stream; and, as I lie close to the earth, a thousand unknown plants are noticed by me: when I hear the buzz of the little world among the stalks, and grow familiar with the countless indescribable forms of the insects and flies, then I feel the presence of the Almighty, who formed us in his own image, and the breath\n\nI am so happy, my dear friend, so absorbed in the exquisite sense of mere tranquil existence, that I neglect my talents. I should be incapable of drawing a single stroke at the present moment; and yet I feel that I never was a greater artist than now.\n\nWhen, while the lovely valley teems with vapour around me, and the meridian sun strikes the upper surface of the impenetrable foliage of my trees, and but a few stray gleams steal into the inner sanctuary, I throw myself down among the tall grass by the trickling stream; and, as I lie close to the earth, a thousand unknown plants are noticed by me: when I hear the buzz of the little world among the stalks, and grow familiar with the countless indescribable forms of the insects and flies, then I feel the presence of the Almighty, who formed us in his own image, and the breath\n\nI am so happy, my dear friend, so absorbed in the exquisite sense of mere tranquil existence, that I neglect my talents. I should be incapable of drawing a single stroke at the present moment; and yet I feel that I never was a greater artist than now.\n\nWhen, while the lovely valley teems with vapour around me, and the meridian sun strikes the upper surface of the impenetrable foliage of my trees, and but a few stray gleams steal into the inner sanctuary, I throw myself down among the tall grass by the trickling stream; and, as I lie close to the earth, a thousand unknown plants are noticed by me: when I hear the buzz of the little world among the stalks, and grow familiar with the countless indescribable forms of the insects and flies, then I feel the presence of the Almighty, who formed us in his own image, and the breath" -} diff --git a/Diary/Protocol/ActivityViewControllerShowable.swift b/Diary/Protocol/ActivityViewControllerShowable.swift new file mode 100644 index 000000000..ed6e6a2f3 --- /dev/null +++ b/Diary/Protocol/ActivityViewControllerShowable.swift @@ -0,0 +1,20 @@ +// +// ActivityViewControllerShowable.swift +// Diary +// +// Created by Zion, Serena on 2023/09/11. +// + +import UIKit + +protocol ActivityViewControllerShowable where Self: UIViewController { + func showActivityViewController(items: [Any]) +} + +extension ActivityViewControllerShowable { + func showActivityViewController(items: [Any]) { + let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil) + + present(activityViewController, animated: true) + } +} diff --git a/Diary/Protocol/AlertControllerShowable.swift b/Diary/Protocol/AlertControllerShowable.swift new file mode 100644 index 000000000..bc8a0e807 --- /dev/null +++ b/Diary/Protocol/AlertControllerShowable.swift @@ -0,0 +1,30 @@ +// +// AlertControllerShowable.swift +// Diary +// +// Created by Zion, Serena on 2023/09/11. +// + +import UIKit + +protocol AlertControllerShowable where Self: UIViewController { + func showAlertController(title: String?, + message: String?, + style: UIAlertController.Style, + actions: [UIAlertAction]) +} + +extension AlertControllerShowable { + func showAlertController(title: String? = nil, + message: String? = nil, + style: UIAlertController.Style = .actionSheet, + actions: [UIAlertAction]) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: style) + + actions.forEach { + alertController.addAction($0) + } + + present(alertController, animated: true) + } +} diff --git a/Diary/UseCase/DiaryDetailViewControllerUseCase.swift b/Diary/UseCase/DiaryDetailViewControllerUseCase.swift new file mode 100644 index 000000000..1b9802224 --- /dev/null +++ b/Diary/UseCase/DiaryDetailViewControllerUseCase.swift @@ -0,0 +1,64 @@ +// +// DiaryDetailViewControllerUseCase.swift +// Diary +// +// Created by Hyungmin Lee on 2023/09/14. +// + +import Foundation + +protocol DiaryDetailViewControllerUseCaseType { + func setUpTextFromDiaryContentDTO() -> String + func upsert(diaryDetailContent: String) + func deleteDiary() +} + +final class DiaryDetailViewControllerUseCase: DiaryDetailViewControllerUseCaseType { + private let coreDataManager: any CoreDataManagerType + private let diaryContent: DiaryContentDTO + + init(coreDataManager: any CoreDataManagerType, diaryContent: DiaryContentDTO) { + self.coreDataManager = coreDataManager + self.diaryContent = diaryContent + } + + func setUpTextFromDiaryContentDTO() -> String { + if diaryContent.title.count == 0 && diaryContent.body.count == 0 { + return "" + } + + return diaryContent.title + "\n" + diaryContent.body + } + + func upsert(diaryDetailContent: String) { + guard let (title, body) = convertDiaryData(text: diaryDetailContent) else { return } + let isExistDiary = coreDataManager.isExistData(request: DiaryEntity.fetchRequest(), identifier: diaryContent.identifier) + let diaryEntityProperty: [String: Any] = ["title": title, + "body": body, + "date": Date().timeIntervalSince1970, + "id": diaryContent.identifier] + if isExistDiary { + coreDataManager.updateData(request: DiaryEntity.fetchRequest(), entityProperties: diaryEntityProperty) + } else { + coreDataManager.insertData(entityName: "DiaryEntity", entityProperties: diaryEntityProperty) + } + } + + func deleteDiary() { + coreDataManager.deleteData(request: DiaryEntity.fetchRequest(), identifier: diaryContent.identifier) + } +} + +// MARK: - Private +extension DiaryDetailViewControllerUseCase { + private func convertDiaryData(text: String) -> (String, String)? { + let separatedText = text.split(separator: "\n", maxSplits: 1) + guard let titleText = separatedText.first?.description else { return nil } + + if let bodyText = separatedText.dropFirst().first { + return (titleText, String(describing: bodyText)) + } + + return (titleText, "") + } +} diff --git a/Diary/UseCase/MainViewControllerUseCase.swift b/Diary/UseCase/MainViewControllerUseCase.swift new file mode 100644 index 000000000..025f8df37 --- /dev/null +++ b/Diary/UseCase/MainViewControllerUseCase.swift @@ -0,0 +1,47 @@ +// +// MainViewControllerUseCase.swift +// Diary +// +// Created by Hyungmin Lee on 2023/09/14. +// + +import Foundation + +protocol MainViewControllerUseCaseType { + func fetchDiaryContentDTO() -> [DiaryContentDTO]? + func createNewDiary() -> DiaryContentDTO + func deleteDiary(deleteDiaryId: UUID) +} + +final class MainViewControllerUseCase: MainViewControllerUseCaseType { + private let coreDataManager: any CoreDataManagerType + + init(coreDataManager: any CoreDataManagerType) { + self.coreDataManager = coreDataManager + } + + func fetchDiaryContentDTO() -> [DiaryContentDTO]? { + let fetchRequest = DiaryEntity.fetchRequest() + + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)] + guard let diaryEntity = try? coreDataManager.fetchData(request: fetchRequest) else { return nil } + + return diaryEntity.map { + DiaryContentDTO(body: $0.body, + date: $0.date, + title: $0.title, + identifier: $0.id) + } + } + + func createNewDiary() -> DiaryContentDTO { + return DiaryContentDTO(body: "", + date: Double.zero, + title: "", + identifier: UUID()) + } + + func deleteDiary(deleteDiaryId: UUID) { + coreDataManager.deleteData(request: DiaryEntity.fetchRequest(), identifier: deleteDiaryId) + } +} diff --git a/Diary/View/MainTableViewCell.swift b/Diary/View/MainTableViewCell.swift index ef05b8a8b..73046a6f3 100644 --- a/Diary/View/MainTableViewCell.swift +++ b/Diary/View/MainTableViewCell.swift @@ -84,10 +84,13 @@ extension MainTableViewCell { NSLayoutConstraint.activate([ mainStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), mainStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), - mainStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5), - mainStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5) + mainStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5) ]) + let mainStackViewBottomConstraint = mainStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5) + + mainStackViewBottomConstraint.priority = .init(999) + mainStackViewBottomConstraint.isActive = true dateLabel.setContentCompressionResistancePriority(.required, for: .horizontal) previewLabel.setContentHuggingPriority(.init(1), for: .horizontal) } diff --git a/README.md b/README.md index 98112b9ef..599850a42 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ 나만의 일기를 작성해보세요✏️ - - 주요 개념: UITableViewDiffableDataSource, TextView, Keyboard Layout, DateFormatter, AppManager + - 주요 개념: UITableViewDiffableDataSource, TextView, Keyboard Layout, DateFormatter, AppManager, CoreData, ActivityViewController, AlertController
@@ -46,24 +46,30 @@ |:---:|---| | **2023.08.29** |▫️ Prototype 구현
| | **2023.08.30** |▫️ Lint 적용
▫️ MainVC에 TableView 및 TableViewCell 코드 구현
▫️ AddDiaryVC에 TextView 코드 구현
▫️ DiarySample JSON 모델 추가
▫️ TableViewDiffableDataSource 적용
▫️ DateFormatter에 Localization 적용
▫️ AppManager 구현
▫️ 순환참조 제거
| - +| **2023.09.01** |▫️ ReuseIdentifiable 프로토콜 구현
▫️ CompressionPriority, HuggingPriority를 setUpConstriants로 위치 변경
| +| **2023.09.06** |▫️ CoreData과 DiaryCoreData 생성 및 CRUD 구현
▫️ ActivityViewControllerShowable, AlertControllerShowable 타입 구현
▫️ ViewController에 Showable Type 적용| +| **2023.09.07** |▫️ AppManager에 DiaryDataManager 주입
▫️ DiarySample 삭제 및 DiaryEntity 타입 적용
| +| **2023.09.12** |▫️ viewWillAppear 시 dataFetch
▫️ DiaryDetailViewController Update, Delete 구현
▫️ TextView EndEditing 시, background 시 Save로직 추가
▫️ NotificationCenter Observer 등록
▫️ diffableDataSource delete시 bottom Constraint 충돌해결
▫️ 사용하지 않는 model 삭제
| +| **2023.09.14** |▫️ title만 존재할경우 body도 title처럼 나오는 이슈 수정
▫️ insertData로직 추가 및 data Create반환 값 삭제, DiaryCoreDataManager 삭제
▫️ insertData 에러 수정
▫️ CoreData 수정 및 DiaryContentsDTO 생성
▫️ DiaryDetailViewController UseCase 생성 및 적용
▫️ CoreDataManagerType 생성 및 적용
▫️ Save, Delete 시 에러 수정|
## 4. 📊 다이어그램 - -> 추후 작성 예정 +
## 5. 📲 실행 화면 -| 다이어리 화면 구동 | 다이어리 편집 시 키보드 동작 | -| :--------------: | :-------: | -| | | - +| 다이어리 화면 구동 | 다이어리 편집 시 키보드 동작 | 다이어리 생성 | +| :--------------: | :-------: | :-------: | +| | | | +| **선택 시 편집화면 이동** | **다이어리 화면 share** | **다이어리 화면 delete** | +| | | | + | **다이어리 편집 시 Backgound 저장** | **다이어리 편집 화면 share** | **다이어리 편집 화면 delete**| + | | | |
@@ -119,6 +125,79 @@ <정리 자료> [Compression Resistance Priority](https://medium.com/@LeeZion94/compressionresistance-priority-d17c6f407b7f) [Hugging Priority](https://medium.com/@LeeZion94/uistackview-alignment-fill-hugging-84af069eb694) + +
+ +### 🔥 ViewController에서의 DiaryCoreDataManager에 대한 의존성 +- ViewController(이하 VC)는 VC를 띄우는 데 있어서 필요한 데이터가 아닌 다른 것들을 의존하게 된다면 재사용성이 많이 떨어질 수 밖에 없다고 생각합니다. 따라서 이를 위해 상위 타입인 AppManager를 만들게 되었습니다. + + 하지만 과제를 추가적으로 진행해나가면서 VC에 진입할 때 마다 fetch한 데이터를 갱신하고, Delete기능들 등이 추가되면서 DiaryCoreDataManager를 직접적으로 VC에서 주입했다면 재사용성은 떨어져도 코드의 가독성 및 이후의 유지 보수 관련해서는 더 쉽게 이해할 수 있는 코드가 되지않을까? 라고 생각했습니다. 그렇게 생각한 이유는 DiaryCoreDataManager를 주입받지 않았을 때 발생하는 AppManager와의 많은 소통때문이라고 생각합니다. 따라서 ViewController의 재사용성을 살리면서 불필요하게 많아진 상위 타입으로의 소통을 변경할 수 있는 방법을 생각해봐야했습니다. + +- UseCase Type을 만들어서 위와 같은 문제점을 해결할 수 있었습니다. +ViewController에서 View를 띄우는 로직 및 이벤트를 받아 처리하는 로직 이외의 모든 로직을 UseCase로 넘겨주어 처리하게 했습니다. 따라서 UseCase가 DiaryCoreDataManager를 주입받아 CRUD를 담당하는 기능들을 처리할 수 있도록 했고 이에 따라 ViewController에서는 상위 타입과 소통을 하는 것이 아닌 ViewController에서 가지고 있는 UseCase와 소통함에 따라 의사소통 방식의 개선으로 인해 가독성이 상승했습니다. +또한, ViewController에서는 View를 띄우거나, 사용자로부터 이벤트를 받아 처리하는 로직외의 모든 부분들을 UseCase에서 담당하게 되면서 ViewController의 재사용성도 챙길 수 있게 되었습니다. + +
+ +### 🔥 TableView delete 한 후 Scroll할 때의 Constraint 충돌 +- 현재 TableView에서는 DiffableDataSource을 활용하여 Cell을 관리하고 있습니다. Cell을 Swipe 했을 때 존재하는 Delete 기능을 사용하여 Cell을 삭제하고 스크롤시 아래와 같은 오류가 발생했습니다. +```swift +2023-09-12 16:28:45.001423+0900 Diary[23292:2537329] [LayoutConstraints] Unable to simultaneously satisfy constraints. + Probably at least one of the constraints in the following list is one you don't want. + Try this: + (1) look at each constraint and try to figure out which you don't expect; + (2) find the code that added the unwanted constraint or constraints and fix it. +( + "", + "", + "" +) + +Will attempt to recover by breaking constraint + + +Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. +The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in may also be helpful. +``` +- 오류 내용을 분석한 결과 Cell이 화면에 보이지 않을 때 발생하는 Constraint(UIView-Encapsulated-Layout-Height)와 제가 부여한 Cell이 Constraints가 충돌하여 나타나는 오류로 보였습니다. + 따라서 해당 오류를 해결하기 위해서 bottomConstraint에 대한 Priority를 999로 낮춰서 constraint가 충돌하는 것을 해결할 수 있었습니다. + + 파악한 오류의 원인만 봐서는 누구나 경험할 수 있는 오류라고 판단됩니다. 그렇게 생각한 이유는 화면에 보이지 않을 때의 셀의 제약조건을 height == 0으로 준다면 어떤 조건이라도 충돌이 발생할 수 있기 때문입니다. + +[문제에 대한 질문: StackOverFlow](https://stackoverflow.com/questions/77043899/using-uitableviewdiffabledatasource-in-uitableview-add-or-deletecell-warning) +오류해결 커밋: (https://github.com/yagom-academy/ios-diary/commit/bc89e580507d90d480db6a61672760fc5ee205ae) + +
+ +### 🔥 DiaryDetailViewController에서 delete시 save 중복 +- `CoreData`의 `content`를 `delete`한 후 이를 `context`에 `save`하는 로직을 구현하였습니다. 이때 하기와 같은 에러가 발생하였습니다. + > [error] error: Mutating a managed object 0xaef4c17744feb12c (0x6000013d75c0) after it has been removed from its context. +- 오류 원인을 `save()`의 공식 문서에서 알 수 있게 되었습니다. + > Always verify that the context has uncommitted changes (using the hasChanges property) before invoking the save: method. Otherwise, Core Data may perform unnecessary work. + > `save`메서드는 호출하기 전 `context`의 변동사항을 체크하는데, 변동사항이 없을 시 `save`를 호출하는 것은 불필요한 작업이 됩니다. + [AppleDeveloper - save()](https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/1506866-save) + + 이를 기반으로 디버깅을 한 결과 `delete` 시 `save`가 연달아 `두 번` 호출되는 것을 확인할 수 있었습니다. 두번째 화면인 `DiaryDetailViewController`에서 `delete`와 `save`를 한 후 첫번째 화면으로 바로 넘기는데, 첫번째 화면이 띄워지는 과정에서 데이터를 `fetch`하고 `save`를 호출하고 있었습니다. + + 이에 `save` 호출이 중복되지 않게 코드 수정을 하여 에러 발생을 방지하였습니다. + +
+ +### 🔥 Protocol을 활용하여 중복코드 삭제 및 수평적 확장 +- `share`와 `delete` 기능 관련 `AlertController`와 `ActivityViewController` 코드가 각 `ViewController`에서 중복되었습니다. 하여 중복코드를 삭제하면서 `UIViewController`에 `Alert/Activity Controller`의 역할을 확장시킬 수 있도록 하고자 하였습니다. + + 각각 `AlertControllerShowable`, `ActivityViewControllerShowable` 프로토콜을 생성하였습니다. `extension`에 `기본 구현`을 함으로 코드의 중복을 삭제시킬 수 있었습니다. + + 프로토콜을 사용하면 수평적 기능 확장이 가능하게 된다는 장점이 존재합니다. 하지만 무분별하게 확장사용하는 것을 제한할 수 있도록 `where Self`를 사용하여 `UIViewController`에서만 기능 확장 사용이 가능하도록 하였습니다. + +
+ +### 🔥 DiaryDetailViewController에 entity 주입 +- `diary text`를 작성하는 도중 앱이 `background`로 가는 경우 `저장`이 될 수 있도록 구현하고자 하였습니다. 이때 `text`가 있는 경우 새롭게 `create`를 하거나 `update`를 하여 변경된 내용을 저장하고자 하였습니다. + + 저희는 `entity`의 유무로 `create`와 `update`의 기준을 나누었기 때문에, 앱 사용 도중 최초 저장을 하게 되면 `create` 로직을 타고 새로운 `content`를 생성하였습니다. 하지만 `background`에 있던 앱을 다시 `foreground`로 가져와 수정을 완료하여 저장하고자 할 때 `entity`가 없기 때문에 다시 `create`로직을 타는 문제가 생겼습니다. + + 이를 해결하고자 기존의 `DiaryCoreDataManager`의 `createDiaryData` 메서드가 `entity`를 반환하도록 수정하여 `create` 시 `DiaryDetailViewController`에 주입해주었습니다. 이로서 `text`가 있는 경우 중간에 앱이 `background`로 가게되면 바로 `entity`를 `create`하여 새로운 `entity`를 `DiaryDetailViewController`에 주입하였습니다. 이렇게 하여 추가 수정을 완료하여 저장하게 되면 `entity`가 있다고 판단하여 `update`로직을 탈 수 있도록 하였습니다.
@@ -141,4 +220,9 @@ - [🍎 Apple Developer - UICollectionViewDiffableDataSource](https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource) - [🍎 Apple Developer - Date](https://developer.apple.com/documentation/foundation/date) - [🍎 Apple Developer - DateFormatter](https://developer.apple.com/documentation/foundation/dateformatter) +- [🍎 Apple Developer - Core Data](https://developer.apple.com/documentation/coredata) +- [🍎 Apple Developer - Making Apps with Core Data](https://developer.apple.com/videos/play/wwdc2019/230 +- [🍎 Apple Developer - UITextViewDelegate](https://developer.apple.com/documentation/uikit/uitextviewdelegate) +- [🍎 Apple Developer - UISwipeActionsConfiguration](https://developer.apple.com/documentation/uikit/uiswipeactionsconfiguration) - [📒 Blog - NSDate, DateFormatter](https://velog.io/@dev_jane/NSDate-DateFormatter-사용하여-사용자의-기기에-맞는-날짜-설정하기) +- [📒 Blog - Core Data](https://zeddios.tistory.com/987)