I have a GenerateExpenseReportViewController class to generate a PDF report based on an array of expenses retrieved from a DBManager. The PDF is generated using PDFKit and the contents of the report are laid out in a table format.
The table contains headers for "Amount", "Category", "Payment Type", and "Expense Date". The data for each expense is then inserted into a row below its respective header.
The code first grabs references to the expenses array and initializes a new PDFDocument, PDFPage, and PDFView. It then generates the table headers by calculating the size and location of each header cell.
Next, it iterates over the expenses array, generating a row for each expense. The data for each row is inserted into a cell using the same technique as the headers. If there are too many rows for a single PDFPage, the code will create a new one and continue adding rows.
Finally, the generated PDFDocument is set to the PDFView document property. The problem is header always sits on the bottom of the table, how to make it sit at the top?
import UIKit
import PDFKit
class GenerateExpenseReportViewController: UIViewController {
var db = DBManager()
var expenses = Array<Expense>()
let pdfView = PDFView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(pdfView)
expenses = db.read()
let newDocument = PDFDocument()
let page = PDFPage()
let tableWidth = page.bounds(for: .mediaBox).width - 2 * 50 // Subtracting the left and right margins
let tableHeight: CGFloat = 20
let tableRect = CGRect(x: 50, y: page.bounds(for: .mediaBox).height - 100 - CGFloat(expenses.count) * tableHeight, width: tableWidth, height: CGFloat(expenses.count) * tableHeight)
// Create table headers
let headers = ["Amount", "Category", "Payment Type", "Expense Date"]
let headerFont = UIFont.boldSystemFont(ofSize: 12)
let headerCellWidth = tableWidth / CGFloat(headers.count)
for (index, header) in headers.enumerated() {
let headerCellRect = CGRect(x: tableRect.origin.x + CGFloat(index) * headerCellWidth, y: tableRect.origin.y - tableHeight, width: headerCellWidth, height: tableHeight)
let headerAnnotation = PDFAnnotation(bounds: headerCellRect, forType: .freeText, withProperties: nil)
headerAnnotation.font = headerFont
headerAnnotation.alignment = .center
headerAnnotation.contents = header
headerAnnotation.color = UIColor.white
page.addAnnotation(headerAnnotation)
}
newDocument.insert(page, at: 0)
var currentPageIndex = 0
var currentRowIndex = expenses.count - 1 // Start from the last row
while currentRowIndex >= 0 {
let currentPage = newDocument.page(at: currentPageIndex)
let expense = expenses[currentRowIndex]
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "d MMM yy"
let expenseDateString = dateFormatter.string(from: expense.expenseDate)
let rowData = ["\(expense.amount)", expense.category, expense.paymentType, expenseDateString]
let rowFont = UIFont.systemFont(ofSize: 12)
let rowCellWidth = tableWidth / CGFloat(rowData.count)
for (index, data) in rowData.enumerated() {
let rowCellRect = CGRect(x: tableRect.origin.x + CGFloat(index) * rowCellWidth, y: tableRect.origin.y + CGFloat(expenses.count - currentRowIndex + 1) * tableHeight, width: rowCellWidth, height: tableHeight)
let rowAnnotation = PDFAnnotation(bounds: rowCellRect, forType: .freeText, withProperties: nil)
rowAnnotation.font = rowFont
rowAnnotation.alignment = .center
rowAnnotation.contents = data
rowAnnotation.color = UIColor.white
currentPage?.addAnnotation(rowAnnotation)
}
currentRowIndex -= 1
// Check if the current page is full
let availableHeight = page.bounds(for: .mediaBox).height - 250 - CGFloat(expenses.count + 1) * tableHeight
if availableHeight < tableHeight {
currentPageIndex += 1
currentRowIndex = expenses.count - 1
let newPage = PDFPage()
newDocument.insert(newPage, at: newDocument.pageCount)
}
}
pdfView.document = newDocument
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
pdfView.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height)
}
}