fixed iphone layout issue now :P

This commit is contained in:
Dallas Groot 2026-04-06 02:16:19 -07:00
parent 6c54c18c8e
commit ee28fab17a

View file

@ -8,140 +8,97 @@ struct ContentView: View {
@AppStorage("currentSessionNotes") private var sessionNotes: String = ""
@Environment(\.modelContext) private var modelContext
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@State private var showingNotes = false
@State private var showingHistory = false
@State private var showingResetAlert = false
var body: some View {
VStack(spacing: 0) {
// Header
HStack {
Menu {
Button(action: { showingNotes = true }) {
Label("Session Notes", systemImage: "note.text")
}
Button(action: { showingHistory = true }) {
Label("Saved History", systemImage: "clock.arrow.circlepath")
}
Divider()
Button(action: saveCurrentSession) {
Label("Save This Total", systemImage: "square.and.arrow.down")
}
} label: {
Image(systemName: "ellipsis.circle")
.font(.title)
.foregroundColor(.white)
}
ZStack(alignment: .trailing) {
// 1. MAIN INTERFACE LAYER
VStack(spacing: 0) {
headerView
Spacer()
Text("Money Counter")
.font(.title2).bold()
.foregroundColor(.white)
Spacer()
Button(action: { showingResetAlert = true }) {
Image(systemName: "arrow.counterclockwise")
.font(.title)
.foregroundColor(.white)
// Magic Layout Container: Automatically snaps between layouts based on exact window width
ViewThatFits(in: .horizontal) {
// OPTION 1: Wide iPad Layout
HStack(alignment: .top, spacing: 0) {
ScrollView { VStack(spacing: 24) { looseSection }.padding() }
.frame(minWidth: 280, maxWidth: .infinity)
Divider()
ScrollView { VStack(spacing: 24) { rollsSection }.padding() }
.frame(minWidth: 280, maxWidth: .infinity)
Divider()
dashboardCard
.frame(width: 320, maxHeight: .infinity, alignment: .top) // Forces iPad column to stretch
.background(Color(UIColor.secondarySystemBackground))
}
.scrollDismissesKeyboard(.interactively)
// OPTION 2: Fallback Narrow/iPhone Layout
VStack(spacing: 0) {
ScrollView {
VStack(spacing: 24) {
looseSection
VStack(alignment: .leading, spacing: 8) {
Divider().background(Color.gray)
Text("Rolls").font(.title3).bold().foregroundColor(.secondary)
}.padding(.top, 8)
rollsSection
}
.padding()
}
.scrollDismissesKeyboard(.interactively)
dashboardCard
.background(Color(UIColor.secondarySystemBackground)) // Hugs tightly on iPhone
}
}
}
.padding()
.background(Color(red: 0.4, green: 0.7, blue: 0.4))
// Scrolling Denominations
ScrollView {
VStack(spacing: 24) {
ForEach($viewModel.looseDenominations) { $denom in
DenominationRow(denomination: $denom, focusedField: $focusedField, valueColor: Color(UIColor.systemGreen))
}
VStack(alignment: .leading, spacing: 8) {
Divider().background(Color.gray)
Text("Rolls")
.font(.title3)
.bold()
.foregroundColor(.secondary)
}
.padding(.top, 8)
ForEach($viewModel.rollDenominations) { $denom in
DenominationRow(denomination: $denom, focusedField: $focusedField, valueColor: Color(UIColor.systemRed))
}
}
.padding()
}
.contentShape(Rectangle())
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(UIColor.systemBackground))
.onTapGesture { dismissKeyboard() }
// 1. Native drag-to-dismiss behavior
.scrollDismissesKeyboard(.interactively)
// Footer (Math & Totals)
VStack(spacing: 8) {
HStack {
Text("Starting Float")
.font(.headline)
.foregroundColor(.secondary)
Spacer()
HStack(spacing: 2) {
Text("$")
.foregroundColor(.secondary)
TextField("0.00", value: $viewModel.startingFloat, format: .number.precision(.fractionLength(2)))
.keyboardType(.decimalPad)
.focused($isFloatFocused)
.multilineTextAlignment(.trailing)
}
.font(.headline)
.padding(6)
.frame(width: 100)
.background(Color(UIColor.tertiarySystemFill), in: RoundedRectangle(cornerRadius: 8))
}
if viewModel.startingFloat > 0 {
HStack {
Text(viewModel.discrepancy >= 0 ? "Deposit Amount" : "Shortage")
.font(.headline)
Spacer()
Text(String(format: "$%.2f", viewModel.discrepancy))
.font(.headline)
.foregroundColor(viewModel.discrepancy >= 0 ? .primary : .red)
}
}
Divider()
HStack {
Text("Total")
.font(.largeTitle)
.bold()
Spacer()
Text(String(format: "$%.2f", viewModel.total))
.font(.largeTitle)
.bold()
}
// 2. IPAD SLIDE-OVER PANEL LAYER
if showingNotes && horizontalSizeClass == .regular {
NotesView(notes: $sessionNotes, total: viewModel.total, discrepancy: viewModel.discrepancy, expectedFloat: viewModel.startingFloat, isPresented: $showingNotes)
.frame(width: 375)
.background(Color(UIColor.systemBackground))
.shadow(color: Color.black.opacity(0.3), radius: 25, x: -10, y: 0)
.transition(.move(edge: .trailing))
.zIndex(2)
.ignoresSafeArea(.all, edges: .bottom)
}
.padding()
.background(Color(UIColor.secondarySystemBackground))
.foregroundColor(.primary)
}
.background(Color(UIColor.systemBackground))
// 2. Adds the "Done" button directly above the keyboard
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
dismissKeyboard()
if UIDevice.current.userInterfaceIdiom != .pad {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") { dismissKeyboard() }
}
.font(.headline)
.foregroundColor(.blue)
}
}
.keyboardShortcut("s", modifiers: .command)
.onKeyPress(.downArrow) { moveFocus(forward: true); return .handled }
.onKeyPress(.upArrow) { moveFocus(forward: false); return .handled }
.onChange(of: viewModel.looseDenominations) { _, _ in viewModel.saveCache() }
.onChange(of: viewModel.rollDenominations) { _, _ in viewModel.saveCache() }
.onChange(of: viewModel.startingFloat) { _, _ in viewModel.saveCache() }
.sheet(isPresented: $showingNotes) {
NotesView(notes: $sessionNotes, total: viewModel.total, discrepancy: viewModel.discrepancy, expectedFloat: viewModel.startingFloat)
// IPHONE FALLBACK: Continues to use a standard bottom sheet on mobile
.sheet(isPresented: Binding(
get: { showingNotes && horizontalSizeClass != .regular },
set: { showingNotes = $0 }
)) {
NotesView(notes: $sessionNotes, total: viewModel.total, discrepancy: viewModel.discrepancy, expectedFloat: viewModel.startingFloat, isPresented: $showingNotes)
.presentationDetents([.medium, .large])
}
.sheet(isPresented: $showingHistory) {
HistoryView(viewModel: viewModel)
@ -149,7 +106,7 @@ struct ContentView: View {
.confirmationDialog("Reset Everything?", isPresented: $showingResetAlert) {
Button("Reset Counts & Notes", role: .destructive) {
withAnimation { viewModel.reset() }
sessionNotes = ""
sessionNotes = ""
}
Button("Reset Counts Only", role: .destructive) {
withAnimation { viewModel.reset() }
@ -158,23 +115,114 @@ struct ContentView: View {
}
}
// MARK: - Subviews
private var headerView: some View {
HStack {
Menu {
Button(action: {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
showingNotes.toggle()
}
}) { Label("Session Notes", systemImage: "note.text") }
Button(action: { showingHistory = true }) { Label("Saved History", systemImage: "clock.arrow.circlepath") }
Divider()
Button(action: saveCurrentSession) { Label("Save This Total", systemImage: "square.and.arrow.down") }
} label: {
Image(systemName: "ellipsis.circle").font(.title).foregroundColor(.white)
}
Spacer()
Text("Money Counter").font(.title2).bold().foregroundColor(.white)
Spacer()
Button(action: { showingResetAlert = true }) {
Image(systemName: "arrow.counterclockwise").font(.title).foregroundColor(.white)
}
}
.padding()
.background(Color(red: 0.4, green: 0.7, blue: 0.4))
}
private var looseSection: some View {
ForEach($viewModel.looseDenominations) { $denom in
DenominationRow(denomination: $denom, focusedField: $focusedField, valueColor: Color(UIColor.systemGreen))
}
}
private var rollsSection: some View {
ForEach($viewModel.rollDenominations) { $denom in
DenominationRow(denomination: $denom, focusedField: $focusedField, valueColor: Color(UIColor.systemRed))
}
}
// Notice: The internal Spacer() and .background() modifiers have been completely removed
private var dashboardCard: some View {
VStack(spacing: 16) {
HStack {
Text("Starting Float").font(.headline).foregroundColor(.secondary)
Spacer()
HStack(spacing: 2) {
Text("$").foregroundColor(.secondary)
TextField("0.00", value: $viewModel.startingFloat, format: .number.precision(.fractionLength(2)))
.keyboardType(.decimalPad)
.focused($isFloatFocused)
.multilineTextAlignment(.trailing)
}
.font(.headline).padding(6).frame(width: 100)
.background(Color(UIColor.tertiarySystemFill), in: RoundedRectangle(cornerRadius: 8))
}
if viewModel.startingFloat > 0 {
HStack {
Text(viewModel.discrepancy >= 0 ? "Deposit Amount" : "Shortage").font(.headline)
Spacer()
Text(String(format: "$%.2f", viewModel.discrepancy))
.font(.headline)
.foregroundColor(viewModel.discrepancy >= 0 ? .primary : .red)
}
}
Divider()
HStack {
Text("Total").font(.largeTitle).bold()
Spacer()
Text(String(format: "$%.2f", viewModel.total)).font(.largeTitle).bold()
}
}
.padding()
}
// MARK: - Logic
private func dismissKeyboard() {
focusedField = nil
isFloatFocused = false
}
private func moveFocus(forward: Bool) {
guard let current = focusedField,
let currentIndex = viewModel.allDenominationIDs.firstIndex(of: current) else { return }
let nextIndex = forward ? currentIndex + 1 : currentIndex - 1
if viewModel.allDenominationIDs.indices.contains(nextIndex) {
focusedField = viewModel.allDenominationIDs[nextIndex]
} else if forward && nextIndex == viewModel.allDenominationIDs.count {
focusedField = nil
isFloatFocused = true
}
}
private func saveCurrentSession() {
let newSave = SavedCount(
total: viewModel.total,
startingFloat: viewModel.startingFloat,
notes: sessionNotes,
snapshotData: viewModel.generateSnapshot()
)
let newSave = SavedCount(total: viewModel.total, startingFloat: viewModel.startingFloat, notes: sessionNotes, snapshotData: viewModel.generateSnapshot())
modelContext.insert(newSave)
viewModel.reset()
sessionNotes = ""
viewModel.startingFloat = 0.0
}
}
// DenominationRow remains exactly the same as the previous iteration
// MARK: - Denomination Row
struct DenominationRow: View {
@Binding var denomination: MoneyCounterViewModel.Denomination
var focusedField: FocusState<UUID?>.Binding
@ -183,52 +231,34 @@ struct DenominationRow: View {
var body: some View {
HStack(spacing: 16) {
HStack {
Text(formatValue(denomination.value))
.font(.title2).bold()
.foregroundColor(valueColor)
.frame(width: 70, alignment: .leading)
Text(formatValue(denomination.value)).font(.title2).bold().foregroundColor(valueColor).frame(width: 70, alignment: .leading)
Spacer()
}
.contentShape(Rectangle())
.contentShape(Rectangle())
.onTapGesture { focusedField.wrappedValue = nil }
Button(action: {
denomination.count += 1
focusedField.wrappedValue = nil
}) {
Image(systemName: "plus.circle.fill")
.foregroundColor(Color(UIColor.systemGreen))
.font(.title)
}
}) { Image(systemName: "plus.circle.fill").foregroundColor(Color(UIColor.systemGreen)).font(.title) }
Button(action: {
if denomination.count > 0 { denomination.count -= 1 }
focusedField.wrappedValue = nil
}) {
Image(systemName: "minus.circle.fill")
.foregroundColor(Color(UIColor.systemRed))
.font(.title)
}
}) { Image(systemName: "minus.circle.fill").foregroundColor(Color(UIColor.systemRed)).font(.title) }
TextField("0", value: $denomination.count, format: .number)
.keyboardType(.numberPad)
.multilineTextAlignment(.center)
.font(.title2).bold()
.frame(width: 80)
.padding(.vertical, 4)
.font(.title2).bold().frame(width: 80).padding(.vertical, 4)
.focused(focusedField, equals: denomination.id)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color(UIColor.separator)),
alignment: .bottom
)
.foregroundColor(.primary)
.overlay(Rectangle().frame(height: 1).foregroundColor(Color(UIColor.separator)), alignment: .bottom)
.foregroundColor(.primary)
}
}
private func formatValue(_ value: Double) -> String {
if value >= 1.0 { return String(format: "%.1f", value) }
if value >= 1.0 { return String(format: "%.1f", value) }
else { return String(format: "%.2f", value) }
}
}
}