Search code examples
swiftpdfswiftuiios17

Problems with Positioning in PDF Rendering using Swift


I've been working on adding a print feature to an app I developed for myself. The app consists of a parent / child relationship. So in the print function I want to first print the name of the parent, and then print all the child rows as a grid.

Sizing a page of US Letter this means I have roughly 612 x 792 units to draw in. Each child consists of a view, that set a frame of roughly 1/3 the width of the view 1/5 of the heigh. This should give me approximately 15 children on the page. However; my code seems to print the entire set of data (both parent and child) on top of each other at the top of the page.

    @MainActor func render(viewsPerPage: Int) -> URL {
        let eventsArray: [Event] = events.map { $0 }
        let url = URL.documentsDirectory.appending(path: "\(recipient.wrappedFirstName)-\(recipient.wrappedLastName)-cards.pdf")
        var pageSize = CGRect(x: 0, y: 0, width: 612, height: 792)
        
        guard let pdfOutput = CGContext(url as CFURL, mediaBox: &pageSize, nil) else {
            return url
        }
        
        let numberOfPages = Int((events.count + viewsPerPage - 1) / viewsPerPage)   // Round to number of pages
        let xColumn = [0.0, 153.0, 306.0, 459.0]
        let yRow = [548.0, 413.0, 269.0, 115.0, 0.0]
        let viewsPerRow = 4
        let rowsPerPage = 5
        let spacing = 10.0
        
        // Note the page should be laid out as follows
        // Header Start on Row 792 to Row 692 (100 Pixels)
        // Body is a Grid of 143w X 134h PrintViews
        // Footer Starts on Row 0 to Row 20 (20 Pixels)
        
        for pageIndex in 0..<numberOfPages {
            pdfOutput.beginPDFPage(nil)
            let rendererTop = ImageRenderer(content: Color.red.frame(width: pageSize.width, height: 90))
            rendererTop.render { size, renderTop in
                // Go to Bottom Left of Page
                pdfOutput.move(to: CGPoint(x: 0.0, y: 0.0))
                // Translate to top Left with size of AddressView and Padding
                pdfOutput.translateBy(x: 0.0, y: pageSize.height - size.height - spacing)
                renderTop(pdfOutput)
                print("\n\nStarting page = \(pageIndex)")
            }
            
            let startIndex = pageIndex * viewsPerPage
            let endIndex = min(startIndex + viewsPerPage, eventsArray.count)
            
            for row in 0..<rowsPerPage {
                let yTranslation = yRow[row]
                
                for col in 0..<viewsPerRow {
                    let index = startIndex + row * viewsPerRow + col
                    if index < endIndex, let event = eventsArray[safe: index] {
                        let xTranslation = xColumn[col] // CGFloat(col) * (viewWidth + spacing)
                        let renderBody = ImageRenderer(content: Text("Event[\(index)] x=\(xColumn[col])/y=\(yRow[row] - 148)").frame(width: 134, height: 148).background(Color.blue))
                        
                        renderBody.render { size, renderBody in
                            pdfOutput.move(to: CGPoint(x: xColumn[col], y: yRow[row] - size.height))
                            renderBody(pdfOutput)
                            print("Event \(index) Position x= \(xColumn[col]) / y = \(yRow[row] - 148)")
                        }
                    }
                }
            }
            
            let renderBottom = ImageRenderer(content: Text("Page \((pageIndex + 1).formatted()) of \(numberOfPages.formatted())").frame(width: pageSize.width, height: 20).background(Color.yellow))
            pdfOutput.move(to: CGPoint(x: pageSize.width / 2 , y: 0))
            
            renderBottom.render { size, renderBottom in
                renderBottom(pdfOutput)
                print("\nEnding page = \(pageIndex)")
            }
            
            pdfOutput.endPDFPage()
        }
        
        pdfOutput.closePDF()
        return url
    }

Solution

  • The issues is incorrect usage of .move. This needs to use .translatesBy Using this means you have to remember were you were and then move the offset to a new location by .translatesBy Final code is here

    @MainActor func render(viewsPerPage: Int) -> URL {
            let eventsArray: [Event] = events.map { $0 }
            let url = URL.documentsDirectory.appending(path: "\(recipient.wrappedFirstName)-\(recipient.wrappedLastName)-cards.pdf")
            var pageSize = CGRect(x: 0, y: 0, width: 612, height: 792)
            
            guard let pdfOutput = CGContext(url as CFURL, mediaBox: &pageSize, nil) else {
                return url
            }
            
            let numberOfPages = Int((events.count + viewsPerPage - 1) / viewsPerPage)   // Round to number of pages
            let viewsPerRow = 4
            let rowsPerPage = 4
            let spacing = 10.0
            
            // Note the page should be laid out as follows
            // Header Start on Row 792 to Row 692 (100 Pixels)
            // Body is a Grid of 143w X 134h PrintViews
            // Footer Starts on Row 0 to Row 20 (20 Pixels)
            
            for pageIndex in 0..<numberOfPages {
                var currentX : Double = 0
                var currentY : Double = 0
                
                pdfOutput.beginPDFPage(nil)
                let rendererTop = ImageRenderer(content: AddressView(recipient: recipient))
                rendererTop.render { size, renderTop in
                    // Go to Bottom Left of Page
                    pdfOutput.move(to: CGPoint(x: 0.0, y: 0.0))
                    // Translate to top Left with size of AddressView and Padding
                    pdfOutput.translateBy(x: 0.0, y: pageSize.height - size.height - spacing)
                    currentY += pageSize.height - size.height - spacing
                    renderTop(pdfOutput)
                    print("\n\nStarting page = \(pageIndex)")
                }
                print("Header - currentX = \(currentX), currentY = \(currentY)")
                
                let startIndex = pageIndex * viewsPerPage
                let endIndex = min(startIndex + viewsPerPage, eventsArray.count)
                pdfOutput.translateBy(x: spacing / 2, y: -160)
                
                for row in 0..<rowsPerPage {
                    for col in 0..<viewsPerRow {
                        let index = startIndex + row * viewsPerRow + col
                        if index < endIndex, let event = eventsArray[safe: index] {
                            let renderBody = ImageRenderer(content: PrintView(event: event))
                            renderBody.render { size, renderBody in
                                renderBody(pdfOutput)
                                pdfOutput.translateBy(x: 144, y: 0) // (to: CGPoint(x: xColumn[col], y: yRow[row] - size.height))
                                currentX += size.width
                            }
                        }
                    }
                    pdfOutput.translateBy(x: -pageSize.width + 39.5, y: -153)
                    currentY -= 153
                    currentX = -pageSize.width + 39.5
                    print("Body - currentX = \(currentX), currentY = \(currentY)")
                }
                
                let renderBottom = ImageRenderer(
                    content:
                        Text("Page \((pageIndex + 1).formatted()) of \(numberOfPages.formatted())").frame(width: pageSize.width ,height: 20)
                )
                pdfOutput.translateBy(x: -pageSize.width + 39.5, y: -currentY)
                print("Footer - currentX = \(currentX), currentY = \(currentY)")
                renderBottom.render { size, renderBottom in
                    renderBottom(pdfOutput)
                    print("\nEnding page = \(pageIndex), size.width =\(size.width)  , size.height=\(size.height)")
                }
                pdfOutput.endPDFPage()
            }
            pdfOutput.closePDF()
            return url
        }