Jelajahi Sumber

refactor: relocated a bunch of things and push some mutator functions into Project/Task

Sam Jaffe 2 minggu lalu
induk
melakukan
94ba61cabe

+ 16 - 0
Todos/Model/Aggregate.swift

@@ -0,0 +1,16 @@
+//
+//  Aggregate.swift
+//  Todos
+//
+//  Created by Sam Jaffe on 3/5/26.
+//
+
+import Foundation
+
+protocol Aggregate {
+  associatedtype Element
+
+  func move(fromOffsets: IndexSet, toOffset: Int)
+  func remove(_ item: Element)
+  func reindex()
+}

+ 21 - 2
Todos/Model/Project.swift

@@ -7,13 +7,16 @@
 
 import Foundation
 import SwiftData
+import SwiftUI
 
 @Model
-final class Project: Codable, Ordered {
+final class Project: Codable, Ordered, Aggregate {
+  typealias Element = Task
+
   var sortOrder: Int = 0
   var timestamp: Date
-  var category: String = ""
   var name: String = "New Project"
+  var category: String = ""
   @Relationship(deleteRule: .cascade, inverse: \Task.project)
   var tasks: [Task] = []
 
@@ -21,6 +24,22 @@ final class Project: Codable, Ordered {
     self.timestamp = timestamp
   }
 
+  func move(fromOffsets: IndexSet, toOffset: Int) {
+    tasks.move(fromOffsets: fromOffsets, toOffset: toOffset)
+    reindex()
+  }
+
+  func remove(_ item: Element) {
+    tasks.removeAll(where: { $0.id == item.id })
+    reindex()
+  }
+
+  func reindex() {
+    for (index, item) in tasks.enumerated() {
+      item.sortOrder = index
+    }
+  }
+
   func yaml(_ indent: Int = 0) -> String {
     let hdr = String(repeating: "  ", count: indent)
     var rval = hdr + "\(name):\n"

+ 34 - 0
Todos/Model/Status.swift

@@ -0,0 +1,34 @@
+//
+//  Status.swift
+//  Todos
+//
+//  Created by Sam Jaffe on 3/5/26.
+//
+
+import Foundation
+
+enum Status: String, CaseIterable, Identifiable, Codable {
+  case Todo = " "
+  case Complete = "V"
+  case InProgress = "C"
+  case Hiatus = "H"
+  case Waiting = "R"
+
+  var id: Self { self }
+
+  var description: String { self.rawValue }
+
+  var isStrong: Bool {
+    self == .Complete || self == .Hiatus || self == .Waiting
+  }
+
+  var label: String {
+    switch self {
+    case .Todo: return "square.and.pencil"
+    case .Complete: return "checkmark"
+    case .InProgress: return "ellipsis.circle"
+    case .Hiatus: return "clock.badge.questionmark"
+    case .Waiting: return "airplane.circle"
+    }
+  }
+}

+ 50 - 0
Todos/Model/SubTask.swift

@@ -0,0 +1,50 @@
+//
+//  SubTask.swift
+//  Todos
+//
+//  Created by Sam Jaffe on 3/5/26.
+//
+
+import Foundation
+import SwiftData
+
+@Model
+final class SubTask: Codable, Ordered {
+  var sortOrder: Int = 0
+  var name: String
+  var task: Task?
+  var notes: String = ""
+  var status: Status = Status.Todo
+
+  init(name: String, parent: Task? = nil) {
+    self.name = name
+    self.task = parent
+    self.sortOrder = parent?.subtasks.count ?? 0
+  }
+
+  func yaml(_ indent: Int = 0) -> String {
+    let hdr = String(repeating: "  ", count: indent)
+    let subhdr = hdr + "  # "
+    var rval = hdr + "- [\(status.rawValue)] \(name)\n"
+    if !notes.isEmpty {
+      rval += subhdr + notes.replacingOccurrences(of: "\n", with: "\n" + subhdr) + "\n"
+    }
+    return rval
+  }
+
+  enum CodingKeys: CodingKey { case name, notes, status }
+
+  required init(from decoder: any Decoder) throws {
+    let container = try decoder.container(keyedBy: CodingKeys.self)
+    name = try container.decode(String.self, forKey: .name)
+    notes = try container.decode(String.self, forKey: .notes)
+    status = try container.decode(Status.self, forKey: .status)
+  }
+
+  func encode(to encoder: any Encoder) throws {
+    var container = encoder.container(keyedBy: CodingKeys.self)
+    try container.encode(name, forKey: .name)
+    try container.encode(notes, forKey: .notes)
+    try container.encode(status, forKey: .status)
+  }
+}

+ 33 - 0
Todos/Model/Tag.swift

@@ -0,0 +1,33 @@
+//
+//  Tag.swift
+//  Todos
+//
+//  Created by Sam Jaffe on 3/5/26.
+//
+
+import Foundation
+import SwiftData
+
+@Model
+final class Tag: Codable {
+  var id: String
+  var task: Task?
+
+  init(id: String, parent: Task? = nil) {
+    self.id = id
+    self.task = parent
+  }
+
+  func like(_ str: String) -> Bool {
+    return id.caseInsensitiveCompare(str) == .orderedSame
+  }
+
+  required init(from decoder: any Decoder) throws {
+    id = try decoder.singleValueContainer().decode(String.self)
+  }
+
+  func encode(to encoder: any Encoder) throws {
+    var single = encoder.singleValueContainer()
+    try single.encode(id)
+  }
+}

+ 19 - 88
Todos/Model/Task.swift

@@ -7,97 +7,12 @@
 
 import Foundation
 import SwiftData
-
-enum Status: String, CaseIterable, Identifiable, Codable {
-  case Todo = " "
-  case Complete = "V"
-  case InProgress = "C"
-  case Hiatus = "H"
-  case Waiting = "R"
-
-  var id: Self { self }
-  var description: String { self.rawValue }
-  var isStrong: Bool {
-    self == .Complete || self == .Hiatus || self == .Waiting
-  }
-  var label: String {
-    switch self {
-    case .Todo: return "square.and.pencil"
-    case .Complete: return "checkmark"
-    case .InProgress: return "ellipsis.circle"
-    case .Hiatus: return "clock.badge.questionmark"
-    case .Waiting: return "airplane.circle"
-    }
-  }
-}
+import SwiftUI
 
 @Model
-final class SubTask: Codable, Ordered {
-  var sortOrder: Int = 0
-  var name: String
-  var task: Task?
-  var notes: String = ""
-  var status: Status = Status.Todo
-
-  init(name: String, parent: Task? = nil) {
-    self.name = name
-    self.task = parent
-    self.sortOrder = parent?.subtasks.count ?? 0
-  }
-
-  func yaml(_ indent: Int = 0) -> String {
-    let hdr = String(repeating: "  ", count: indent)
-    let subhdr = hdr + "  # "
-    var rval = hdr + "- [\(status.rawValue)] \(name)\n"
-    if !notes.isEmpty {
-      rval += subhdr + notes.replacingOccurrences(of: "\n", with: "\n" + subhdr) + "\n"
-    }
-    return rval
-  }
-
-  enum CodingKeys: CodingKey { case name, notes, status }
-
-  required init(from decoder: any Decoder) throws {
-    let container = try decoder.container(keyedBy: CodingKeys.self)
-    name = try container.decode(String.self, forKey: .name)
-    notes = try container.decode(String.self, forKey: .notes)
-    status = try container.decode(Status.self, forKey: .status)
-  }
+final class Task: Codable, Ordered, Aggregate {
+  typealias Element = SubTask
 
-  func encode(to encoder: any Encoder) throws {
-    var container = encoder.container(keyedBy: CodingKeys.self)
-    try container.encode(name, forKey: .name)
-    try container.encode(notes, forKey: .notes)
-    try container.encode(status, forKey: .status)
-  }
-}
-
-@Model
-final class Tag: Codable {
-  var id: String
-  var task: Task?
-
-  init(id: String, parent: Task? = nil) {
-    self.id = id
-    self.task = parent
-  }
-
-  func like(_ str: String) -> Bool {
-    return id.caseInsensitiveCompare(str) == .orderedSame
-  }
-
-  required init(from decoder: any Decoder) throws {
-    id = try decoder.singleValueContainer().decode(String.self)
-  }
-
-  func encode(to encoder: any Encoder) throws {
-    var single = encoder.singleValueContainer()
-    try single.encode(id)
-  }
-}
-
-@Model
-final class Task: Codable, Ordered {
   var sortOrder: Int = 0
   var name: String
   var project: Project?
@@ -116,6 +31,22 @@ final class Task: Codable, Ordered {
     self.sortOrder = parent?.tasks.count ?? 0
   }
 
+  func move(fromOffsets: IndexSet, toOffset: Int) {
+    subtasks.move(fromOffsets: fromOffsets, toOffset: toOffset)
+    reindex()
+  }
+
+  func remove(_ item: Element) {
+    subtasks.removeAll(where: { $0.id == item.id })
+    reindex()
+  }
+
+  func reindex() {
+    for (index, item) in subtasks.enumerated() {
+      item.sortOrder = index
+    }
+  }
+
   func yaml(_ indent: Int = 0) -> String {
     let hdr = String(repeating: "  ", count: indent)
     let subhdr = hdr + "  # "

+ 4 - 23
Todos/View/ProjectPanelView.swift

@@ -76,34 +76,15 @@ struct ProjectPanelView: View {
     }
   }
 
-  private func moveItem(_ within: Project, _ fromOffsets: IndexSet, _ toOffset: Int) {
+  private func moveItem(_ within: any Aggregate, _ fromOffsets: IndexSet, _ toOffset: Int) {
     withAnimation {
-      within.tasks.move(fromOffsets: fromOffsets, toOffset: toOffset)
-      for (index, item) in within.tasks.enumerated() {
-        item.sortOrder = index
-      }
-    }
-  }
-
-  private func moveItem(_ within: Task, _ fromOffsets: IndexSet, _ toOffset: Int) {
-    withAnimation {
-      within.subtasks.move(fromOffsets: fromOffsets, toOffset: toOffset)
-      for (index, item) in within.subtasks.enumerated() {
-        item.sortOrder = index
-      }
-    }
-  }
-
-  private func deleteItem(item: Task, from: Project) {
-    withAnimation {
-      from.tasks.removeAll(where: { $0.id == item.id })
-      modelContext.delete(item)
+      within.move(fromOffsets: fromOffsets, toOffset: toOffset)
     }
   }
 
-  private func deleteItem(item: SubTask, from: Task) {
+  private func deleteItem<T : Aggregate>(item : T.Element, from: T) where T.Element : PersistentModel {
     withAnimation {
-      from.subtasks.removeAll(where: { $0.id == item.id })
+      from.remove(item)
       modelContext.delete(item)
     }
   }