Skip to content

Commit

Permalink
Implement export maintenance events
Browse files Browse the repository at this point in the history
  • Loading branch information
Omar Hegazy authored and Omar Hegazy committed Oct 19, 2024
1 parent 7595807 commit d244023
Show file tree
Hide file tree
Showing 7 changed files with 410 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import FirebaseFirestoreSwift
import Foundation

@Observable
class DashboardViewModel {
class DashboardViewModel: MaintenanceEventsFetcher {

let userUID: String?

Expand Down Expand Up @@ -166,6 +166,12 @@ class DashboardViewModel {
}
}
}

func fetchEvents(for vehicle: Vehicle) -> [MaintenanceEvent] {
events
.filter { $0.vehicleID == vehicle.id }
.sorted(by: { $0.date < $1.date })
}
}

// MARK: - Sort Option
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
//
// CarMaintenancePDFGenerator.swift
// Basic-Car-Maintenance
//
// https://github.com/mikaelacaron/Basic-Car-Maintenance
// See LICENSE for license information.
//

import UIKit
import SwiftUI

protocol PDFGeneratable {
func generatePDF() -> URL?
}

final class CarMaintenancePDFGenerator: PDFGeneratable {
private let title = "Basic Car Maintenance"
private let vehicleName: String
private let events: [MaintenanceEvent]
// Define the PDF page size (A4 size in points)
private let pageWidth: CGFloat = 595.2
private let pageHeight: CGFloat = 841.8
private var pageSize: CGSize

// Define page margins
private let topMargin: CGFloat = 50
private let bottomMargin: CGFloat = 50
private let leftMargin: CGFloat = 20
private let rightMargin: CGFloat = 20
private let columnWidth: CGFloat
private let documentsDirectory = FileManager
.default
.urls(for: .documentDirectory, in: .userDomainMask)
.first

init(vehicleName: String, events: [MaintenanceEvent]) {
self.vehicleName = vehicleName
self.events = events
self.pageSize = CGSize(width: pageWidth, height: pageHeight)
self.columnWidth = (pageWidth - leftMargin - rightMargin) / 3
}

func generatePDF() -> URL? {

Check warning on line 43 in Basic-Car-Maintenance/Shared/Dashboard/Views/CarMaintenancePDFGenerator.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Function body should span 50 lines or less excluding comments and whitespace: currently spans 60 lines (function_body_length)
guard !events.isEmpty else { return nil }
let fileName = "\(vehicleName)MaintenanceReport.pdf"

// Get the path to the documents directory
let fileURL = documentsDirectory?.appendingPathComponent(fileName)

// Create a PDF renderer
let pdfRenderer = UIGraphicsPDFRenderer(bounds: CGRect(origin: .zero, size: pageSize))

// Render content to the PDF file
let pdfData = pdfRenderer.pdfData { context in

// Initialize the y-position (start after the headers)
var yPosition: CGFloat = topMargin

// Function to begin a new page
func beginNewPage(isFirstPage: Bool) {
context.beginPage()

// Reset yPosition to start after the headers
yPosition = topMargin

// Draw headers for the first page
if isFirstPage {
drawHeader(
context: context,
yPosition: &yPosition
)
}
}

// Start the first page
beginNewPage(isFirstPage: true)

// Draw the content of the PDF
let tableRowAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 14),
.foregroundColor: UIColor.black
]

for (index, event) in events.enumerated() {
// Check if yPosition is close to the bottom of the page and create a new page if necessary
if yPosition + 60 > pageHeight - bottomMargin { // 60 is the estimated height of one row
beginNewPage(isFirstPage: index == 0)
}

// Draw Date and Vehicle Name as single-line text
event.date
.formatted()
.draw(
at: CGPoint(
x: leftMargin,
y: yPosition
),
withAttributes: tableRowAttributes
)

vehicleName
.draw(
at: CGPoint(
x: leftMargin + columnWidth,
y: yPosition
),
withAttributes: tableRowAttributes
)

// Draw Notes with text wrapping within a bounding rectangle
let notesRect = CGRect(
x: leftMargin + 2 * columnWidth,
y: yPosition,
width: columnWidth - 20,
height: 50
)
event.notes.draw(in: notesRect, withAttributes: tableRowAttributes)

// Adjust the spacing for the next row
yPosition += 60
}
}

// Save the PDF data to the file URL
do {
try pdfData.write(to: fileURL!)
print("PDF saved to: \(fileURL!.path)")
return fileURL
} catch {
print("Could not save the PDF: \(error)")
return nil
}
}

// Helper function to draw header
private func drawHeader(
context: UIGraphicsPDFRendererContext,
yPosition: inout CGFloat
) {
let titleAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.boldSystemFont(ofSize: 20),
.foregroundColor: UIColor.black
]
let subtitleAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 16),
.foregroundColor: UIColor.black
]

let titleString = "Basic Car Maintenance"
let eventTitleString = "Maintenance Events"
guard
let startDate = events.first?.date.formatted(date: .numeric, time: .omitted),
let endDate = events.last?.date.formatted(date: .numeric, time: .omitted)
else { return }
let dateRangeString = "From \(startDate) to \(endDate)"

let titleSize = titleString.size(withAttributes: titleAttributes)
let vehicleSize = vehicleName.size(withAttributes: subtitleAttributes)
let eventTitleSize = eventTitleString.size(withAttributes: subtitleAttributes)
let dateRangeSize = dateRangeString.size(withAttributes: subtitleAttributes)

