Skip to content

Instantly share code, notes, and snippets.

@jmb
Last active June 5, 2024 05:04
Show Gist options
  • Save jmb/c6087d5299c1b795aa01684511c341ab to your computer and use it in GitHub Desktop.
Save jmb/c6087d5299c1b795aa01684511c341ab to your computer and use it in GitHub Desktop.
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
}
}
}
}
}
}
}
@jmb
Copy link
Author

jmb commented Apr 28, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment