From a2e4cf938ede8d6f4f6c0e86d8c4e35d8ad3f5f5 Mon Sep 17 00:00:00 2001 From: JH713 Date: Tue, 27 Feb 2024 12:01:34 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20CoreData=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iBox.xcdatamodel/contents | 17 +- iBox/Sources/AppDelegate.swift | 45 --- iBox/Sources/Model/Bookmark.swift | 3 +- iBox/Sources/Model/Folder.swift | 3 +- .../Presenter/BoxList/BoxListCell.swift | 1 + .../Presenter/BoxList/BoxListView.swift | 2 +- .../BoxList/BoxListViewController.swift | 5 +- .../Presenter/Web/PreloadedWebView.swift | 4 +- .../Web/PreloadedWebViewController.swift | 18 +- iBox/Sources/SceneDelegate.swift | 26 +- iBox/Sources/Utils/CoreDataManager.swift | 342 ++++++++++++++++++ iBox/Sources/Utils/UserDefaultsManager.swift | 13 +- .../ViewModel/BoxListCellViewModel.swift | 14 +- .../ViewModel/BoxListSectionViewModel.swift | 4 +- iBox/Sources/ViewModel/BoxListViewModel.swift | 13 +- 15 files changed, 425 insertions(+), 85 deletions(-) create mode 100644 iBox/Sources/Utils/CoreDataManager.swift diff --git a/iBox/Resources/iBox.xcdatamodeld/iBox.xcdatamodel/contents b/iBox/Resources/iBox.xcdatamodeld/iBox.xcdatamodel/contents index 50d2514..c40ccfd 100644 --- a/iBox/Resources/iBox.xcdatamodeld/iBox.xcdatamodel/contents +++ b/iBox/Resources/iBox.xcdatamodeld/iBox.xcdatamodel/contents @@ -1,4 +1,17 @@ - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/iBox/Sources/AppDelegate.swift b/iBox/Sources/AppDelegate.swift index 1d9e30e..b299a39 100644 --- a/iBox/Sources/AppDelegate.swift +++ b/iBox/Sources/AppDelegate.swift @@ -32,50 +32,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // 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: "iBox") - 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/iBox/Sources/Model/Bookmark.swift b/iBox/Sources/Model/Bookmark.swift index 2091151..ab1a85f 100644 --- a/iBox/Sources/Model/Bookmark.swift +++ b/iBox/Sources/Model/Bookmark.swift @@ -8,6 +8,7 @@ import Foundation struct Bookmark: Codable { + let id: UUID let name: String - let url: String + let url: URL } diff --git a/iBox/Sources/Model/Folder.swift b/iBox/Sources/Model/Folder.swift index 12f21e6..261755f 100644 --- a/iBox/Sources/Model/Folder.swift +++ b/iBox/Sources/Model/Folder.swift @@ -8,9 +8,10 @@ import Foundation struct Folder { + var id: UUID let name: String let color: ColorName let bookmarks: [Bookmark] - var isOpened: Bool = true + var isOpened: Bool = false } diff --git a/iBox/Sources/Presenter/BoxList/BoxListCell.swift b/iBox/Sources/Presenter/BoxList/BoxListCell.swift index d457aac..158decd 100644 --- a/iBox/Sources/Presenter/BoxList/BoxListCell.swift +++ b/iBox/Sources/Presenter/BoxList/BoxListCell.swift @@ -29,6 +29,7 @@ class BoxListCell: UITableViewCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) backgroundColor = .systemGroupedBackground + selectionStyle = .none setupLayout() } diff --git a/iBox/Sources/Presenter/BoxList/BoxListView.swift b/iBox/Sources/Presenter/BoxList/BoxListView.swift index 9a7bddb..93a2caf 100644 --- a/iBox/Sources/Presenter/BoxList/BoxListView.swift +++ b/iBox/Sources/Presenter/BoxList/BoxListView.swift @@ -11,7 +11,7 @@ import UIKit import SnapKit protocol BoxListViewDelegate: AnyObject { - func didSelectWeb(at url: String, withName name: String) + func didSelectWeb(at url: URL, withName name: String) } class BoxListView: BaseView { diff --git a/iBox/Sources/Presenter/BoxList/BoxListViewController.swift b/iBox/Sources/Presenter/BoxList/BoxListViewController.swift index 5607379..b1cb49d 100644 --- a/iBox/Sources/Presenter/BoxList/BoxListViewController.swift +++ b/iBox/Sources/Presenter/BoxList/BoxListViewController.swift @@ -31,10 +31,9 @@ class BoxListViewController: BaseNavigationBarViewController { } extension BoxListViewController: BoxListViewDelegate { - func didSelectWeb(at url: String, withName name: String) { - let viewController = PreloadedWebViewController() + func didSelectWeb(at url: URL, withName name: String) { + let viewController = PreloadedWebViewController(selectedWebsite: url) viewController.title = name - viewController.selectedWebsite = url navigationController?.pushViewController(viewController, animated: true) } } diff --git a/iBox/Sources/Presenter/Web/PreloadedWebView.swift b/iBox/Sources/Presenter/Web/PreloadedWebView.swift index 202a489..9856e7b 100644 --- a/iBox/Sources/Presenter/Web/PreloadedWebView.swift +++ b/iBox/Sources/Presenter/Web/PreloadedWebView.swift @@ -11,7 +11,7 @@ import WebKit import SnapKit class PreloadedWebView: BaseView { - var selectedWebsite: String? { + var selectedWebsite: URL? { didSet { getWebView() } @@ -30,7 +30,7 @@ class PreloadedWebView: BaseView { private func getWebView() { guard let selectedWebsite else { return } - webView = WebViewPreloader.shared.getWebView(for: URL(string: selectedWebsite)!) + webView = WebViewPreloader.shared.getWebView(for: selectedWebsite) guard let webView else { return } addSubview(webView) webView.snp.makeConstraints { make in diff --git a/iBox/Sources/Presenter/Web/PreloadedWebViewController.swift b/iBox/Sources/Presenter/Web/PreloadedWebViewController.swift index c3872a0..e248059 100644 --- a/iBox/Sources/Presenter/Web/PreloadedWebViewController.swift +++ b/iBox/Sources/Presenter/Web/PreloadedWebViewController.swift @@ -8,8 +8,17 @@ import UIKit class PreloadedWebViewController: BaseNavigationBarViewController { - var selectedWebsite: String? - + var selectedWebsite: URL + + init(selectedWebsite: URL) { + self.selectedWebsite = selectedWebsite + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground @@ -21,9 +30,8 @@ class PreloadedWebViewController: BaseNavigationBarViewController) { if let urlContext = URLContexts.first { let url = urlContext.url @@ -87,7 +108,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // 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/iBox/Sources/Utils/CoreDataManager.swift b/iBox/Sources/Utils/CoreDataManager.swift new file mode 100644 index 0000000..3ee52f4 --- /dev/null +++ b/iBox/Sources/Utils/CoreDataManager.swift @@ -0,0 +1,342 @@ +// +// CoreDataManager.swift +// iBox +// +// Created by 이지현 on 2/9/24. +// + +import CoreData +import Foundation + +class CoreDataManager { + static let shared = CoreDataManager() + + lazy var persistentContainer = { + let container = NSPersistentContainer(name: "iBox") + + container.loadPersistentStores { _, error in + if let error { + fatalError(error.localizedDescription) + } + } + + return container + }() + + private init() {} + + private var lastFolderOrder: Int64 = 0 + private var lastBookmarkOrder = [UUID: Int64]() + + private func save() { + guard persistentContainer.viewContext.hasChanges else { return } + do { + try persistentContainer.viewContext.save() + } catch { + print("Fail to save the context:", error.localizedDescription) + } + } +} + +// 폴더 관련 +extension CoreDataManager { + func addInitialFolders(_ folders: [Folder]) { + let context = persistentContainer.viewContext + + for folder in folders { + let newFolder = FolderEntity(context: context) + newFolder.id = folder.id + newFolder.name = folder.name + newFolder.color = folder.color.rawValue + newFolder.order = lastFolderOrder + lastFolderOrder += 1 + let bookmarks = NSMutableOrderedSet() + lastBookmarkOrder[folder.id] = 0 + for bookmark in folder.bookmarks { + let newBookmark = BookmarkEntity(context: context) + newBookmark.id = bookmark.id + newBookmark.name = bookmark.name + newBookmark.url = bookmark.url + newBookmark.order = lastBookmarkOrder[folder.id] ?? 0 + lastBookmarkOrder[folder.id] = (lastBookmarkOrder[folder.id] ?? 0) + 1 + bookmarks.add(newBookmark) + } + newFolder.bookmarks = bookmarks + } + save() + } + + func addFolder(_ folder: Folder) { + let context = persistentContainer.viewContext + let newFolder = FolderEntity(context: context) + newFolder.id = folder.id + newFolder.name = folder.name + newFolder.color = folder.color.rawValue + newFolder.order = lastFolderOrder + lastFolderOrder += 1 + let bookmarks = NSMutableOrderedSet() + lastBookmarkOrder[folder.id] = 0 + for bookmark in folder.bookmarks { + let newBookmark = BookmarkEntity(context: context) + newBookmark.id = bookmark.id + newBookmark.name = bookmark.name + newBookmark.url = bookmark.url + newBookmark.order = lastBookmarkOrder[folder.id] ?? 0 + lastBookmarkOrder[folder.id] = (lastBookmarkOrder[folder.id] ?? 0) + 1 + bookmarks.add(newBookmark) + } + newFolder.bookmarks = bookmarks + save() + } + + private func getFolderEntity(id: UUID) -> FolderEntity? { + let context = persistentContainer.viewContext + + let fetchRequest = FolderEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "id == %@", id as NSUUID) + + do { + let results = try context.fetch(fetchRequest) + return results.first + } catch { + print(error.localizedDescription) + return nil + } + } + + private func getAllFolderEntity() -> [FolderEntity] { + let context = persistentContainer.viewContext + + let fetchRequest = FolderEntity.fetchRequest() + let sortDescriptor = NSSortDescriptor(key: "order", ascending: true) + fetchRequest.sortDescriptors = [sortDescriptor] + + do { + return try context.fetch(fetchRequest) + } catch { + print(error.localizedDescription) + return [] + } + } + + func getFolders() -> [Folder] { + let folderEntities = getAllFolderEntity() + var folders = [Folder]() + + lastFolderOrder = (folderEntities.last?.order ?? -1) + 1 + for folderEntity in folderEntities { + let bookmarkEntities = (folderEntity.bookmarks?.array as? [BookmarkEntity] ?? []).sorted { + $0.order < $1.order + } + guard let folderId = folderEntity.id else { return [] } + lastBookmarkOrder[folderId] = (bookmarkEntities.last?.order ?? -1) + 1 + let bookmarks = bookmarkEntities.map{ Bookmark(id: $0.id ?? UUID(), name: $0.name ?? "" , url: $0.url ?? URL(string: "")!) } + folders.append(Folder(id: folderEntity.id ?? UUID(), name: folderEntity.name ?? "", color: ColorName(rawValue: folderEntity.color ?? "gray") ?? .gray , bookmarks: bookmarks)) + } + + return folders + } + + func deleteFolder(id: UUID) { + let context = persistentContainer.viewContext + + guard let folder = getFolderEntity(id: id) else { return } + let deletedOrder = folder.order + context.delete(folder) + + let subsequentFolderEntities = getAllFolderEntity().filter{ $0.order > deletedOrder } + for folderEntity in subsequentFolderEntities { + folderEntity.order -= 1 + } + lastFolderOrder -= 1 + save() + } + + func deleteAllFolders() { + let context = persistentContainer.viewContext + + let folders = getAllFolderEntity() + for folder in folders { + context.delete(folder) + } + save() + } + + func updateFolder(id: UUID, name: String, color: String) { + guard let folder = getFolderEntity(id: id) else { return } + folder.name = name + folder.color = color + save() + } + + func moveFolder(from source: Int, to destination: Int) { + let folderEntities = getAllFolderEntity() + + if source < destination { + var startIndex = source + 1 + let endIndex = destination + var startOrder = folderEntities[source].order + while startIndex <= endIndex { + folderEntities[startIndex].order = startOrder + startOrder += 1 + startIndex += 1 + } + folderEntities[source].order = startOrder + } else if destination < source { + var startIndex = destination + let endIndex = source - 1 + var startOrder = folderEntities[destination].order + 1 + let newOrder = folderEntities[destination].order + while startIndex <= endIndex { + folderEntities[startIndex].order = startOrder + startOrder += 1 + startIndex += 1 + } + folderEntities[source].order = newOrder + } + save() + } + +} + +// 북마크 관련 +extension CoreDataManager { + + func addBookmark(_ bookmark: Bookmark, folderId: UUID) { + let context = persistentContainer.viewContext + + guard let folder = getFolderEntity(id: folderId) else { return } + let newBookmark = BookmarkEntity(context: context) + newBookmark.id = bookmark.id + newBookmark.name = bookmark.name + newBookmark.url = bookmark.url + guard let folderId = folder.id else { return } + newBookmark.order = lastBookmarkOrder[folderId] ?? 0 + lastBookmarkOrder[folderId] = (lastBookmarkOrder[folderId] ?? 0) + 1 + newBookmark.folder = folder + save() + } + + func updateBookmark(id: UUID, name: String, url: URL) { + guard let bookmark = getBookmarkEntity(id: id) else { return } + bookmark.name = name + bookmark.url = url + save() + } + + private func getBookmarkEntity(id: UUID) -> BookmarkEntity? { + let context = persistentContainer.viewContext + + let fetchRequest = BookmarkEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "id == %@", id as NSUUID) + + do { + let results = try context.fetch(fetchRequest) + return results.first + } catch { + print(error.localizedDescription) + return nil + } + } + + func deleteBookmark(id: UUID) { + let context = persistentContainer.viewContext + + guard let bookmark = getBookmarkEntity(id: id) else { return } + let deletedOrder = bookmark.order + context.delete(bookmark) + + guard let folderId = bookmark.folder?.id else { return } + let subsequentBookmarkEntities = getAllBookmarkEntity(in: folderId).filter{ $0.order > deletedOrder } + for bookmarkEntity in subsequentBookmarkEntities { + bookmarkEntity.order -= 1 + } + lastBookmarkOrder[folderId] = (lastBookmarkOrder[folderId] ?? 1) - 1 + save() + } + + private func getAllBookmarkEntity(in folderId: UUID) -> [BookmarkEntity] { + let context = persistentContainer.viewContext + + guard let folder = getFolderEntity(id: folderId) else { return [] } + let fetchRequest = BookmarkEntity.fetchRequest() + let sortDescriptor = NSSortDescriptor(key: "order", ascending: true) + fetchRequest.predicate = NSPredicate(format: "folder == %@", folder) + fetchRequest.sortDescriptors = [sortDescriptor] + + do { + return try context.fetch(fetchRequest) + } catch { + print(error.localizedDescription) + return [] + } + } + + func deleteAllBookmarks(folderId: UUID) { + let context = persistentContainer.viewContext + + let bookmarks = getAllBookmarkEntity(in: folderId) + for bookmark in bookmarks { + context.delete(bookmark) + } + save() + } + + func moveBookmark(from source: IndexPath, to destination: IndexPath, srcId: UUID) { + if source.section == destination.section { + guard let bookmarkEntity = getBookmarkEntity(id: srcId) else { return } + guard let folderId = bookmarkEntity.folder?.id else { return } + let bookmarkEntities = getAllBookmarkEntity(in: folderId) + let source = source.row + let destination = destination.row + if source < destination { + var startIndex = source + 1 + let endIndex = destination + var startOrder = bookmarkEntity.order + while startIndex <= endIndex { + bookmarkEntities[startIndex].order = startOrder + startOrder += 1 + startIndex += 1 + } + bookmarkEntities[source].order = startOrder + } else if destination < source { + var startIndex = destination + let endIndex = source - 1 + var startOrder = bookmarkEntities[destination].order + 1 + let newOrder = bookmarkEntities[destination].order + while startIndex <= endIndex { + bookmarkEntities[startIndex].order = startOrder + startOrder += 1 + startIndex += 1 + } + bookmarkEntities[source].order = newOrder + } + } else { + guard let srcBookmarkEntity = getBookmarkEntity(id: srcId) else { return } + guard let srcFolderId = srcBookmarkEntity.folder?.id else { return } + let deletedOrder = srcBookmarkEntity.order + let srcBookmarkEntities = getAllBookmarkEntity(in: srcFolderId).filter{ $0.order > deletedOrder } + for bookmarkEntity in srcBookmarkEntities { + bookmarkEntity.order -= 1 + } + lastBookmarkOrder[srcFolderId] = (lastBookmarkOrder[srcFolderId] ?? 1) - 1 + + let folderEntities = getAllFolderEntity() + let destFolder = folderEntities[destination.section] + guard let destFolderId = destFolder.id else { return } + let destinationOrder = Int64(destination.row) + let destBookmarkEntities = getAllBookmarkEntity(in: destFolderId).filter{ $0.order >= deletedOrder } + for bookmarkEntity in destBookmarkEntities { + bookmarkEntity.order += 1 + } + lastBookmarkOrder[destFolderId] = (lastBookmarkOrder[destFolderId] ?? 0) + 1 + + + srcBookmarkEntity.folder = destFolder + srcBookmarkEntity.order = destinationOrder + } + save() + } + +} + diff --git a/iBox/Sources/Utils/UserDefaultsManager.swift b/iBox/Sources/Utils/UserDefaultsManager.swift index 8baef12..a4450bb 100644 --- a/iBox/Sources/Utils/UserDefaultsManager.swift +++ b/iBox/Sources/Utils/UserDefaultsManager.swift @@ -8,9 +8,10 @@ import Foundation enum UserDefaultsAccessKey: String { - case theme // 다크 모드 - case favorite // 즐겨찾기 - case homeTab // 첫 화면 + case theme // 다크 모드 + case favorite // 즐겨찾기 + case homeTab // 첫 화면 + case isDefaultDataInserted // 기본 데이터 삽입 여부 } final class UserDefaultsManager { @@ -20,12 +21,16 @@ final class UserDefaultsManager { ) static let favorite = UserDefaultValue( key: .favorite, - defaultValue: Bookmark(name: "42 Intra", url: "https://profile.intra.42.fr/") + defaultValue: Bookmark(id: UUID(), name: "42 Intra", url: URL(string: "https://profile.intra.42.fr/")!) ) static let homeTabIndex = UserDefaultValue( key: .homeTab, defaultValue: 0 ) + static let isDefaultDataInserted = UserDefaultValue( + key: .isDefaultDataInserted, + defaultValue: false + ) } class UserDefaultValue { diff --git a/iBox/Sources/ViewModel/BoxListCellViewModel.swift b/iBox/Sources/ViewModel/BoxListCellViewModel.swift index 68d89aa..b8a6cdd 100644 --- a/iBox/Sources/ViewModel/BoxListCellViewModel.swift +++ b/iBox/Sources/ViewModel/BoxListCellViewModel.swift @@ -12,16 +12,16 @@ class BoxListCellViewModel: Identifiable { init(bookmark: Bookmark) { self.bookmark = bookmark + self.name = bookmark.name + self.url = bookmark.url } - let id = UUID() - - var name: String { - bookmark.name + var id: UUID { + bookmark.id } + + var name: String - var url: String { - bookmark.url - } + var url: URL } diff --git a/iBox/Sources/ViewModel/BoxListSectionViewModel.swift b/iBox/Sources/ViewModel/BoxListSectionViewModel.swift index 70a928c..e106344 100644 --- a/iBox/Sources/ViewModel/BoxListSectionViewModel.swift +++ b/iBox/Sources/ViewModel/BoxListSectionViewModel.swift @@ -20,7 +20,9 @@ class BoxListSectionViewModel: Identifiable { return isOpened ? originalBoxListCellViewModels : [] } - let id = UUID() + var id: UUID { + folder.id + } var name: String { folder.name diff --git a/iBox/Sources/ViewModel/BoxListViewModel.swift b/iBox/Sources/ViewModel/BoxListViewModel.swift index cb73926..ec1ba32 100644 --- a/iBox/Sources/ViewModel/BoxListViewModel.swift +++ b/iBox/Sources/ViewModel/BoxListViewModel.swift @@ -10,16 +10,7 @@ import Foundation class BoxListViewModel { - var boxList = [ - BoxListSectionViewModel(folder: Folder(name: "기본 폴더", color: .gray, bookmarks: [ - Bookmark(name: "42 Intra", url: "https://profile.intra.42.fr/"), - Bookmark(name: "42Where", url: "https://www.where42.kr/"), - Bookmark(name: "42Stat", url: "https://stat.42seoul.kr/"), - Bookmark(name: "집현전", url: "https://42library.kr/") - ])), - BoxListSectionViewModel(folder: Folder(name: "새 폴더", color: .green, bookmarks: [Bookmark(name: "Cabi", url: "https://cabi.42seoul.io/")], isOpened: false)), - BoxListSectionViewModel(folder: Folder(name: "새 폴더(2)", color: .yellow, bookmarks: [Bookmark(name: "24HANE", url: "https://24hoursarenotenough.42seoul.kr/")], isOpened: false)) - ] + var boxList = [BoxListSectionViewModel]() enum Input { case viewDidLoad @@ -39,6 +30,8 @@ class BoxListViewModel { guard let self else { return } switch event { case .viewDidLoad: + let folders = CoreDataManager.shared.getFolders() + self.boxList = folders.map{ BoxListSectionViewModel(folder: $0) } output.send(.sendBoxList(boxList: boxList)) case let .folderTapped(section): boxList[section].isOpened.toggle()