fixed iphone layout issue now :P
This commit is contained in:
parent
6c54c18c8e
commit
ee28fab17a
1 changed files with 180 additions and 150 deletions
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue