diff --git a/Luto.xcodeproj/project.pbxproj b/Luto.xcodeproj/project.pbxproj index 5ed34fe..5e5acd7 100644 --- a/Luto.xcodeproj/project.pbxproj +++ b/Luto.xcodeproj/project.pbxproj @@ -17,6 +17,9 @@ 682D06C22A5487D600EA4745 /* LutoUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682D06C12A5487D600EA4745 /* LutoUITests.swift */; }; 682D06C42A5487D600EA4745 /* LutoUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682D06C32A5487D600EA4745 /* LutoUITestsLaunchTests.swift */; }; 6838051A2A57356700CEF29C /* TextFieldExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683805192A57356700CEF29C /* TextFieldExtensions.swift */; }; + 684640A72A65569D00D5A369 /* TaskDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684640A62A65569D00D5A369 /* TaskDataStore.swift */; }; + 684640AA2A6566B300D5A369 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = 684640A92A6566B300D5A369 /* SQLite */; }; + 68A773762A66F9A1000887A3 /* TaskViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A773752A66F9A1000887A3 /* TaskViewModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -51,6 +54,8 @@ 682D06C12A5487D600EA4745 /* LutoUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LutoUITests.swift; sourceTree = ""; }; 682D06C32A5487D600EA4745 /* LutoUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LutoUITestsLaunchTests.swift; sourceTree = ""; }; 683805192A57356700CEF29C /* TextFieldExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldExtensions.swift; sourceTree = ""; }; + 684640A62A65569D00D5A369 /* TaskDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskDataStore.swift; sourceTree = ""; }; + 68A773752A66F9A1000887A3 /* TaskViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskViewModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -58,6 +63,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 684640AA2A6566B300D5A369 /* SQLite in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -118,6 +124,8 @@ 682D06A42A5487D200EA4745 /* Luto */ = { isa = PBXGroup; children = ( + 68A773742A66F984000887A3 /* ViewModels */, + 684640A52A65568A00D5A369 /* Store */, 680A62162A600F75004C21A4 /* Models */, 683805182A57354900CEF29C /* UI */, 682D06A52A5487D200EA4745 /* LutoApp.swift */, @@ -162,6 +170,22 @@ path = UI; sourceTree = ""; }; + 684640A52A65568A00D5A369 /* Store */ = { + isa = PBXGroup; + children = ( + 684640A62A65569D00D5A369 /* TaskDataStore.swift */, + ); + path = Store; + sourceTree = ""; + }; + 68A773742A66F984000887A3 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 68A773752A66F9A1000887A3 /* TaskViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -178,6 +202,9 @@ dependencies = ( ); name = Luto; + packageProductDependencies = ( + 684640A92A6566B300D5A369 /* SQLite */, + ); productName = Luto; productReference = 682D06A22A5487D200EA4745 /* Luto.app */; productType = "com.apple.product-type.application"; @@ -250,6 +277,9 @@ Base, ); mainGroup = 682D06992A5487D200EA4745; + packageReferences = ( + 684640A82A6566B300D5A369 /* XCRemoteSwiftPackageReference "SQLite.swift" */, + ); productRefGroup = 682D06A32A5487D200EA4745 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -292,6 +322,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 684640A72A65569D00D5A369 /* TaskDataStore.swift in Sources */, + 68A773762A66F9A1000887A3 /* TaskViewModel.swift in Sources */, 682D06A82A5487D200EA4745 /* MainView.swift in Sources */, 6838051A2A57356700CEF29C /* TextFieldExtensions.swift in Sources */, 680A62182A600F81004C21A4 /* Task.swift in Sources */, @@ -383,7 +415,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 13.3; + MACOSX_DEPLOYMENT_TARGET = 13.4; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -437,7 +469,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 13.3; + MACOSX_DEPLOYMENT_TARGET = 13.4; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; @@ -460,6 +492,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -487,6 +520,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -612,6 +646,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 684640A82A6566B300D5A369 /* XCRemoteSwiftPackageReference "SQLite.swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/stephencelis/SQLite.swift.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.14.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 684640A92A6566B300D5A369 /* SQLite */ = { + isa = XCSwiftPackageProductDependency; + package = 684640A82A6566B300D5A369 /* XCRemoteSwiftPackageReference "SQLite.swift" */; + productName = SQLite; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 682D069A2A5487D200EA4745 /* Project object */; } diff --git a/Luto.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Luto.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..cb4b757 --- /dev/null +++ b/Luto.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "sqlite.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stephencelis/SQLite.swift.git", + "state" : { + "revision" : "7a2e3cd27de56f6d396e84f63beefd0267b55ccb", + "version" : "0.14.1" + } + } + ], + "version" : 2 +} diff --git a/Luto.xcodeproj/xcuserdata/pboulch.xcuserdatad/xcschemes/xcschememanagement.plist b/Luto.xcodeproj/xcuserdata/pboulch.xcuserdatad/xcschemes/xcschememanagement.plist index 95b80a5..d20410a 100644 --- a/Luto.xcodeproj/xcuserdata/pboulch.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Luto.xcodeproj/xcuserdata/pboulch.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,6 +9,27 @@ orderHint 0 + SQLite (Playground) 1.xcscheme + + isShown + + orderHint + 2 + + SQLite (Playground) 2.xcscheme + + isShown + + orderHint + 3 + + SQLite (Playground).xcscheme + + isShown + + orderHint + 0 + diff --git a/Luto/Assets.xcassets/arrow-down.imageset/Contents.json b/Luto/Assets.xcassets/arrow-down.imageset/Contents.json new file mode 100644 index 0000000..72fdab2 --- /dev/null +++ b/Luto/Assets.xcassets/arrow-down.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "arrow-down.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Luto/Assets.xcassets/arrow-down.imageset/arrow-down.svg b/Luto/Assets.xcassets/arrow-down.imageset/arrow-down.svg new file mode 100644 index 0000000..5438ebe --- /dev/null +++ b/Luto/Assets.xcassets/arrow-down.imageset/arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/Luto/LutoApp.swift b/Luto/LutoApp.swift index 5073df9..42f8613 100644 --- a/Luto/LutoApp.swift +++ b/Luto/LutoApp.swift @@ -14,7 +14,7 @@ struct LutoApp: App { var body: some Scene { WindowGroup(id: "MainWindow") { - MainView() + MainView(viewModel: TaskViewModel()) } } } @@ -40,8 +40,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { self.popover = NSPopover() self.popover.contentSize = NSSize(width: 500, height: 500) self.popover.behavior = .transient - self.popover.contentViewController = NSHostingController(rootView: MainView() -) + self.popover.contentViewController = NSHostingController(rootView: MainView(viewModel: TaskViewModel())) } @objc func togglePopover() { diff --git a/Luto/Models/Task.swift b/Luto/Models/Task.swift index 63ea18b..ceaee63 100644 --- a/Luto/Models/Task.swift +++ b/Luto/Models/Task.swift @@ -7,7 +7,20 @@ import Foundation -struct Task { +class Task { + let id: Int64 var title: String - var description: String + var body: String + + init(title: String, body: String) { + self.id = 0 + self.title = title + self.body = body + } + + init(id: Int64, title: String, body: String) { + self.id = id + self.title = title + self.body = body + } } diff --git a/Luto/Store/TaskDataStore.swift b/Luto/Store/TaskDataStore.swift new file mode 100644 index 0000000..f586ac7 --- /dev/null +++ b/Luto/Store/TaskDataStore.swift @@ -0,0 +1,138 @@ +// +// TaskDataStore.swift +// Luto +// +// Created by Pierre Boulc'h on 17/07/2023. +// + +import Foundation +import SQLite + +class TaskDataStore { + + static let DIR_TASK_DB = "TaskDB" + static let STORE_NAME = "task.sqlite3" + + private let tasks = Table("tasks") + + private let id = Expression("id") + private let title = Expression("title") + private let body = Expression("body") + + static let shared = TaskDataStore() + + private var db: Connection? = nil + + private init() { + if let docDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + let dirPath = docDir.appendingPathComponent(Self.DIR_TASK_DB) + + do { + try FileManager.default.createDirectory(atPath: dirPath.path, withIntermediateDirectories: true, attributes: nil) + let dbPath = dirPath.appendingPathComponent(Self.STORE_NAME).path + db = try Connection(dbPath) + createTable() + print("SQLiteDataStore init successfully at: \(dbPath) ") + } catch { + db = nil + print("SQLiteDataStore init error: \(error)") + } + } else { + db = nil + } + } + + private func createTable() { + guard let database = db else { + return + } + do { + try database.run(tasks.create { table in + table.column(id, primaryKey: .autoincrement) + table.column(title) + table.column(body) + }) + print("Table Created...") + } catch { + print(error) + } + } + + func insert(name: String, description: String) -> Int64? { + guard let database = db else { return nil } + + let insert = tasks.insert(self.title <- name, + self.body <- description) + + do { + let rowID = try database.run(insert) + return rowID + } catch { + print(error) + return nil + } + } + + func getAllTasks() -> [Task] { + var tasks: [Task] = [] + guard let database = db else { return [] } + + do { + for task in try database.prepare(self.tasks) { + tasks.append(Task(id: task[id], title: task[title], body: task[body])) + } + } catch { + print(error) + } + return tasks + } + + + func findTask(taskId: Int64) -> Task? { + var task: Task = Task(id: taskId, title: "", body: "") + guard let database = db else { return nil } + + let filter = self.tasks.filter(id == taskId) + do { + for t in try database.prepare(filter) { + task.title = t[title] + task.body = t[body] + } + } catch { + print(error) + } + return task + } + + func update(id: Int64, name: String, date: Date = Date(), status: Bool = false) -> Bool { + guard let database = db else { return false } + + let task = tasks.filter(self.id == id) + do { + let update = task.update([ + title <- title, + self.body <- body, + ]) + if try database.run(update) > 0 { + return true + } + } catch { + print(error) + } + return false + } + + func delete(id: Int64) -> Bool { + guard let database = db else { + return false + } + do { + let filter = tasks.filter(self.id == id) + try database.run(filter.delete()) + return true + } catch { + print(error) + return false + } + } +} diff --git a/Luto/UI/MainView.swift b/Luto/UI/MainView.swift index 165444d..921364e 100644 --- a/Luto/UI/MainView.swift +++ b/Luto/UI/MainView.swift @@ -15,9 +15,10 @@ struct MainView: View { case descriptionField } + @ObservedObject var viewModel: TaskViewModel + @State var taskTitle = "" @State var taskDescription = "" - @State var listTask: [Task] = [] @State var showAdditionnalFields = false @@ -28,12 +29,12 @@ struct MainView: View { Text("Mes tâches") ScrollView { LazyVStack(alignment: .leading) { - ForEach(Array(listTask.enumerated()), id: \.offset) { index, task in + ForEach(Array(viewModel.allTask.enumerated()), id: \.offset) { index, task in HStack { VStack(alignment: .leading) { Text("\(task.title)").font(.system(size: 16)) - if !task.description.isEmpty { - Text("\(task.description)") + if !task.body.isEmpty { + Text("\(task.body)") } } .padding(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 8)) @@ -60,6 +61,8 @@ struct MainView: View { } }.onSubmit { addTask() + }.onAppear { + focusState = .titleField } Button(action: { addTask() @@ -74,6 +77,13 @@ struct MainView: View { TextField("Description ...", text: $taskDescription).focused($focusState, equals: .descriptionField).textFieldStyle(OvalTextFieldStyle()).padding(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0)).onSubmit { addTask() } + } else { + Button(action: { + showAdditionnalFields = true + }, label: { + Image("arrow-down").tint(Color.white) + }).buttonStyle(PlainButtonStyle()) + } } .padding() @@ -83,7 +93,7 @@ struct MainView: View { func addTask() { if !taskTitle.isEmpty { withAnimation { - listTask.append(Task(title: taskTitle, description: taskDescription)) + viewModel.addTask(name: taskTitle, body: taskDescription) } taskTitle = "" taskDescription = "" @@ -93,14 +103,8 @@ struct MainView: View { } func removeTask(index: Int) { - _ = withAnimation { - listTask.remove(at: index) + withAnimation { + viewModel.deleteTask(at: IndexSet([index])) } } } - -struct MainView_Previews: PreviewProvider { - static var previews: some View { - MainView() - } -} diff --git a/Luto/ViewModels/TaskViewModel.swift b/Luto/ViewModels/TaskViewModel.swift new file mode 100644 index 0000000..7bd34ba --- /dev/null +++ b/Luto/ViewModels/TaskViewModel.swift @@ -0,0 +1,39 @@ +// +// TaskViewModel.swift +// Luto +// +// Created by Pierre Boulc'h on 18/07/2023. +// + +import Foundation + +class TaskViewModel: ObservableObject { + + @Published var allTask: [Task] = [] + + init() { + getTaskList() + } + + func addTask(name: String, body: String) { + let id = TaskDataStore.shared.insert(name: name, description: body) + if id != 0 { + getTaskList() + } + } + + func getTaskList() { + allTask = TaskDataStore.shared.getAllTasks() + } + + func deleteTask(at indexSet: IndexSet) { + let id = indexSet.map { self.allTask[$0].id }.first + if let id = id { + let delete = TaskDataStore.shared.delete(id: id) + if delete { + getTaskList() + } + } + } + +}