Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions Basic-Car-Maintenance/Shared/Design/LiquidGlassModifiers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import SwiftUI

struct LiquidGlassCardModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.colorSchemeContrast) var colorSchemeContrast

func body(content: Content) -> some View {
content
.background(
reduceTransparency ?
Color(UIColor.secondarySystemGroupedBackground) : Color.clear
)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(colorScheme == .dark ? 0.3 : 0.1), radius: 6, y: 3)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(
colorSchemeContrast == .increased ?
Color.primary.opacity(0.8) : .primary.opacity(0.15),
lineWidth: colorSchemeContrast == .increased ? 2.0 : 0.5
)
)
}
}

struct LiquidGlassSectionModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.colorSchemeContrast) var colorSchemeContrast

func body(content: Content) -> some View {
content
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(colorScheme == .dark ? 0.2 : 0.08), radius: 4, y: 2)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(
colorSchemeContrast == .increased ?
Color.primary.opacity(0.5) : .primary.opacity(0.12),
lineWidth: colorSchemeContrast == .increased ? 1.0 : 0.5
)
)
}
}

struct LiquidGlassChartModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.colorSchemeContrast) var colorSchemeContrast

func body(content: Content) -> some View {
content
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(colorScheme == .dark ? 0.25 : 0.1), radius: 8, y: 4)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(
colorSchemeContrast == .increased ?
Color.primary.opacity(0.6) : .primary.opacity(0.2),
lineWidth: colorSchemeContrast == .increased ? 1.0 : 0.5
)
)
.padding(.horizontal)
}
}

