-
-
Save jmb/c6087d5299c1b795aa01684511c341ab to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import SwiftUI | |
import SwiftData | |
// MARK - Model | |
@Model | |
class Validation: Hashable, Identifiable { | |
var id: String | |
var name: String | |
var colourName: String | |
var expiry: Date = Calendar.current.date(byAdding: .year, value: 1, to: Date()) ?? Date.distantPast | |
@Transient var isNew: Bool = false | |
init(id: String = "", name: String = "", colourName: String = "", expiry: Date = Date()) { | |
self.id = id.uppercased() | |
self.name = name | |
self.colourName = colourName | |
self.expiry = expiry | |
} | |
init() { | |
self.id = "" | |
self.name = "" | |
self.colourName = "" | |
self.isNew = true | |
} | |
var isEmpty: Bool { | |
self.id.isEmpty && self.name.isEmpty | |
} | |
var daysUntilExpiry: Int { | |
let expiry = Calendar.current.startOfDay(for: self.expiry) | |
let today = Calendar.current.startOfDay(for: .now) | |
let days = Calendar.current.dateComponents([.day], from: today, to: expiry).day | |
return days ?? -1 | |
} | |
var isExpired: Bool { | |
self.daysUntilExpiry < 0 | |
} | |
} | |
// MARK - Main View for settings | |
struct SettingsView: View { | |
@Environment(\.dismiss) var dismiss | |
@Environment(\.modelContext) var modelContext | |
@Query var validationEntries: [Validation] | |
@State private var selectedValidation: Validation? | |
@State private var addEditValidationID: PersistentIdentifier? = nil | |
var body: some View { | |
NavigationStack { | |
List { | |
Section( | |
header: | |
HStack{ | |
Text("Validations") | |
Spacer() | |
EditButton() | |
} | |
) { | |
ForEach(validationEntries) { validation in | |
ValidationSummaryRow(validation: validation) | |
.onTapGesture { | |
selectedValidation = validation | |
addEditValidationID = validation.persistentModelID | |
} | |
} | |
.onDelete(perform: deleteValidations) | |
Button { | |
selectedValidation = nil | |
addEditValidationID = Validation().persistentModelID | |
} label: { | |
HStack { | |
Image(systemName: "plus") | |
Text("Add Validation") | |
} | |
} | |
} | |
} | |
.font(.headline) | |
.listStyle(.grouped) | |
.navigationTitle("Settings") | |
.sheet(item: $addEditValidationID, onDismiss: { | |
let count = validationEntries.count | |
print("Validation entries count: \(count)") | |
}) { addEditID in | |
do { | |
print("Presenting sheet for \(addEditID)") | |
return AddEditValidationView(validationID: addEditID, in: modelContext.container) | |
} | |
} | |
} | |
} | |
func validationRow(validation: Validation) -> some View { | |
HStack(spacing: 20){ | |
ValidationButton(text: validation.id, colourName: validation.colourName) | |
Text(validation.name) | |
.frame(maxWidth: .infinity, alignment: .leading) | |
if validation.isExpired { | |
HStack { | |
Text("Expired") | |
Image(systemName: "exclamationmark.triangle.fill") | |
} | |
.foregroundStyle(.red) | |
} else { | |
Text("\(validation.daysUntilExpiry) days") | |
.foregroundStyle( | |
validation.daysUntilExpiry < 15 ? .red : | |
validation.daysUntilExpiry < 30 ? .orange : | |
validation.daysUntilExpiry < 90 ? .green : .primary | |
) | |
} | |
} | |
} | |
} | |
// MARK - Add/Edit Validation | |
struct AddEditValidationView: View { | |
@Environment(\.dismiss) var dismiss | |
@Bindable var validation: Validation | |
var modelContext: ModelContext | |
private var editorTitle: String = "New Validation" | |
init(validationID: PersistentIdentifier, in container: ModelContainer) { | |
print("Initialising AddEditValidationView with ID: \(validationID)") | |
modelContext = ModelContext(container) | |
modelContext.autosaveEnabled = false | |
print("Created new ModelContext") | |
if var editValidation: Validation = try? modelContext.existingModel(for: validationID) { | |
print("Looked up ID in modelContext and got: \(editValidation)") | |
validation = editValidation | |
editorTitle = "Edit Validation" | |
} else { | |
print("New validation") | |
validation = Validation() | |
modelContext.insert(validation) | |
} | |
} | |
@Query var allValidations: [Validation] | |
private var disableSave: Bool { | |
return validation.id.isEmpty || validation.name.isEmpty | |
} | |
var body: some View { | |
NavigationStack { | |
Form { | |
TextField("ID", text: $validation.id) | |
TextField("Name", text: $validation.name) | |
DatePicker("Expiry Date", selection: $validation.expiry, displayedComponents: [.date]) | |
ColourPicker(selectedColour: $validation.colourName) | |
} | |
.toolbar { | |
ToolbarItem(placement: .principal) { | |
Text(editorTitle) | |
} | |
ToolbarItem(placement: .confirmationAction) { | |
Button("Save") { | |
withAnimation { | |
save() | |
dismiss() | |
} | |
} | |
.disabled(disableSave) | |
} | |
ToolbarItem(placement: .cancellationAction) { | |
Button("Cancel", role: .cancel) { | |
dismiss() | |
} | |
} | |
} | |
} | |
} | |
func cancel() { | |
dismiss() | |
} | |
private func save() { | |
let expiryStartOfDay = Calendar.current.startOfDay(for: validation.expiry) | |
validation.expiry = expiryStartOfDay | |
try? modelContext.save() | |
} | |
} | |
// MARK - Extra helper views | |
struct ValidationSummaryRow: View { | |
@State var validation: Validation | |
var body: some View { | |
HStack(spacing: 20){ | |
ValidationButton(text: validation.id, colourName: validation.colourName) | |
Text(validation.name) | |
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) | |
.background(.secondary.opacity(0.0001)) // make the space in the row tappable | |
if validation.isExpired { | |
HStack { | |
Text("Expired") | |
Image(systemName: "exclamationmark.triangle.fill") | |
} | |
.foregroundStyle(.red) | |
} else { | |
Text("\(validation.daysUntilExpiry) days") | |
.foregroundStyle( | |
validation.daysUntilExpiry < 15 ? .red : | |
validation.daysUntilExpiry < 30 ? .orange : | |
validation.daysUntilExpiry < 90 ? .green : .primary | |
) | |
} | |
} | |
} | |
} | |
struct ColourPicker: View { | |
@Binding var selectedColour: String | |
@Namespace private var colourPickerNamespace | |
var body: some View { | |
HStack { | |
ForEach(Color.buttonColours, id: \.self) { colour in | |
ZStack { | |
if selectedColour == colour { | |
RoundedRectangle(cornerRadius: 6) | |
.fill(selectedColour == colour ? Color(colour) : .white) | |
.matchedGeometryEffect(id: "picker", in: colourPickerNamespace) | |
} | |
RoundedRectangle(cornerRadius: 6) | |
.stroke(Color(colour), lineWidth: 2) | |
.fill(Color(colour).opacity(0.1)) | |
} | |
.frame(maxWidth: .infinity, maxHeight: .infinity) | |
.onTapGesture { | |
withAnimation(.spring){ | |
if selectedColour == colour { | |
selectedColour = "" | |
} else { | |
selectedColour = colour | |
} | |
} | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Wow, thank you for the suggestions! I tried the first of your versions and it seems to work, so no idea why my original in my project was blocking editing the text.