diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 000000000..ad9650c5d --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,10 @@ +disabled_rules: + - trailing_whitespace + - line_length + - force_cast + +excluded: + - Diary/Application/AppDelegate.swift + - Diary/Application/SceneDelegate.swift + + diff --git a/Diary+CoreDataClass.swift b/Diary+CoreDataClass.swift new file mode 100644 index 000000000..9af415271 --- /dev/null +++ b/Diary+CoreDataClass.swift @@ -0,0 +1,14 @@ +// +// Diary+CoreDataClass.swift +// Diary +// +// Created by 예찬 on 2023/09/05. +// +// + +import Foundation +import CoreData + +@objc(Diary) +public class Diary: NSManagedObject { +} diff --git a/Diary+CoreDataProperties.swift b/Diary+CoreDataProperties.swift new file mode 100644 index 000000000..4dbd97007 --- /dev/null +++ b/Diary+CoreDataProperties.swift @@ -0,0 +1,25 @@ +// +// Diary+CoreDataProperties.swift +// Diary +// +// Created by 예찬 on 2023/09/05. +// +// + +import Foundation +import CoreData + +extension Diary { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "Diary") + } + + @NSManaged public var createdAt: String? + @NSManaged public var title: String? + @NSManaged public var body: String? + @NSManaged public var identifier: UUID +} + +extension Diary: Identifiable { +} diff --git a/Diary.xcodeproj/project.pbxproj b/Diary.xcodeproj/project.pbxproj index da144935d..c6e1c40c5 100644 --- a/Diary.xcodeproj/project.pbxproj +++ b/Diary.xcodeproj/project.pbxproj @@ -7,25 +7,45 @@ objects = { /* Begin PBXBuildFile section */ + 3B95E4922AA18B8A008BFD76 /* CellIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B95E4912AA18B8A008BFD76 /* CellIdentifiable.swift */; }; + 3B95E4DE2AA5D24B008BFD76 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B95E4DD2AA5D24B008BFD76 /* CoreDataManager.swift */; }; + 3B95E4E52AA6E156008BFD76 /* Diary+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B95E4E32AA6E155008BFD76 /* Diary+CoreDataClass.swift */; }; + 3B95E4E62AA6E156008BFD76 /* Diary+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B95E4E42AA6E155008BFD76 /* Diary+CoreDataProperties.swift */; }; + 3BBC98902A9D73D70047DE81 /* DiaryCollectionViewListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BBC988F2A9D73D70047DE81 /* DiaryCollectionViewListCell.swift */; }; + 3BBC98962A9DC5330047DE81 /* DiaryDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BBC98952A9DC5330047DE81 /* DiaryDetailViewController.swift */; }; + 3BBC98992A9E449E0047DE81 /* DateFormatter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BBC98982A9E449E0047DE81 /* DateFormatter+.swift */; }; + 3BBC989D2A9F2BFA0047DE81 /* Array+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BBC989C2A9F2BFA0047DE81 /* Array+.swift */; }; C739AE25284DF28600741E8F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE24284DF28600741E8F /* AppDelegate.swift */; }; C739AE27284DF28600741E8F /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE26284DF28600741E8F /* SceneDelegate.swift */; }; - C739AE29284DF28600741E8F /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE28284DF28600741E8F /* ViewController.swift */; }; - C739AE2C284DF28600741E8F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C739AE2A284DF28600741E8F /* Main.storyboard */; }; + C739AE29284DF28600741E8F /* DiaryListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE28284DF28600741E8F /* DiaryListViewController.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 */; }; + DC3EA1662A9CAE8400986F72 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = DC3EA1652A9CAE8400986F72 /* .swiftlint.yml */; }; + DC5B192E2AA07FAD0064550A /* KeyboardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5B192D2AA07FAD0064550A /* KeyboardManager.swift */; }; + DC5B19302AA08AE70064550A /* LocaleIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5B192F2AA08AE70064550A /* LocaleIdentifier.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 3B95E4912AA18B8A008BFD76 /* CellIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellIdentifiable.swift; sourceTree = ""; }; + 3B95E4DD2AA5D24B008BFD76 /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; + 3B95E4E32AA6E155008BFD76 /* Diary+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Diary+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; }; + 3B95E4E42AA6E155008BFD76 /* Diary+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Diary+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; }; + 3BBC988F2A9D73D70047DE81 /* DiaryCollectionViewListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryCollectionViewListCell.swift; sourceTree = ""; }; + 3BBC98952A9DC5330047DE81 /* DiaryDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryDetailViewController.swift; sourceTree = ""; }; + 3BBC98982A9E449E0047DE81 /* DateFormatter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+.swift"; sourceTree = ""; }; + 3BBC989C2A9F2BFA0047DE81 /* Array+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+.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 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - C739AE2B284DF28600741E8F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + C739AE28284DF28600741E8F /* DiaryListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryListViewController.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 = ""; }; + DC3EA1652A9CAE8400986F72 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; + DC5B192D2AA07FAD0064550A /* KeyboardManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardManager.swift; sourceTree = ""; }; + DC5B192F2AA08AE70064550A /* LocaleIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocaleIdentifier.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -39,9 +59,73 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3B95E4902AA18B74008BFD76 /* Protocol */ = { + isa = PBXGroup; + children = ( + 3B95E4912AA18B8A008BFD76 /* CellIdentifiable.swift */, + ); + path = Protocol; + sourceTree = ""; + }; + 3B95E4C12AA30D09008BFD76 /* Application */ = { + isa = PBXGroup; + children = ( + C739AE24284DF28600741E8F /* AppDelegate.swift */, + C739AE26284DF28600741E8F /* SceneDelegate.swift */, + ); + path = Application; + sourceTree = ""; + }; + 3B95E4C22AA30D4C008BFD76 /* Application Support */ = { + isa = PBXGroup; + children = ( + 3B95E4E32AA6E155008BFD76 /* Diary+CoreDataClass.swift */, + 3B95E4E42AA6E155008BFD76 /* Diary+CoreDataProperties.swift */, + C739AE2D284DF28600741E8F /* Diary.xcdatamodeld */, + ); + path = "Application Support"; + sourceTree = ""; + }; + 3BBC988C2A9D67EC0047DE81 /* Resource */ = { + isa = PBXGroup; + children = ( + C739AE35284DF28600741E8F /* Info.plist */, + C739AE30284DF28600741E8F /* Assets.xcassets */, + ); + path = Resource; + sourceTree = ""; + }; + 3BBC988D2A9D68290047DE81 /* Controller */ = { + isa = PBXGroup; + children = ( + C739AE28284DF28600741E8F /* DiaryListViewController.swift */, + 3BBC98952A9DC5330047DE81 /* DiaryDetailViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + 3BBC988E2A9D683C0047DE81 /* View */ = { + isa = PBXGroup; + children = ( + C739AE32284DF28600741E8F /* LaunchScreen.storyboard */, + 3BBC988F2A9D73D70047DE81 /* DiaryCollectionViewListCell.swift */, + ); + path = View; + sourceTree = ""; + }; + 3BBC98972A9E44820047DE81 /* Extension */ = { + isa = PBXGroup; + children = ( + 3BBC98982A9E449E0047DE81 /* DateFormatter+.swift */, + 3BBC989C2A9F2BFA0047DE81 /* Array+.swift */, + ); + path = Extension; + sourceTree = ""; + }; C739AE18284DF28600741E8F = { isa = PBXGroup; children = ( + DC3EA1652A9CAE8400986F72 /* .swiftlint.yml */, C739AE23284DF28600741E8F /* Diary */, C739AE22284DF28600741E8F /* Products */, ); @@ -58,18 +142,36 @@ C739AE23284DF28600741E8F /* Diary */ = { isa = PBXGroup; children = ( - C739AE24284DF28600741E8F /* AppDelegate.swift */, - C739AE26284DF28600741E8F /* SceneDelegate.swift */, - C739AE28284DF28600741E8F /* ViewController.swift */, - C739AE2A284DF28600741E8F /* Main.storyboard */, - C739AE30284DF28600741E8F /* Assets.xcassets */, - C739AE32284DF28600741E8F /* LaunchScreen.storyboard */, - C739AE35284DF28600741E8F /* Info.plist */, - C739AE2D284DF28600741E8F /* Diary.xcdatamodeld */, + 3B95E4C22AA30D4C008BFD76 /* Application Support */, + 3B95E4C12AA30D09008BFD76 /* Application */, + 3B95E4902AA18B74008BFD76 /* Protocol */, + DC5B19312AA08AEB0064550A /* Enum */, + DC5B192C2AA07FA10064550A /* Manager */, + 3BBC98972A9E44820047DE81 /* Extension */, + 3BBC988E2A9D683C0047DE81 /* View */, + 3BBC988D2A9D68290047DE81 /* Controller */, + 3BBC988C2A9D67EC0047DE81 /* Resource */, ); path = Diary; sourceTree = ""; }; + DC5B192C2AA07FA10064550A /* Manager */ = { + isa = PBXGroup; + children = ( + DC5B192D2AA07FAD0064550A /* KeyboardManager.swift */, + 3B95E4DD2AA5D24B008BFD76 /* CoreDataManager.swift */, + ); + path = Manager; + sourceTree = ""; + }; + DC5B19312AA08AEB0064550A /* Enum */ = { + isa = PBXGroup; + children = ( + DC5B192F2AA08AE70064550A /* LocaleIdentifier.swift */, + ); + path = Enum; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -77,6 +179,7 @@ isa = PBXNativeTarget; buildConfigurationList = C739AE38284DF28600741E8F /* Build configuration list for PBXNativeTarget "Diary" */; buildPhases = ( + DC3EA1622A9CAAAF00986F72 /* SwiftLint Script */, C739AE1D284DF28600741E8F /* Sources */, C739AE1E284DF28600741E8F /* Frameworks */, C739AE1F284DF28600741E8F /* Resources */, @@ -128,37 +231,61 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + DC3EA1662A9CAE8400986F72 /* .swiftlint.yml in Resources */, C739AE34284DF28600741E8F /* LaunchScreen.storyboard in Resources */, C739AE31284DF28600741E8F /* Assets.xcassets in Resources */, - C739AE2C284DF28600741E8F /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + DC3EA1622A9CAAAF00986F72 /* SwiftLint Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "SwiftLint Script"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nexport PATH=\"$PATH:/opt/homebrew/bin\"\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ C739AE1D284DF28600741E8F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C739AE29284DF28600741E8F /* ViewController.swift in Sources */, + C739AE29284DF28600741E8F /* DiaryListViewController.swift in Sources */, + 3BBC989D2A9F2BFA0047DE81 /* Array+.swift in Sources */, C739AE25284DF28600741E8F /* AppDelegate.swift in Sources */, C739AE27284DF28600741E8F /* SceneDelegate.swift in Sources */, + 3BBC98962A9DC5330047DE81 /* DiaryDetailViewController.swift in Sources */, + 3BBC98992A9E449E0047DE81 /* DateFormatter+.swift in Sources */, C739AE2F284DF28600741E8F /* Diary.xcdatamodeld in Sources */, + DC5B19302AA08AE70064550A /* LocaleIdentifier.swift in Sources */, + 3BBC98902A9D73D70047DE81 /* DiaryCollectionViewListCell.swift in Sources */, + DC5B192E2AA07FAD0064550A /* KeyboardManager.swift in Sources */, + 3B95E4922AA18B8A008BFD76 /* CellIdentifiable.swift in Sources */, + 3B95E4DE2AA5D24B008BFD76 /* CoreDataManager.swift in Sources */, + 3B95E4E62AA6E156008BFD76 /* Diary+CoreDataProperties.swift in Sources */, + 3B95E4E52AA6E156008BFD76 /* Diary+CoreDataClass.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - C739AE2A284DF28600741E8F /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - C739AE2B284DF28600741E8F /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; C739AE32284DF28600741E8F /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -221,7 +348,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + INFOPLIST_KEY_LSBackgroundOnly = NO; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -276,7 +404,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + INFOPLIST_KEY_LSBackgroundOnly = NO; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -294,13 +423,12 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = Diary/Info.plist; + INFOPLIST_FILE = Diary/Resource/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -322,13 +450,12 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = Diary/Info.plist; + INFOPLIST_FILE = Diary/Resource/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", 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/AppDelegate.swift b/Diary/AppDelegate.swift deleted file mode 100644 index 7efc2f7c0..000000000 --- a/Diary/AppDelegate.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// Diary - AppDelegate.swift -// Created by yagom. -// Copyright © yagom. All rights reserved. -// - -import UIKit -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. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // 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/Diary.xcdatamodeld/.xccurrentversion b/Diary/Application Support/Diary.xcdatamodeld/.xccurrentversion similarity index 100% rename from Diary/Diary.xcdatamodeld/.xccurrentversion rename to Diary/Application Support/Diary.xcdatamodeld/.xccurrentversion diff --git a/Diary/Application Support/Diary.xcdatamodeld/Diary.xcdatamodel/contents b/Diary/Application Support/Diary.xcdatamodeld/Diary.xcdatamodel/contents new file mode 100644 index 000000000..c8826ecde --- /dev/null +++ b/Diary/Application Support/Diary.xcdatamodeld/Diary.xcdatamodel/contents @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Diary/Application/AppDelegate.swift b/Diary/Application/AppDelegate.swift new file mode 100644 index 000000000..9c4e0ce5d --- /dev/null +++ b/Diary/Application/AppDelegate.swift @@ -0,0 +1,16 @@ +// +// Diary - AppDelegate.swift +// Created by yagom. +// Copyright © yagom. All rights reserved. +// + +import UIKit +import CoreData + +@main +final class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } +} diff --git a/Diary/Application/SceneDelegate.swift b/Diary/Application/SceneDelegate.swift new file mode 100644 index 000000000..5a0330793 --- /dev/null +++ b/Diary/Application/SceneDelegate.swift @@ -0,0 +1,22 @@ +// +// Diary - SceneDelegate.swift +// Created by yagom. +// Copyright © yagom. All rights reserved. +// + +import UIKit + +final class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + window = UIWindow(windowScene: windowScene) + let mainViewController: UIViewController = DiaryListViewController() + + let navigationController = UINavigationController(rootViewController: mainViewController) + window?.rootViewController = navigationController + window?.makeKeyAndVisible() + } +} diff --git a/Diary/Base.lproj/Main.storyboard b/Diary/Base.lproj/Main.storyboard deleted file mode 100644 index 25a763858..000000000 --- a/Diary/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Diary/Controller/DiaryDetailViewController.swift b/Diary/Controller/DiaryDetailViewController.swift new file mode 100644 index 000000000..d610a8f38 --- /dev/null +++ b/Diary/Controller/DiaryDetailViewController.swift @@ -0,0 +1,138 @@ +// +// DiaryDetailViewController.swift +// Diary +// +// Created by idinaloq, yetti on 2023/08/29. +// + +import UIKit + +final class DiaryDetailViewController: UIViewController { + private let diary: Diary + private var keyboardManager: KeyboardManager? + + private let textView: UITextView = { + let view: UITextView = UITextView() + view.font = UIFont.systemFont(ofSize: 17.0) + view.translatesAutoresizingMaskIntoConstraints = false + view.keyboardDismissMode = .interactive + view.alwaysBounceVertical = true + + return view + }() + + override func viewDidLoad() { + super.viewDidLoad() + configureNavigation() + configureUI() + configureTextView() + configureLayout() + setUpKeyboard() + } + + init(diary: Diary) { + self.diary = diary + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureNavigation() { + navigationItem.title = diary.createdAt + let seeMoreButton: UIBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .done, target: self, action: #selector(seeMoreButtonTapped)) + navigationItem.rightBarButtonItem = seeMoreButton + } + + @objc func seeMoreButtonTapped() { + let title = diary.title + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + let shareAction = UIAlertAction(title: "Share", style: .default) { _ in + self.showActivityView(title) + } + let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { _ in + self.deleteButtonTapped() + } + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) + + alertController.addAction(shareAction) + alertController.addAction(deleteAction) + alertController.addAction(cancelAction) + present(alertController, animated: true) + } + + func deleteButtonTapped() { + let deleteAlertController = UIAlertController(title: "Really??", message: "Think one more", preferredStyle: .alert) + let cancelAction = UIAlertAction(title: "Cancel", style: .default) + let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { _ in + CoreDataManager.shared.delete(diary: self.diary.identifier) + self.navigationController?.popViewController(animated: true) + } + + deleteAlertController.addAction(cancelAction) + deleteAlertController.addAction(deleteAction) + present(deleteAlertController, animated: true) + } + + func showActivityView(_ diary: String?) { + let activityViewController = UIActivityViewController(activityItems: [diary as Any], applicationActivities: nil) + present(activityViewController, animated: true) + } + + private func configureUI() { + view.addSubview(textView) + view.backgroundColor = .systemBackground + } + + private func configureTextView() { + textView.delegate = self + guard let title = diary.title, + let body = diary.body else { + return + } + + textView.text = title + "\n" + body + } + + private func configureLayout() { + let safeArea = view.safeAreaLayoutGuide + NSLayoutConstraint.activate([ + textView.topAnchor.constraint(equalTo: safeArea.topAnchor), + textView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor), + textView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor), + textView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor) + ]) + } + + private func setUpKeyboard() { + keyboardManager = KeyboardManager(textView: textView) + } +} + +extension DiaryDetailViewController: UITextViewDelegate { + private func splitText() -> (title: String, body: String)? { + guard let text = textView.text?.trimmingCharacters(in: .whitespacesAndNewlines), + !text.isEmpty else { + return nil + } + + let lines = text.components(separatedBy: "\n") + let title = lines.first ?? "일기 제목" + let body = lines.dropFirst().joined(separator: "\n") + "\n" + + return (title: title, body: body) + } + + private func getDiaryContents() { + let text = splitText() + diary.title = text?.title + diary.body = text?.body + } + + func textViewDidEndEditing(_ textView: UITextView) { + getDiaryContents() + CoreDataManager.shared.saveContext() + } +} diff --git a/Diary/Controller/DiaryListViewController.swift b/Diary/Controller/DiaryListViewController.swift new file mode 100644 index 000000000..5b3a72c50 --- /dev/null +++ b/Diary/Controller/DiaryListViewController.swift @@ -0,0 +1,141 @@ +// +// Diary - DiaryListViewController.swift +// Created by yagom. +// Copyright © yagom. All rights reserved. +// + +import UIKit + +final class DiaryListViewController: UIViewController { + var diaries: [Diary] = [] + + private lazy var collectionView: UICollectionView = { + var configuration: UICollectionLayoutListConfiguration = UICollectionLayoutListConfiguration(appearance: .plain) + swipeAction(&configuration) + + let layout = UICollectionViewCompositionalLayout.list(using: configuration) + + let view = UICollectionView(frame: .zero, collectionViewLayout: layout) + view.translatesAutoresizingMaskIntoConstraints = false + view.register(DiaryCollectionViewListCell.self, forCellWithReuseIdentifier: DiaryCollectionViewListCell.identifier) + + return view + }() + + func swipeAction(_ configuration: inout UICollectionLayoutListConfiguration) { + configuration.trailingSwipeActionsConfigurationProvider = { indexPath in + let uuid = CoreDataManager.shared.fetchAllDiaries()[indexPath.item].identifier + + let delete = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, completionHandler in + self?.collectionView.performBatchUpdates { + CoreDataManager.shared.delete(diary: uuid) + self?.collectionView.deleteItems(at: [indexPath]) + self?.diaries = CoreDataManager.shared.fetchAllDiaries() + } + + completionHandler(true) + } + let title = self.diaries[indexPath.item].title + let share = UIContextualAction(style: .normal, title: "Share") { [weak self] _, _, completionHandler in + self?.showActivityView(title) + completionHandler(true) + } + + return UISwipeActionsConfiguration(actions: [delete, share]) + } + } + + override func viewDidLoad() { + super.viewDidLoad() + configureCollectionView() + configureNavigation() + configureUI() + configureLayout() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + self.diaries = CoreDataManager.shared.fetchAllDiaries() + + diaries.forEach { diary in + if diary.title == nil && diary.body == nil { + CoreDataManager.shared.delete(diary: diary.identifier) + } + } + self.diaries = CoreDataManager.shared.fetchAllDiaries() + collectionView.reloadData() + } + + private func configureCollectionView() { + collectionView.dataSource = self + collectionView.delegate = self + } + + private func configureNavigation() { + let addDiary: UIBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "plus"), style: .done, target: self, action: #selector(createNewDiaryButtonTapped)) + self.navigationItem.rightBarButtonItem = addDiary + navigationItem.title = "일기장" + } + + private func configureUI() { + view.addSubview(collectionView) + view.backgroundColor = .systemBackground + } + + private func configureLayout() { + let safeArea = view.safeAreaLayoutGuide + NSLayoutConstraint.activate([ + collectionView.topAnchor.constraint(equalTo: safeArea.topAnchor), + collectionView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor), + collectionView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor) + ]) + } + + @objc private func createNewDiaryButtonTapped() { + let uuid: UUID = UUID() + CoreDataManager.shared.create(diary: uuid) + + guard let diary: Diary = CoreDataManager.shared.fetchSingleDiary(by: uuid)[safe: 0] else { + return + } + + let diaryDetailViewController: DiaryDetailViewController = DiaryDetailViewController(diary: diary) + navigationController?.pushViewController(diaryDetailViewController, animated: true) + } + + func showActivityView(_ diary: String?) { + let activityViewController = UIActivityViewController(activityItems: [diary as Any], applicationActivities: nil) + present(activityViewController, animated: true) + } +} + +extension DiaryListViewController: UICollectionViewDataSource, UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return diaries.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DiaryCollectionViewListCell.identifier, for: indexPath) as? DiaryCollectionViewListCell else { + return UICollectionViewCell() + } + + guard let diary = diaries[safe: indexPath.item] else { + return cell + } + + cell.configureLabel(with: diary) + + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let diary = diaries[safe: indexPath.item] else { + return + } + + collectionView.deselectItem(at: indexPath, animated: true) + let diaryDetailViewController: DiaryDetailViewController = DiaryDetailViewController(diary: diary) + navigationController?.pushViewController(diaryDetailViewController, animated: true) + } +} diff --git a/Diary/Diary.xcdatamodeld/Diary.xcdatamodel/contents b/Diary/Diary.xcdatamodeld/Diary.xcdatamodel/contents deleted file mode 100644 index 50d2514e8..000000000 --- a/Diary/Diary.xcdatamodeld/Diary.xcdatamodel/contents +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Diary/Enum/LocaleIdentifier.swift b/Diary/Enum/LocaleIdentifier.swift new file mode 100644 index 000000000..7d2a2017b --- /dev/null +++ b/Diary/Enum/LocaleIdentifier.swift @@ -0,0 +1,19 @@ +// +// LocaleIdentifier.swift +// Diary +// +// Created by idinaloq, yetti on 2023/08/31. +// + +import Foundation + +enum LocaleIdentifier: CustomStringConvertible { + case KOR + + var description: String { + switch self { + case .KOR: + return "ko_KR" + } + } +} diff --git a/Diary/Extension/Array+.swift b/Diary/Extension/Array+.swift new file mode 100644 index 000000000..49cc3c480 --- /dev/null +++ b/Diary/Extension/Array+.swift @@ -0,0 +1,12 @@ +// +// Array+.swift +// Diary +// +// Created by idinaloq, yetti on 2023/08/30. +// + +extension Array { + subscript(safe index: Int) -> Element? { + return self.indices ~= index ? self[index] : nil + } +} diff --git a/Diary/Extension/DateFormatter+.swift b/Diary/Extension/DateFormatter+.swift new file mode 100644 index 000000000..7173ca257 --- /dev/null +++ b/Diary/Extension/DateFormatter+.swift @@ -0,0 +1,20 @@ +// +// DateFormatter+.swift +// Diary +// +// Created by idinaloq, yetti on 2023/08/30. +// + +import Foundation + +extension DateFormatter { + static let dateFormatter: DateFormatter = DateFormatter() + static var today: String { + let date: Date = Date(timeIntervalSinceNow: 0) + dateFormatter.locale = Locale(identifier: LocaleIdentifier.KOR.description) + dateFormatter.timeZone = TimeZone(identifier: TimeZone.current.identifier) + dateFormatter.dateStyle = .long + + return dateFormatter.string(from: date) + } +} diff --git a/Diary/Manager/CoreDataManager.swift b/Diary/Manager/CoreDataManager.swift new file mode 100644 index 000000000..ceb6514b0 --- /dev/null +++ b/Diary/Manager/CoreDataManager.swift @@ -0,0 +1,84 @@ +// +// CoreDataManager.swift +// Diary +// +// Created by idinaloq, yetti on 2023/09/04. +// + +import UIKit +import CoreData + +final class CoreDataManager { + static let shared: CoreDataManager = CoreDataManager() + + lazy var persistentContainer: NSPersistentContainer = { + let container = NSPersistentContainer(name: "Diary") + container.loadPersistentStores { _, error in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + } + return container + }() + + var context: NSManagedObjectContext { + return persistentContainer.viewContext + } + + private init() {} + + func create(diary uuid: UUID) { + let object = Diary(context: context) + object.setValue(DateFormatter.today, forKey: "createdAt") + object.setValue(uuid, forKey: "identifier") + saveContext() + } + + func fetchAllDiaries() -> [Diary] { + let fetchRequest: NSFetchRequest = Diary.fetchRequest() + var data: [Diary] = [] + data = fetch(fetchRequest) + + return data + } + + func fetchSingleDiary(by uuid: UUID) -> [Diary] { + let fetchRequest: NSFetchRequest = Diary.fetchRequest() + var data: [Diary] = [] + fetchRequest.predicate = NSPredicate(format: "identifier == %@", uuid.uuidString) + data = fetch(fetchRequest) + + return data + } + + private func fetch(_ request: NSFetchRequest) -> [Diary] { + do { + let data = try context.fetch(request) + return data + } catch { + print(error.localizedDescription) + } + return [] + } + + func delete(diary uuid: UUID) { + guard let diary = fetchSingleDiary(by: uuid)[safe: 0] else { + return + } + + persistentContainer.viewContext.delete(diary) + saveContext() + } + + func saveContext() { + let context = persistentContainer.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } + } +} diff --git a/Diary/Manager/KeyboardManager.swift b/Diary/Manager/KeyboardManager.swift new file mode 100644 index 000000000..c94097cf5 --- /dev/null +++ b/Diary/Manager/KeyboardManager.swift @@ -0,0 +1,40 @@ +// +// KeyboardManager.swift +// Diary +// +// Created by idinaloq, yetti on 2023/08/31. +// + +import UIKit + +final class KeyboardManager { + private let textView: UITextView + + init(textView: UITextView) { + self.textView = textView + setUpKeyboardEvent() + } + + private func setUpKeyboardEvent() { + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) + } + + @objc private func keyboardWillShow(_ notification: Notification) { + guard let userInfo = notification.userInfo as NSDictionary?, + var keyboardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { + return + } + + keyboardFrame = textView.convert(keyboardFrame, from: nil) + var contentInset = textView.contentInset + contentInset.bottom = keyboardFrame.size.height + textView.contentInset = contentInset + textView.verticalScrollIndicatorInsets = textView.contentInset + } + + @objc private func keyboardWillHide() { + textView.contentInset = UIEdgeInsets.zero + textView.verticalScrollIndicatorInsets = textView.contentInset + } +} diff --git a/Diary/Protocol/CellIdentifiable.swift b/Diary/Protocol/CellIdentifiable.swift new file mode 100644 index 000000000..b2e345d45 --- /dev/null +++ b/Diary/Protocol/CellIdentifiable.swift @@ -0,0 +1,16 @@ +// +// IdentifiableCell.swift +// Diary +// +// Created by idinaloq, yetti on 2023/09/01. +// + +protocol CellIdentifiable { + static var identifier: String { get } +} + +extension CellIdentifiable { + static var identifier: String { + return String(describing: self) + } +} diff --git a/Diary/Assets.xcassets/AccentColor.colorset/Contents.json b/Diary/Resource/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Diary/Assets.xcassets/AccentColor.colorset/Contents.json rename to Diary/Resource/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Diary/Assets.xcassets/AppIcon.appiconset/Contents.json b/Diary/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Diary/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Diary/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Diary/Assets.xcassets/Contents.json b/Diary/Resource/Assets.xcassets/Contents.json similarity index 100% rename from Diary/Assets.xcassets/Contents.json rename to Diary/Resource/Assets.xcassets/Contents.json diff --git a/Diary/Info.plist b/Diary/Resource/Info.plist similarity index 90% rename from Diary/Info.plist rename to Diary/Resource/Info.plist index dd3c9afda..0eb786dc1 100644 --- a/Diary/Info.plist +++ b/Diary/Resource/Info.plist @@ -15,8 +15,6 @@ Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main diff --git a/Diary/SceneDelegate.swift b/Diary/SceneDelegate.swift deleted file mode 100644 index c739cbc38..000000000 --- a/Diary/SceneDelegate.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Diary - SceneDelegate.swift -// Created by yagom. -// Copyright © yagom. All rights reserved. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - 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/Base.lproj/LaunchScreen.storyboard b/Diary/View/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Diary/Base.lproj/LaunchScreen.storyboard rename to Diary/View/Base.lproj/LaunchScreen.storyboard diff --git a/Diary/View/DiaryCollectionViewListCell.swift b/Diary/View/DiaryCollectionViewListCell.swift new file mode 100644 index 000000000..b0c5f0db6 --- /dev/null +++ b/Diary/View/DiaryCollectionViewListCell.swift @@ -0,0 +1,103 @@ +// +// DiaryCollectionViewCell.swift +// Diary +// +// Created by idinaloq, yetti on 2023/08/29. +// + +import UIKit + +final class DiaryCollectionViewListCell: UICollectionViewListCell, CellIdentifiable { + private let titleLabel: UILabel = { + let label: UILabel = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 1 + label.font = .preferredFont(forTextStyle: .title2) + + return label + }() + + private let dateLabel: UILabel = { + let label: UILabel = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 1 + label.font = .preferredFont(forTextStyle: .body) + label.setContentCompressionResistancePriority(.init(800), for: .horizontal) + + return label + }() + + private let previewLabel: UILabel = { + let label: UILabel = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 1 + label.font = UIFont.systemFont(ofSize: 12) + label.setContentHuggingPriority(.init(300), for: .horizontal) + return label + }() + + private let titleLabelStackView: UIStackView = { + let stackView: UIStackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + + return stackView + }() + + private let dateAndPreviewStackView: UIStackView = { + let stackView: UIStackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .horizontal + stackView.spacing = 8 + + return stackView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + configureLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureUI() { + titleLabelStackView.addArrangedSubview(titleLabel) + + dateAndPreviewStackView.addArrangedSubview(dateLabel) + dateAndPreviewStackView.addArrangedSubview(previewLabel) + + contentView.addSubview(titleLabelStackView) + contentView.addSubview(dateAndPreviewStackView) + + self.accessories = [.disclosureIndicator()] + } + + private func configureLayout() { + NSLayoutConstraint.activate([ + titleLabelStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5), + titleLabelStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + titleLabelStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + titleLabelStackView.bottomAnchor.constraint(equalTo: dateAndPreviewStackView.topAnchor, constant: -8), + + dateAndPreviewStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + dateAndPreviewStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + dateAndPreviewStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5) + ]) + } + + func configureLabel(with diary: Diary) { + titleLabel.text = diary.title + dateLabel.text = diary.createdAt + previewLabel.text = diary.body + } + + override func prepareForReuse() { + super.prepareForReuse() + titleLabel.text = "" + dateLabel.text = "" + previewLabel.text = "" + } +} diff --git a/Diary/ViewController.swift b/Diary/ViewController.swift deleted file mode 100644 index dd724e13a..000000000 --- a/Diary/ViewController.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Diary - ViewController.swift -// Created by yagom. -// Copyright © yagom. All rights reserved. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } - - -} - diff --git a/README.md b/README.md index 497819d76..6a1e38ec5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,309 @@ -## iOS 커리어 스타터 캠프 +# 📓 일기장 +## 일기를 생성하고 작성 후에 저장 및 삭제할 수 있는 앱입니다. -### 일기장 프로젝트 저장소 +**핵심 개념 및 경험** + +- **DateFormatter** + - `locale` 프로퍼티를 이용한 지역화 +- **CoreData** + - `CoreData`모델을 통한 CRUD 기능 + - (Create, Read(Retrieve), Update, Delete) +- **UITextView** + - `UITextView`에서 텍스트 편집 +- **keyboardWillShowNotification / keyboardWillHideNotification** + - 키보드가 나타나거나 사라질 때 `post`된 `Notification`을 `addObserver`를 통해 수신 +- **subscript** + - 배열의 범위를 벗어난 접근을 할 때 안전하게 접근할 수 있도록 `subscript`를 사용하여 `Array`의 기능 확장 +- **textViewDidEndEditing()** + - `UITextView`의 입력이 끝났을 때 `UITextViewDelegate`를 통해 실행되는 메서드 +- **UIAlertAction** + - 얼럿 버튼을 눌렀을 때 실행되는 액션 -- 이 저장소를 자신의 저장소로 fork하여 프로젝트를 진행합니다 -- 자신의 브랜치에 PR을 보내는지 꼭 확인한 후 PR을 보냅니다 +**프로젝트 기간 : 23.08.28 ~ 23.09.15** +
+ +## 📖 목차 +1. [팀원 소개](#1.) +2. [타임 라인](#2.) +3. [시각화 구조](#3.) +4. [실행 화면](#4. ) +5. [트러블 슈팅](#5.) +6. [참고 자료](#6.) +7. [팀 회고](#7.) + +
+## 👨‍💻 팀원 소개 +||| +|:-:|:-:| +|[**Yetti**](https://github.com/iOS-Yetti)|[**idinaloq**](https://github.com/idinaloq)| + +
+## ⏰ 타임 라인 +|날짜|내용| +|:--:|--| +|2023.08.28.|메인 스토리보드 삭제
SceneDelegate에 rootViewController 추가
SwiftLint적용| +|2023.08.29.|SwiftLint설정 변경
DiaryListViewController구현
DiaryCollectionViewListCell구현
DiaryEntity구현
DiaryDetailViewController생성| +|2023.08.30.|DateFormatter 기능확장
키보드 사용을 위한 setUpKeyboardEvent() 메서드 추가
NewDiaryViewController 구현
리팩토링
| +|2023.08.31.|KeyboardManager 클래스로 키보드 기능분리
LocaleIdentifier타입 생성
리팩토링| +|2023.09.01.|README 작성| +|2023.09.04.|CoreData생성
textView키보드 기능추가
테스트용json제거| +|2023.09.05|CoreData 테스트용 코드 작성| +|2023.09.06|CoreData의 Create,Retieve,Update기능 구현
CoreData관련코드 리팩토링| +|2023.09.07|CoreData의 Delete기능 추가
swipe기능 구현| +|2023.09.08|README 작성| +|2023.09.11|CoreDataManager 리팩토링| +|2023.09.12|textView 데이터 CRUD 기능 리팩토링| +|2023.09.13|Step2 PR 작성| +|2023.09.15|Stpe2 리뷰에 따른 수정
README 작성| + +
+## 👀 시각화 구조 +### 1. File Tree + Diary + ├── Application + │   ├── AppDelegate.swift + │   └── SceneDelegate.swift + ├── Application Support + │   └── Diary.xcdatamodeld  + ├── Controller + │   ├── DiaryDetailViewController.swift + │   └── DiaryListViewController.swift + ├── Enum + │   └── LocaleIdentifier.swift + ├── Error + │   └── DecodingError.swift + ├── Extension + │   ├── Array+.swift + │   ├── CellIdentifiable+.swift + │   └── DateFormatter+.swift + ├── Manager + │   ├── CoreDataManager.swift + │   └── KeyboardManager.swift + ├── Protocol + │   └── CellIdentifiable.swift + ├── View + │ ├── Base.lproj + │ │   └── LaunchScreen.storyboard + │ └── DiaryCollectionViewListCell.swift + ├── Resource + │   ├── Assets.xcassets + │   │   ├── AccentColor.colorset + │   │   ├── AppIcon.appiconset + └── └── Info.plist + + +### 2. 클래스 다이어그램 +![일기장 UML](https://github.com/idinaloq/testRep/assets/124647187/1daf56e3-d2b5-4ca6-b7d8-8646f29e3cab) + +
+## 💻 실행화면 + +|일기 생성화면|스와이프 액션기능|더보기 버튼| +|:-----:|:-----------:|:--------:| +|||| + + +|실행화면(가로)| +|:---:| +|| + +
+## 🧨 트러블 슈팅 + +### 1️⃣ out of range +⚠️ **문제점**
+- collectionView 메서드에서 셀을 생성할 때, diaryEntity 배열에 indexPath.item으로 접근을 해서 데이터를 가져오고 있었습니다. 하지만 이렇게 되면 만약 diaryEntity 배열을 벗어난 indexPath로 접근을 하게되면 앱이 크래시가 날 수 있는 가능성이 있었습니다. + +**기존코드** +```swift +extension DiaryListViewController: UICollectionViewDataSource, UICollectionViewDelegate { + ... + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DiaryCollectionViewListCell.identifier, for: indexPath) as? DiaryCollectionViewListCell else { + return UICollectionViewCell() + } + + guard let diaryEntity = diaryEntity else { + return UICollectionViewCell() + + cell.configureLabel(diaryEntity[indexPath.item]) + + return cell + } + ... +} +``` + +✅ **해결방법**
+- 배열에 잘못된 접근을 할 때(범위를 벗어난 접근) nil이 설정되도록 subscript를 사용해서 안전하게 배열에 접근할 수 있도록 array에 기능을 추가했고, diaryEntity가 nil일 때 빈 셀을 반환하는 부분도 그에 맞게 수정을 다음과 같이 진행했습니다. + +**현재코드** +```swift +extension Array { + subscript(index index: Int) -> Element? { + return self.indices ~= index ? self[index] : nil + } +} + +extension DiaryListViewController: UICollectionViewDataSource, UICollectionViewDelegate { + ... + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DiaryCollectionViewListCell.identifier, for: indexPath) as? DiaryCollectionViewListCell else { + return UICollectionViewCell() + } + + guard let diaryIndex = diaryEntity?[index: indexPath.item] else { + return cell + } + + cell.configureLabel(with: diaryIndex) + + return cell + } + ... +} +``` + + +### 2️⃣ View의 LifeCycle +⚠️ **문제점**
+- `NavigationController`를 통해 다음 뷰로 이동하고 다시 이전 뷰 컨트롤러로 돌아올 때 일기장이 생성되거나 수정된 변경사항을 `cell`에 업데이트 하기 위해 `viewWillAppear()`메서드에 `collectionView.reloadData()` 메서드를 통해 셀이 다시 그려지도록 했습니다. +- 하지만 셀이 업데이트가 되지 않고, 한 번씩 업데이트 주기가 밀리는 현상이 있었습니다. +(다음 데이터가 들어와야 이전 데이터가 업데이트 되는 현상) + +**기존코드** +```swift + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.diaries = CoreDataManager.shared.fetchDiary(Diary.fetchRequest()) + collectionView.reloadData() + } +``` + +✅ **해결방법**
+- `collectionView.reloadData()`메서드는 셀을 다시 그리는 메서드인데, `ViewWillApear`에서 실행하게 되면 뷰가 나타나기 전에 셀을 그려서 적용되지 않는 문제였습니다. 아래 코드와 같이 `ViewDidAppear`에서 뷰가 생성된 후 셀을 그리도록 수정하였습니다. + +**현재코드** +```swift + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + self.diaries = CoreDataManager.shared.fetchDiary(Diary.fetchRequest()) + collectionView.reloadData() + } +``` + +### 3️⃣ CoreData에 배열로 저장된 객체 식별하기 +⚠️ **문제점**
+- CoreData에 `Diary`객체가 `Create`될 때 `[Diary]`와 같이 배열로 만들어지고 있었습니다. `Retrieve`할 때 역시 배열로 반환하고 있는데, 이렇게 된다면 특정 객체의 값을 수정하려고 할때 어느 배열에 어떤 값이 있는지 알 수 없었기 때문에 수정과 삭제가 불가능한 문제가 있었습니다. + +**기존코드** +```swift +import CoreData + +extension Diary { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "Diary") + } + + @NSManaged public var createdAt: String? + @NSManaged public var title: String? + @NSManaged public var body: String? +} + +extension Diary: Identifiable { +} +``` + +✅ **해결방법**
+- 모델 데이터에 `identifier`라는 변수를 만들고, 데이터가 만들어 질 때 `identifier`에 `UUID`값을 할당하는 방식으로 변경해서 원하는 배열에 접근할 수 있도록 변경하였습니다. + +**현재코드** +```siwft +import CoreData + +extension Diary { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "Diary") + } + + @NSManaged public var createdAt: String? + @NSManaged public var title: String? + @NSManaged public var body: String? + @NSManaged public var identifier: String? +} + +extension Diary: Identifiable { +} + +final class CoreDataManager { + ... + func createDiary(_ textView: UITextView) { + ... + object.setValue(UUID().uuidString, forKey: "identifier") + saveContext() + } + ... +} + +``` + +### 4️⃣ 두 개의 ViewController를 하나로 합치기 +⚠️ **문제점**
+- 이전에 뷰컨트롤러는 목록을 보여주는 `DiaryListViewController`, 작성된 일기 내용을 보여주는 `DiaryDetailViewController`, 일기장을 새로 만드는 `NewDiaryViewController` 총 세 개가 있었습니다. +- 일기를 새로 생성하거나, 수정하는 화면을 볼 때 구성 자체는 완전히 동일했고, 기존의 저장된 데이터를 보여주거나 데이터가 없다면 새로 생성해야되는 부분 이외에 차이는 없었습니다. + + +✅ **해결방법**
+- `NewDiaryViewController`를 `DiaryDetailViewController`에 통합시키고, 기존에 `DiaryDetailViewController`의 수정, 저장 기능만 사용하는 로직을 그대로 사용하였습니다. +- 새로운 일기를 미리 생성해서 작성화면으로 넘겨주고, 저장을 하게 되는데, 만약 아무런 내용도 입력하지 않았다면 다시 `DiaryListViewController`로 넘어올 때 생성되었던 일기가 삭제되도록 다음과 같이 작성하였습니다. +```swift +final class DiaryListViewController: UIViewController { + ... + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + self.diaries = CoreDataManager.shared.fetchAllDiaries() + + diaries.forEach { diary in + if diary.title == nil && diary.body == nil { + guard let identifier = diary.identifier else { + return + } + + CoreDataManager.shared.delete(diary: identifier) + } + } + self.diaries = CoreDataManager.shared.fetchAllDiaries() + collectionView.reloadData() + } + ... +} +``` + +
+## 📚 참고자료 +- [🍎 Apple Docs: `DateFormatter`](https://developer.apple.com/documentation/foundation/dateformatter) +- [🍎 Apple Docs: `NotificationCenter`](https://developer.apple.com/documentation/foundation/notificationcenter) +- [🍎 Apple Docs: `keyboardWillShowNotification`](https://developer.apple.com/documentation/uikit/uiresponder/1621576-keyboardwillshownotification) +- [🍎 Apple Docs: `keyboardWillHideNotification`](https://developer.apple.com/documentation/uikit/uiresponder/1621606-keyboardwillhidenotification) +- [🍎 Apple Docs: `UITextView`](https://developer.apple.com/documentation/uikit/uitextview) +- [🍎 Apple Docs: `CoreData`](https://developer.apple.com/documentation/coredata) +- [🍎 Apple Docs: `UIViewController LifeCycle`](https://developer.apple.com/documentation/uikit/uiviewcontroller#1652793) +- [🍎 Apple Docs: `UUID`](https://developer.apple.com/documentation/foundation/uuid) +- [🌐 Blog: `subscript로 안전하게 배열 조회하기`](https://kkimin.tistory.com/86) +- [🌐 Blog: `키보드가 텍스트를 가리지 않도록 하기`](https://velog.io/@qudgh849/keyboard가-TextView를-가릴-때) +- [🌐 Blog: `identifier 재사용 프로토콜`](https://prod.velog.io/@yyyng/셀-재사용-프로토콜) +- [🌐 Blog: `collectionViewCell Swipe`](https://icksw.tistory.com/291) + +
+## 👬 팀 회고 +### To. Idinaloq +- 시간을 잘 맞춰주셔서 좋았습니다 +- 서로 솔직한 의견을 잘 나눌 수 있었던 것 같아 좋았습니다 +- 프로젝트를 마무리 짓지 못한 부분은 아쉽습니다 ㅠㅠ + +### To. Yetti +- 짧은 시간동안 집중해서 팀 프로젝트를 진행하니 효율이 좋다는걸 처음으로 느꼈네요.😄 +- 서로 일정이 있을때 편의를 봐주셔서 너무 좋았습니다!!! 👍