initial pre-viable version of watch app

This commit is contained in:
2025-07-20 19:44:53 -04:00
parent 33b88cb8f0
commit 68d90160c6
35 changed files with 2108 additions and 179 deletions

View File

@ -0,0 +1,27 @@
import SwiftUI
import SwiftData
/// A protocol for entities that can be managed in a generic list view.
protocol EditableEntity: PersistentModel, Identifiable, Hashable {
/// The name of the entity to be displayed in the list.
var name: String { get set }
/// A view for adding or editing the entity.
associatedtype FormView: View
@ViewBuilder static func formView(for model: Self) -> FormView
/// Creates a new, empty instance of the entity.
static func createNew() -> Self
/// The title for the navigation bar in the list view.
static var navigationTitle: String { get }
/// An optional property to specify a count to be displayed in the list item.
var count: Int? { get }
}
extension EditableEntity {
var count: Int? {
return nil
}
}

View File

@ -0,0 +1,48 @@
import SwiftUI
import SwiftData
struct EntityAddEditView<T: EditableEntity, Content: View>: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@State var model: T
private let content: (Binding<T>) -> Content
init(model: T, @ViewBuilder content: @escaping (Binding<T>) -> Content) {
_model = State(initialValue: model)
self.content = content
}
var body: some View {
NavigationStack {
Form {
content($model)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
// If the model is not in a context, it's a new one.
if model.modelContext == nil {
modelContext.insert(model)
}
// The save is in a do-catch block to handle potential errors,
// such as constraint violations.
do {
try modelContext.save()
dismiss()
} catch {
// In a real app, you'd want to present this error to the user.
print("Failed to save model: \(error.localizedDescription)")
}
}
}
}
}
}
}

View File

@ -0,0 +1,96 @@
import SwiftUI
import SwiftData
struct EntityListView<T: EditableEntity>: View {
@Environment(\.modelContext) private var modelContext
@Query var items: [T]
@State private var showingAddSheet = false
@State private var itemToEdit: T? = nil
@State private var itemToDelete: T? = nil
private var sortDescriptors: [SortDescriptor<T>]
init(sort: [SortDescriptor<T>] = [], searchString: String = "") {
self.sortDescriptors = sort
_items = Query(filter: #Predicate { item in
if searchString.isEmpty {
return true
} else {
return item.name.localizedStandardContains(searchString)
}
}, sort: sort)
}
@State private var isInsideNavigationStack: Bool = false
var body: some View {
let content = Form {
List {
ForEach(items) { item in
ListItem(text: item.name, count: item.count)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive) {
itemToDelete = item
} label: {
Label("Delete", systemImage: "trash")
}
Button {
itemToEdit = item
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
}
}
.navigationTitle(T.navigationTitle)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAddSheet.toggle() }) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddSheet) {
T.formView(for: T.createNew())
}
.sheet(item: $itemToEdit) { item in
T.formView(for: item)
}
.confirmationDialog(
"Delete?",
isPresented: Binding<Bool>(
get: { itemToDelete != nil },
set: { if !$0 { itemToDelete = nil } }
),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let item = itemToDelete {
modelContext.delete(item)
try? modelContext.save()
itemToDelete = nil
}
}
Button("Cancel", role: .cancel) {
itemToDelete = nil
}
} message: {
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
}
.background(
NavigationStackChecker(isInside: $isInsideNavigationStack)
)
if isInsideNavigationStack {
content
} else {
NavigationStack {
content
}
}
}
}

View File

@ -0,0 +1,15 @@
import SwiftUI
struct NavigationStackChecker: UIViewControllerRepresentable {
@Binding var isInside: Bool
func makeUIViewController(context: Context) -> UIViewController {
let viewController = UIViewController()
DispatchQueue.main.async {
self.isInside = viewController.navigationController != nil
}
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}