// Center the headers
let titleX = (pageWidth - titleSize.width) / 2
let vehicleX = (pageWidth - vehicleSize.width) / 2
let eventTitleX = (pageWidth - eventTitleSize.width) / 2
let dateRangeX = (pageWidth - dateRangeSize.width) / 2

// Draw the main title centered
titleString.draw(at: CGPoint(x: titleX, y: yPosition), withAttributes: titleAttributes)
yPosition += 30

// Draw the vehicle name centered
vehicleName.draw(at: CGPoint(x: vehicleX, y: yPosition), withAttributes: subtitleAttributes)
yPosition += 30

// Draw the maintenance event title centered
eventTitleString.draw(at: CGPoint(x: eventTitleX, y: yPosition), withAttributes: subtitleAttributes)
yPosition += 30

// Draw date range centered
dateRangeString.draw(at: CGPoint(x: dateRangeX, y: yPosition), withAttributes: subtitleAttributes)
yPosition += 50 // Add more space after the headers

drawColumnsHeaders(yPosition: &yPosition)
}

private func drawColumnsHeaders(
yPosition: inout CGFloat
) {
// Draw the headers of each column
let dateColumnHeader = "Date"
let vehicleColumnHeader = "Vehicle Name"
let noteColumnHeader = "Notes"

let subtitleAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.boldSystemFont(ofSize: 16),
.foregroundColor: UIColor.black
]

dateColumnHeader
.draw(
at: CGPoint(
x: leftMargin,
y: yPosition
),
withAttributes: subtitleAttributes
)

vehicleColumnHeader
.draw(
at: CGPoint(
x: leftMargin + columnWidth,
y: yPosition
),
withAttributes: subtitleAttributes
)

// Draw Notes with text wrapping within a bounding rectangle
let notesRect = CGRect(
x: leftMargin + 2 * columnWidth,
y: yPosition,
width: columnWidth - 20,
height: 50
)
noteColumnHeader.draw(in: notesRect, withAttributes: subtitleAttributes)

// Adjust the spacing for the next row
yPosition += 30
}
}
41 changes: 41 additions & 0 deletions Basic-Car-Maintenance/Shared/Dashboard/Views/DashboardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ struct DashboardView: View {
@State private var isShowingAddView = false
@State private var viewModel: DashboardViewModel
@State private var isShowingEditView = false
@State private var isShowingShareView = false
@State private var didGetURL = false
@State private var pdfURL: URL?
@State private var selectedMaintenanceEvent: MaintenanceEvent?

init(userUID: String?) {
Expand Down Expand Up @@ -149,13 +152,51 @@ struct DashboardView: View {
}
}
}
.toolbar {
if !viewModel.events.isEmpty {
ToolbarItem(placement: .topBarLeading) {
Button {
isShowingShareView = true
} label: {
Image(systemName: SFSymbol.share)
}
.accessibilityShowsLargeContentViewer {
Label {
Text("ExportEvent", comment: "Label for exporting maintenance events")
} icon: {
Image(systemName: SFSymbol.share)
}
}
}
}
}
.task {
await viewModel.getMaintenanceEvents()
await viewModel.getVehicles()
}
.sheet(isPresented: $isShowingAddView) {
makeAddMaintenanceView()
}
.sheet(isPresented: $isShowingShareView) {
ExportOptionsView(
vehicles: viewModel.vehicles,
eventsFetcher: viewModel
) { url in
pdfURL = url
}
.presentationDetents([.fraction(0.35)])
.presentationCornerRadius(10)
}
.onChange(of: pdfURL) { _, newURL in
if newURL != nil {
didGetURL = true
}
}
.sheet(isPresented: $didGetURL) {
if let pdfURL {
PDFShareController(url: pdfURL)
}
}
}
.onChange(of: scenePhase) { _, newScenePhase in
guard case .active = newScenePhase else { return }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// ExportOptionsView.swift
// Basic-Car-Maintenance
//
// https://github.com/mikaelacaron/Basic-Car-Maintenance
// See LICENSE for license information.
//

import SwiftUI

protocol MaintenanceEventsFetcher {
func fetchEvents(for vehicle: Vehicle) -> [MaintenanceEvent]
}

struct ExportOptionsView: View {
@Environment(\.dismiss) var dismiss
@State private var selectedVehicle: Vehicle?
@State private var isShowingShareSheet = false
private let eventsFetcher: MaintenanceEventsFetcher
private let onExport: (URL) -> Void
private let vehicles: [Vehicle]

init(
vehicles: [Vehicle],
eventsFetcher: MaintenanceEventsFetcher,
onExport: @escaping (URL) -> Void
) {
self.vehicles = vehicles
self.eventsFetcher = eventsFetcher
self.onExport = onExport
self._selectedVehicle = State(initialValue: vehicles.first)
}

var body: some View {
NavigationView {
VStack(alignment: .leading, spacing: 16) {
Text("Select the vehicle you want to export the maintenance events for:")
.font(.headline)
.padding(.top, 20)

Picker("Select a Vehicle", selection: $selectedVehicle) {
ForEach(vehicles, id: \.id) { vehicle in
Text(vehicle.name)
.tag(vehicle as Vehicle?)
}
}
.pickerStyle(InlinePickerStyle())
}
.padding(.horizontal)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Export") {
if let selectedVehicle {
let events = eventsFetcher.fetchEvents(for: selectedVehicle)
let pdfGenerator = CarMaintenancePDFGenerator(
vehicleName: selectedVehicle.name,
events: events
)
if let pdfURL = pdfGenerator.generatePDF() {
onExport(pdfURL)
dismiss()
}
}
}
.disabled(selectedVehicle == nil)
}
}
}
}
}
Loading

0 comments on commit d244023

Please sign in to comment.