Add SQLite to save tasks

This commit is contained in:
2023-07-19 13:01:33 +02:00
parent 0ff3803059
commit 3cdad5a4d7
10 changed files with 319 additions and 20 deletions

View File

@ -17,6 +17,9 @@
682D06C22A5487D600EA4745 /* LutoUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682D06C12A5487D600EA4745 /* LutoUITests.swift */; }; 682D06C22A5487D600EA4745 /* LutoUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682D06C12A5487D600EA4745 /* LutoUITests.swift */; };
682D06C42A5487D600EA4745 /* LutoUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682D06C32A5487D600EA4745 /* LutoUITestsLaunchTests.swift */; }; 682D06C42A5487D600EA4745 /* LutoUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682D06C32A5487D600EA4745 /* LutoUITestsLaunchTests.swift */; };
6838051A2A57356700CEF29C /* TextFieldExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683805192A57356700CEF29C /* TextFieldExtensions.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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -51,6 +54,8 @@
682D06C12A5487D600EA4745 /* LutoUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LutoUITests.swift; sourceTree = "<group>"; }; 682D06C12A5487D600EA4745 /* LutoUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LutoUITests.swift; sourceTree = "<group>"; };
682D06C32A5487D600EA4745 /* LutoUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LutoUITestsLaunchTests.swift; sourceTree = "<group>"; }; 682D06C32A5487D600EA4745 /* LutoUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LutoUITestsLaunchTests.swift; sourceTree = "<group>"; };
683805192A57356700CEF29C /* TextFieldExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldExtensions.swift; sourceTree = "<group>"; }; 683805192A57356700CEF29C /* TextFieldExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldExtensions.swift; sourceTree = "<group>"; };
684640A62A65569D00D5A369 /* TaskDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskDataStore.swift; sourceTree = "<group>"; };
68A773752A66F9A1000887A3 /* TaskViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskViewModel.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -58,6 +63,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
684640AA2A6566B300D5A369 /* SQLite in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -118,6 +124,8 @@
682D06A42A5487D200EA4745 /* Luto */ = { 682D06A42A5487D200EA4745 /* Luto */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
68A773742A66F984000887A3 /* ViewModels */,
684640A52A65568A00D5A369 /* Store */,
680A62162A600F75004C21A4 /* Models */, 680A62162A600F75004C21A4 /* Models */,
683805182A57354900CEF29C /* UI */, 683805182A57354900CEF29C /* UI */,
682D06A52A5487D200EA4745 /* LutoApp.swift */, 682D06A52A5487D200EA4745 /* LutoApp.swift */,
@ -162,6 +170,22 @@
path = UI; path = UI;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
684640A52A65568A00D5A369 /* Store */ = {
isa = PBXGroup;
children = (
684640A62A65569D00D5A369 /* TaskDataStore.swift */,
);
path = Store;
sourceTree = "<group>";
};
68A773742A66F984000887A3 /* ViewModels */ = {
isa = PBXGroup;
children = (
68A773752A66F9A1000887A3 /* TaskViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -178,6 +202,9 @@
dependencies = ( dependencies = (
); );
name = Luto; name = Luto;
packageProductDependencies = (
684640A92A6566B300D5A369 /* SQLite */,
);
productName = Luto; productName = Luto;
productReference = 682D06A22A5487D200EA4745 /* Luto.app */; productReference = 682D06A22A5487D200EA4745 /* Luto.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
@ -250,6 +277,9 @@
Base, Base,
); );
mainGroup = 682D06992A5487D200EA4745; mainGroup = 682D06992A5487D200EA4745;
packageReferences = (
684640A82A6566B300D5A369 /* XCRemoteSwiftPackageReference "SQLite.swift" */,
);
productRefGroup = 682D06A32A5487D200EA4745 /* Products */; productRefGroup = 682D06A32A5487D200EA4745 /* Products */;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
@ -292,6 +322,8 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
684640A72A65569D00D5A369 /* TaskDataStore.swift in Sources */,
68A773762A66F9A1000887A3 /* TaskViewModel.swift in Sources */,
682D06A82A5487D200EA4745 /* MainView.swift in Sources */, 682D06A82A5487D200EA4745 /* MainView.swift in Sources */,
6838051A2A57356700CEF29C /* TextFieldExtensions.swift in Sources */, 6838051A2A57356700CEF29C /* TextFieldExtensions.swift in Sources */,
680A62182A600F81004C21A4 /* Task.swift in Sources */, 680A62182A600F81004C21A4 /* Task.swift in Sources */,
@ -383,7 +415,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 13.3; MACOSX_DEPLOYMENT_TARGET = 13.4;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
@ -437,7 +469,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 13.3; MACOSX_DEPLOYMENT_TARGET = 13.4;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
SDKROOT = macosx; SDKROOT = macosx;
@ -460,6 +492,7 @@
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -487,6 +520,7 @@
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -612,6 +646,25 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* 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 */; rootObject = 682D069A2A5487D200EA4745 /* Project object */;
} }