extension View {
func liquidGlassCard() -> some View {
modifier(LiquidGlassCardModifier())
}

func liquidGlassSection() -> some View {
modifier(LiquidGlassSectionModifier())
}

func liquidGlassChart() -> some View {
modifier(LiquidGlassChartModifier())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,43 +34,67 @@ struct AddOdometerReadingView: View {

var body: some View {
NavigationStack {
Form {
Section {
HStack {
TextField("Distance", value: $distance, format: .number)
ScrollView {
VStack(spacing: 20) {
VStack(spacing: 16) {
HStack {
Image(systemName: SFSymbol.speedometer)
.foregroundStyle(.secondary)
TextField("Distance", value: $distance, format: .number)
}

Picker(selection: $isMetric) {
Text("Miles").tag(false)
Text("Kilometers").tag(true)
Text("Miles", comment: "Label for miles unit").tag(false)
Text("Kilometers", comment: "Label for kilometers unit").tag(true)
} label: {
Text("Preferred units",
comment: "Label for units selected when adding an odometer reading")
comment: "Label for unit system picker")
}
.pickerStyle(.segmented)
}
}

Section {
Picker(selection: $selectedVehicleID) {
ForEach(vehicles) { vehicle in
Text(vehicle.name)
.tag(vehicle.id)
.padding()
.liquidGlassSection()

VStack(alignment: .leading, spacing: 12) {
Text("VehicleSectionHeader",
comment: "Label for Picker for selecting a vehicle")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, 8)

HStack {
Image(systemName: SFSymbol.carFill)
.foregroundStyle(.secondary)
Picker(selection: $selectedVehicleID) {
ForEach(vehicles) { vehicle in
Text(vehicle.name)
.tag(vehicle.id as String?)
}
} label: {
Text("Select a vehicle",
comment: "Picker for selecting a vehicle")
}
.pickerStyle(.menu)
}
} label: {
Text("Select a vehicle",
comment: "Picker for selecting a vehicle")
.padding()
.liquidGlassSection()
}
.pickerStyle(.menu)
} header: {
Text("VehicleSectionHeader",
comment: "Label for Picker for selecting a vehicle")
}

DatePicker(selection: $date, displayedComponents: .date) {
Text("Date", comment: "Date picker label")

VStack {
DatePicker(selection: $date, displayedComponents: .date) {
Label {
Text("Date", comment: "Date picker label")
} icon: {
Image(systemName: SFSymbol.calendar)
}
}
.dynamicTypeSize(...DynamicTypeSize.accessibility2)
}
.padding()
.liquidGlassSection()
}
.dynamicTypeSize(...DynamicTypeSize.accessibility2)
}
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
.onAppear {
if !vehicles.isEmpty {
selectedVehicleID = vehicles[0].id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,37 +22,62 @@ struct EditOdometerReadingView: View {

var body: some View {
NavigationStack {
Form {
Section {
HStack {
TextField("Distance", value: $distance, format: .number)
ScrollView {
VStack(spacing: 20) {
VStack(spacing: 16) {
HStack {
Image(systemName: SFSymbol.speedometer)
.foregroundStyle(.secondary)
TextField("Distance", value: $distance, format: .number)
}

Picker(selection: $isMetric) {
Text("Miles").tag(false)
Text("Kilometers").tag(true)
Text("Miles", comment: "Label for miles unit").tag(false)
Text("Kilometers", comment: "Label for kilometers unit").tag(true)
} label: {
Text("Preferred units",
comment: "Label for units selected when adding an odometer reading")
comment: "Label for unit system picker")
}
.pickerStyle(.segmented)
}
}

Section {
if let vehicleName = vehicles
.filter({ $0.id == selectedReading.vehicleID }).first?.name {
Text(vehicleName)
.opacity(0.3)
.padding()
.liquidGlassSection()

VStack(alignment: .leading, spacing: 12) {
Text("Vehicle", comment: "Label for vehicle selection")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, 8)

HStack {
Image(systemName: SFSymbol.carFill)
.foregroundStyle(.secondary)
if let vehicleName = vehicles
.filter({ $0.id == selectedReading.vehicleID }).first?.name {
Text(vehicleName)
.foregroundStyle(.secondary)
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.liquidGlassSection()
}
} header: {
Text("Vehicle")
}

DatePicker(selection: $date, displayedComponents: .date) {
Text("Date", comment: "Date picker label")

VStack {
DatePicker(selection: $date, displayedComponents: .date) {
Label {
Text("Date", comment: "Date picker label")
} icon: {
Image(systemName: SFSymbol.calendar)
}
}
.dynamicTypeSize(...DynamicTypeSize.accessibility2)
}
.padding()
.liquidGlassSection()
}
.dynamicTypeSize(...DynamicTypeSize.accessibility2)
}
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
.onAppear {
setEditReadingValues(selectedReading)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,29 @@ struct OdometerRowView: View {
let vehicleName: String?

var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("\(vehicleName ?? "No Name")")
.font(.title3)
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: SFSymbol.carFill)
.foregroundStyle(.secondary)
Text(vehicleName ?? "No Name")
.font(.headline)
}

VStack(alignment: .leading, spacing: 4) {
Text("Mileage: \(reading.distance) \(reading.isMetric ? "km" : "mi")")
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 8) {
Label {
Text("\(reading.distance) \(reading.isMetric ? "kilometers" : "miles")")
} icon: {
Image(systemName: SFSymbol.speedometer)
}

Text("Recorded On: \(reading.date.formatted(date: .abbreviated, time: .omitted))")
.foregroundStyle(.gray)
Label {
Text(reading.date.formatted(date: .abbreviated, time: .omitted))
} icon: {
Image(systemName: SFSymbol.calendar)
}
}
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
Expand Down
54 changes: 33 additions & 21 deletions Basic-Car-Maintenance/Shared/Odometer/Views/OdometerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,34 +45,35 @@ struct OdometerView: View {
.padding(.horizontal)

if !viewModel.readings.isEmpty {
GroupBox {
Chart {
ForEach(viewModel.vehicles) { vehicle in
let vehicleReadings = filteredReadings(for: vehicle)

if !vehicleReadings.isEmpty {
ForEach(vehicleReadings) { reading in
LineMark(
x: .value("Date", reading.date, unit: .day),
y: .value("Odometer", reading.distance)
)
}
.foregroundStyle(by: .value("Vehicle", vehicle.name))
.symbol(by: .value("Vehicle", vehicle.name))
.interpolationMethod(.monotone)
Chart {
ForEach(viewModel.vehicles) { vehicle in
let vehicleReadings = filteredReadings(for: vehicle)

if !vehicleReadings.isEmpty {
ForEach(vehicleReadings) { reading in
LineMark(
x: .value("Date", reading.date, unit: .day),
y: .value("Odometer", reading.distance)
)
}
.foregroundStyle(by: .value("Vehicle", vehicle.name))
.symbol(by: .value("Vehicle", vehicle.name))
.interpolationMethod(.monotone)
}
}
.frame(height: 200)
}
.padding(.horizontal)
.listRowSeparator(.hidden)
.frame(height: 200)
.liquidGlassChart()
}

List {
ForEach(filteredReadings) { reading in
let vehicleName = viewModel.vehicles.first { $0.id == reading.vehicleID }?.name
OdometerRowView(reading: reading, vehicleName: vehicleName)
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.liquidGlassCard()
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
Task {
Expand All @@ -94,13 +95,24 @@ struct OdometerView: View {
}
}
}
.listStyle(.inset)
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
.background(Color(UIColor.systemGroupedBackground).ignoresSafeArea())
}
.overlay {
if viewModel.readings.isEmpty {
Text("Add your first odometer",
comment: "Placeholder text for empty odometer reading list")
ContentUnavailableView {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call!

Label {
Text("Tap the + to begin",
comment: "Empty odometer list prompt")
} icon: {
Image(systemName: SFSymbol.speedometer)
}
} description: {
Text("Add your first odometer",
comment: "Placeholder description for empty odometer reading list")
}
}
}
.navigationTitle(Text("Odometer"))
Expand Down
3 changes: 3 additions & 0 deletions Basic-Car-Maintenance/Shared/Utilities/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,14 @@ enum SFSymbol {
static let filter = "line.3.horizontal.decrease"
static let plus = "plus"
static let share = "square.and.arrow.up"
static let carFill = "car.fill"

// Dashboard
static let trash = "trash"
static let pencil = "pencil"
static let magnifyingGlass = "magnifyingglass"
static let speedometer = "speedometer"
static let calendar = "calendar"

// Settings
static let document = "doc.badge.plus"
Expand Down