Reputation: 1285
I'm building an identity app which is stored all locally within the app on device.
Part of the app is the creation of IdentityCard
for each user - with the standard things, name, image, position.
The size of the card is a standard credit card size: let cardSize: CGSize = .init(width: 153.8, height: 242.1)
in the vertical position.
I want to create a "generate PDF" button so that each user's ID card will be printed into a PDF. I have the ability to select the paper size (A4, Letter, or custom - where they can select their own width, height, and unit).
At the moment I have this to generate the PDF, but I am running into a few issues:
.sheet
import SwiftUI
import PDFKit
class PDFDataManager {
static let shared = PDFDataManager()
private init() {}
struct Item<Content : View> {
let views: [Content]
let width: CGFloat
let height: CGFloat
}
func generate<Content : View>(
from item: Item<Content>,
paper: PageSize = .a4,
margin: Double = 36,
bleed: Double = 10
) -> PDFDocument {
let usablePageSize: CGSize = .init(
width: paper.size.width - margin - (2 * bleed),
height: paper.size.height - margin - (2 * bleed)
)
let itemSize: CGSize = .init(
width: item.width,
height: item.height
)
let maxRows = Int(floor(usablePageSize.height / itemSize.height))
let maxCols = Int(floor(usablePageSize.width / itemSize.width))
let pdfDocument = PDFDocument()
let pageSize = CGRect(
x: 0, y: 0,
width: paper.size.width,
height: paper.size.height
)
var currentItem = 0
var currentRow = 0
var currentCol = 0
DispatchQueue.global(qos: .userInitiated).async {
while currentItem < item.views.count {
let pdfPage = PDFPage()
pdfPage.setBounds(pageSize, for: .trimBox)
let pdfView = PDFView(frame: pageSize)
pdfView.autoScales = true
pdfView.displayDirection = .vertical
pdfView.displayMode = .singlePageContinuous
pdfView.document = pdfDocument
pdfView.pageBreakMargins = .init(top: 0, left: 0, bottom: 0, right: 0)
while currentItem < item.views.count && currentRow <= maxRows {
autoreleasepool {
DispatchQueue.main.async {
if currentItem < item.views.count {
let itemView = UIHostingController(rootView: item.views[currentItem])
let x = CGFloat(currentCol) * (itemSize.width + (bleed * 2))
let y = CGFloat(currentRow) * (itemSize.height + (bleed * 2))
let itemRect = CGRect(
x: x, y: y,
width: itemSize.width,
height: itemSize.height
)
itemView.view.frame = itemRect
pdfView.addSubview(itemView.view)
currentCol += 1
if currentCol >= maxCols {
currentCol = 0
currentRow += 1
}
currentItem += 1
}
}
}
}
DispatchQueue.main.async {
pdfDocument.insert(pdfPage, at: pdfDocument.pageCount)
}
}
}
return pdfDocument
}
}
Once I would have that working I would be able to share the PDF, print the PDF, or export the PDF. However, at the moment I cant do any of that and I'm kind of lost of where to go from here.
I am targeting iOS/iPadOS 15 and above.
Upvotes: 2
Views: 479
Reputation: 1285
This question was a bit harder to answer than what I thought, but I did manage to get it solved.
I do want to flag that there would probably be smarter, more efficient, and better implementation - but this worked for me.
I started with this framework which helped me build the PDF. It is fairly fleshed out and simple to use.
Using that, I had this code as my even spreading across the pages:
func generatePDF(_ items: [Item]) {
let pageSize: PageSize = .a4
let pageMargin: UIEdgeInsets = .equal(20)
let imageSize = CGSize(width: 160, height: 250)
let pdf = PDFMKit(pageSize: pageSize, pageMargin: pageMargin)
let usablePageSize = CGSize(
width: pageSize.size.width - pageMargin.left - pageMargin.right,
height: pageSize.size.height - pageMargin.top - pageMargin.bottom
)
// -- get the max rows and columns per page
let maxColumnsPerPage = Int(usablePageSize.width / imageSize.width)
let maxRowsPerPage = Int(usablePageSize.height / imageSize.height)
let maxItemsPerPage = maxRowsPerPage * maxColumnsPerPage
// -- get the total item count
let totalItems = users.count
// -- create a chunked array
let pageChunks = users.chunked(into: maxItemsPerPage)
// -- run the operation in the background
DispatchQueue.global(qos: .background).async {
// -- loop over the chunks
// -- this should be all the items per page
for (pageIndex, pageChunk) in pageChunks.enumerated() {
// -- rechunk the page items into the rows
let pageItemChunks = pageChunk.chunked(into: maxColumnsPerPage)
// -- loop over the rows
for pageItemChunk in pageItemChunks {
// -- get an array of the images
let images = pageItemChunk.map { user in
UIImage(
data: user.cardImageData ?? .init(),
scale: .screenScale
) ?? .init()
}
// -- start the horizontal alignment
pdf.beginHorizontalArrangement()
// -- add in the images
for image in images {
pdf.addImage(image)
}
// -- end the horizontal alignment
pdf.endHorizontalArrangement()
// -- update the progress bar
if let lastItem = pageItemChunk.last,
let lastIndex = users.firstIndex(of: lastItem) {
DispatchQueue.main.async {
progress(Double(lastIndex + 1) / Double(totalItems))
}
}
}
// -- create a new page at the end
if pageIndex < pageChunks.count - 1 {
pdf.beginNewPage()
}
}
// -- update the main thread data
DispatchQueue.main.async {
let title = UUID().uuidString
let pdfData = pdf.generate(title,
author: "Author Name",
subject: Bundle.main.displayName)
do {
let savedURL = try self.savePDF(pdfData, name: title)
completion(.success(savedURL))
} catch {
completion(.failure(.unableToGenerate))
}
}
}
}
}
My implementation is similar but I do have some customisations to the source code to make it fully work for me. But the above should help anyone who needs it!
Upvotes: 0