View File

@ -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
}

View File

@ -9,6 +9,27 @@
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>0</integer>
</dict> </dict>
<key>SQLite (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>SQLite (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>3</integer>
</dict>
<key>SQLite (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict> </dict>
</dict> </dict>
</plist> </plist>

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "arrow-down.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 4.7725C0 4.5825 0.08 4.3825 0.23 4.2325C0.53 3.9225 1.02 3.9225 1.32 4.2325L8 10.9125L14.68 4.2325C14.98 3.9325 15.47 3.9325 15.77 4.2325C16.07 4.5325 16.07 5.0225 15.77 5.3225L8.54 12.5525C8.24 12.8525 7.75 12.8525 7.45 12.5525L0.23 5.3225C0.08 5.1725 0 4.9725 0 4.7725Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 403 B

View File

@ -14,7 +14,7 @@ struct LutoApp: App {
var body: some Scene { var body: some Scene {
WindowGroup(id: "MainWindow") { WindowGroup(id: "MainWindow") {
MainView() MainView(viewModel: TaskViewModel())
} }
} }
} }
@ -40,8 +40,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
self.popover = NSPopover() self.popover = NSPopover()
self.popover.contentSize = NSSize(width: 500, height: 500) self.popover.contentSize = NSSize(width: 500, height: 500)
self.popover.behavior = .transient self.popover.behavior = .transient
self.popover.contentViewController = NSHostingController(rootView: MainView() self.popover.contentViewController = NSHostingController(rootView: MainView(viewModel: TaskViewModel()))
)
} }
@objc func togglePopover() { @objc func togglePopover() {

View File

@ -7,7 +7,20 @@
import Foundation import Foundation
struct Task { class Task {
let id: Int64
var title: String 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
}
} }

View File

@ -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<Int64>("id")
private let title = Expression<String>("title")
private let body = Expression<String>("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
}
}
}

View File

@ -15,9 +15,10 @@ struct MainView: View {
case descriptionField case descriptionField
} }
@ObservedObject var viewModel: TaskViewModel
@State var taskTitle = "" @State var taskTitle = ""
@State var taskDescription = "" @State var taskDescription = ""
@State var listTask: [Task] = []
@State var showAdditionnalFields = false @State var showAdditionnalFields = false
@ -28,12 +29,12 @@ struct MainView: View {
Text("Mes tâches") Text("Mes tâches")
ScrollView { ScrollView {
LazyVStack(alignment: .leading) { LazyVStack(alignment: .leading) {
ForEach(Array(listTask.enumerated()), id: \.offset) { index, task in ForEach(Array(viewModel.allTask.enumerated()), id: \.offset) { index, task in
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("\(task.title)").font(.system(size: 16)) Text("\(task.title)").font(.system(size: 16))
if !task.description.isEmpty { if !task.body.isEmpty {
Text("\(task.description)") Text("\(task.body)")
} }
} }
.padding(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 8)) .padding(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 8))
@ -60,6 +61,8 @@ struct MainView: View {
} }
}.onSubmit { }.onSubmit {
addTask() addTask()
}.onAppear {
focusState = .titleField
} }
Button(action: { Button(action: {
addTask() 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 { TextField("Description ...", text: $taskDescription).focused($focusState, equals: .descriptionField).textFieldStyle(OvalTextFieldStyle()).padding(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0)).onSubmit {
addTask() addTask()
} }
} else {
Button(action: {
showAdditionnalFields = true
}, label: {
Image("arrow-down").tint(Color.white)
}).buttonStyle(PlainButtonStyle())
} }
} }
.padding() .padding()
@ -83,7 +93,7 @@ struct MainView: View {
func addTask() { func addTask() {
if !taskTitle.isEmpty { if !taskTitle.isEmpty {
withAnimation { withAnimation {
listTask.append(Task(title: taskTitle, description: taskDescription)) viewModel.addTask(name: taskTitle, body: taskDescription)
} }
taskTitle = "" taskTitle = ""
taskDescription = "" taskDescription = ""
@ -93,14 +103,8 @@ struct MainView: View {
} }
func removeTask(index: Int) { func removeTask(index: Int) {
_ = withAnimation { withAnimation {
listTask.remove(at: index) viewModel.deleteTask(at: IndexSet([index]))
} }
} }
} }
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView()
}
}

View File

@ -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()
}
}
}
}