188 lines
6.4 KiB
Swift
188 lines
6.4 KiB
Swift
import Foundation
|
|
import SQLite3
|
|
|
|
// SQLITE_TRANSIENT tells SQLite to make its own copy of bound text data
|
|
private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
|
|
|
|
final class DatabaseManager {
|
|
static let shared = DatabaseManager()
|
|
|
|
private var db: OpaquePointer?
|
|
|
|
private init() {
|
|
openDatabase()
|
|
createTable()
|
|
}
|
|
|
|
deinit {
|
|
if db != nil {
|
|
sqlite3_close(db)
|
|
}
|
|
}
|
|
|
|
// MARK: - Database Setup
|
|
|
|
private func openDatabase() {
|
|
let fileURL = getDatabasePath()
|
|
if sqlite3_open(fileURL.path, &db) != SQLITE_OK {
|
|
print("Error opening database: \(String(cString: sqlite3_errmsg(db)))")
|
|
db = nil
|
|
}
|
|
}
|
|
|
|
private func getDatabasePath() -> URL {
|
|
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
|
|
return paths[0].appendingPathComponent("todos.db")
|
|
}
|
|
|
|
private func createTable() {
|
|
let sql = """
|
|
CREATE TABLE IF NOT EXISTS todos (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
completed INTEGER DEFAULT 0,
|
|
category TEXT DEFAULT 'Personal',
|
|
priority TEXT DEFAULT 'medium',
|
|
created_at INTEGER DEFAULT (strftime('%s','now'))
|
|
)
|
|
"""
|
|
var statement: OpaquePointer?
|
|
defer { sqlite3_finalize(statement) }
|
|
|
|
if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK {
|
|
if sqlite3_step(statement) != SQLITE_DONE {
|
|
print("Error creating table: \(String(cString: sqlite3_errmsg(db)))")
|
|
}
|
|
} else {
|
|
print("Error preparing create table: \(String(cString: sqlite3_errmsg(db)))")
|
|
}
|
|
}
|
|
|
|
// MARK: - Helper
|
|
|
|
private func bindText(_ statement: OpaquePointer?, index: Int32, value: String) {
|
|
sqlite3_bind_text(statement, index, value, -1, SQLITE_TRANSIENT)
|
|
}
|
|
|
|
// MARK: - CRUD Operations
|
|
|
|
func loadTodos() -> [Todo] {
|
|
let sql = "SELECT id, title, completed, category, priority, created_at FROM todos ORDER BY created_at DESC"
|
|
var statement: OpaquePointer?
|
|
defer { sqlite3_finalize(statement) }
|
|
var todos: [Todo] = []
|
|
|
|
if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK {
|
|
while sqlite3_step(statement) == SQLITE_ROW {
|
|
let id = sqlite3_column_int64(statement, 0)
|
|
let title = String(cString: sqlite3_column_text(statement, 1))
|
|
let completed = sqlite3_column_int(statement, 2) == 1
|
|
let category = String(cString: sqlite3_column_text(statement, 3))
|
|
let priorityStr = String(cString: sqlite3_column_text(statement, 4))
|
|
let priority = Priority(rawValue: priorityStr) ?? .medium
|
|
let createdAt = sqlite3_column_int64(statement, 5)
|
|
|
|
let todo = Todo(
|
|
id: id,
|
|
title: title,
|
|
completed: completed,
|
|
category: category,
|
|
priority: priority,
|
|
createdAt: createdAt
|
|
)
|
|
todos.append(todo)
|
|
}
|
|
} else {
|
|
print("Error loading todos: \(String(cString: sqlite3_errmsg(db)))")
|
|
}
|
|
return todos
|
|
}
|
|
|
|
@discardableResult
|
|
func insertTodo(title: String, category: String, priority: Priority) -> Todo? {
|
|
let sql = "INSERT INTO todos (title, category, priority, completed) VALUES (?, ?, ?, 0)"
|
|
var statement: OpaquePointer?
|
|
defer { sqlite3_finalize(statement) }
|
|
|
|
guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else {
|
|
print("Error preparing insert: \(String(cString: sqlite3_errmsg(db)))")
|
|
return nil
|
|
}
|
|
|
|
bindText(statement, index: 1, value: title)
|
|
bindText(statement, index: 2, value: category)
|
|
bindText(statement, index: 3, value: priority.rawValue)
|
|
|
|
guard sqlite3_step(statement) == SQLITE_DONE else {
|
|
print("Error inserting todo: \(String(cString: sqlite3_errmsg(db)))")
|
|
return nil
|
|
}
|
|
|
|
let id = sqlite3_last_insert_rowid(db)
|
|
let createdAt = Int64(Date().timeIntervalSince1970)
|
|
return Todo(
|
|
id: id,
|
|
title: title,
|
|
completed: false,
|
|
category: category,
|
|
priority: priority,
|
|
createdAt: createdAt
|
|
)
|
|
}
|
|
|
|
func updateTodo(id: Int64, title: String, category: String, priority: Priority) {
|
|
let sql = "UPDATE todos SET title = ?, category = ?, priority = ? WHERE id = ?"
|
|
var statement: OpaquePointer?
|
|
defer { sqlite3_finalize(statement) }
|
|
|
|
guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else {
|
|
print("Error preparing update: \(String(cString: sqlite3_errmsg(db)))")
|
|
return
|
|
}
|
|
|
|
bindText(statement, index: 1, value: title)
|
|
bindText(statement, index: 2, value: category)
|
|
bindText(statement, index: 3, value: priority.rawValue)
|
|
sqlite3_bind_int64(statement, 4, id)
|
|
|
|
if sqlite3_step(statement) != SQLITE_DONE {
|
|
print("Error updating todo: \(String(cString: sqlite3_errmsg(db)))")
|
|
}
|
|
}
|
|
|
|
func toggleTodo(id: Int64, completed: Bool) {
|
|
let sql = "UPDATE todos SET completed = ? WHERE id = ?"
|
|
var statement: OpaquePointer?
|
|
defer { sqlite3_finalize(statement) }
|
|
|
|
guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else {
|
|
print("Error preparing toggle: \(String(cString: sqlite3_errmsg(db)))")
|
|
return
|
|
}
|
|
|
|
sqlite3_bind_int(statement, 1, completed ? 1 : 0)
|
|
sqlite3_bind_int64(statement, 2, id)
|
|
|
|
if sqlite3_step(statement) != SQLITE_DONE {
|
|
print("Error toggling todo: \(String(cString: sqlite3_errmsg(db)))")
|
|
}
|
|
}
|
|
|
|
func deleteTodo(id: Int64) {
|
|
let sql = "DELETE FROM todos WHERE id = ?"
|
|
var statement: OpaquePointer?
|
|
defer { sqlite3_finalize(statement) }
|
|
|
|
guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else {
|
|
print("Error preparing delete: \(String(cString: sqlite3_errmsg(db)))")
|
|
return
|
|
}
|
|
|
|
sqlite3_bind_int64(statement, 1, id)
|
|
|
|
if sqlite3_step(statement) != SQLITE_DONE {
|
|
print("Error deleting todo: \(String(cString: sqlite3_errmsg(db)))")
|
|
}
|
|
}
|
|
}
|