ios – SwiftUI, Converting an Excel type view into PDF?

0
57


I have a pretty tricky problem which I’m trying to figure out so be warned. So to start off, I have a view called ExcelView in which its able to create an excel type of view. In the ExcelView it it has locations on one axis and supply names on another. This view allows the user to be able to scroll through all four cardinal directions and it displays the qty of supplies per location, for the moment I left all the cells as N/A. Now, I currently made a file called “Extensions” which is responsible for creating a PDF of the current screen and saving it. However, since I allow the Excel View to be able to move in 4 directions, whenever the current screen is made into a PDF, it will only convert the current screen to a PDF. I want to be able to have the whole excel view on one page. A good example of what I want can be see from the picture I attached. Now, originally I tried messing with the frame of the PDF to see if I could “Mold” it to the excel view, I also tried created a function called getsafeArea to see if I could eliminate the safe area on specific parts of the screen, but I ended up getting confused. Let me know if you have any questions. Thanks Again.

ExcelView:

import SwiftUI
@available(iOS 16.0, *)
struct ExcelView: View {
    @EnvironmentObject var dataManager: DataManager
    @Environment(\.verticalSizeClass) var heightSizeClass: UserInterfaceSizeClass?
    @Environment(\.horizontalSizeClass) var widthSizeClass: UserInterfaceSizeClass?
   
    let columns = 5
    let rows = 20
    
    @State private var offset = CGPoint.zero
    var excelNames = ["Stassney","SM", "Cameron", "Lamar", "Riverside"]
    
    var excelSuppliesNames = ["Pencil", "Pen", "Highlighter", "Fine Sharpie Pen", "Big Sharpie Pen", "Post-it", "Finger Tip Moist.", "Regular Paper Clip", "Big Paper Clip", "Regular Tape", "Packaging Tape", "Regular Rubber Bands", "Big Rubber Bands", "Regular Staples", "Big Staples", "Paper", "Yellow Filing Folders", "Hand Sanitizer", "Wireless Mouse", "Wireless Keyboard"]
    
    var body: some View {
            VStack(alignment: .leading, spacing: 0){
                buttonHeader

                HStack(alignment: .top, spacing: 0) {
                    VStack(alignment: .leading, spacing: 0) {
                        
                        // empty corner
                        Color.clear.frame(width: 70, height: 50)
                        ScrollView([.vertical]) {
                            rowsHeader
                                .offset(y: offset.y)
                        }
                        .disabled(true)
                    }
                    VStack(alignment: .leading, spacing: 0) {
                        ScrollView([.horizontal]) {
                            colsHeader
                                .offset(x: offset.x)
                        }
                        .disabled(true)

                        table
                            .coordinateSpace(name: "scroll")
                    }
        
                }
            }
            // 1st Option: landscape, 2nd Option: portrait
            .frame(maxWidth: heightSizeClass == .compact ? UIScreen.screenwidth + 335 : UIScreen.screenwidth, maxHeight: heightSizeClass == .compact ? UIScreen.screenHeight / 2.3 : UIScreen.screenHeight)
    }
    
    var buttonHeader: some View {
            VStack {
                HStack {
                    Button {
                        exportPDF {
                            self
                                .environmentObject(dataManager)
                        } completion: { status, url in
                            if let url = url, status {
                                dataManager.PDFUrl = url
                                dataManager.showShareSheet.toggle()
                            }
                            else {
                                print("Failed to produce PDF")
                            }
                        }
                    } label: {
                        Image(systemName: "square.and.arrow.up.fill")
                            .font(.system(size: 45))
                            .foregroundColor(Color.black.opacity(0.7))
                    }
                }
            }
            .sheet(isPresented: $dataManager.showShareSheet) {
                dataManager.PDFUrl = nil
            } content: {
                if let PDFUrl = dataManager.PDFUrl {
                    ShareSheet(urls: [PDFUrl])
                }
            }
        
    }
    
    var colsHeader: some View {
        HStack(alignment: .center, spacing: 0) {
            //The number of col/row must match the # of items in array
            ForEach(0..<columns) { col in
                Text(excelNames[col])
                    .foregroundColor(.black)
                    .font(.system(size: 15))
                    .frame(width: 100, height: 50)
                    .border(Color.blue)
            }
        }
    }
    
    var rowsHeader: some View {
        VStack(alignment: .leading, spacing: 0) {
            ForEach(0..<rows) { row in
                Text("\(excelSuppliesNames[row] )")
                    .foregroundColor(.black)
                    .font(.caption)
                    .frame(width: 100, height: 50)
                    .border(Color.blue)
            }
        }
    }
    
    var table: some View {
        ScrollView([.vertical, .horizontal]) {
            VStack(alignment: .leading, spacing: 0) {
                ForEach(0..<rows) { row in
                    HStack(alignment: .top, spacing: 0) {
                        ForEach(0..<columns) { col in
                            Text("N/A")
                                .font(.system(size: 12))
                                .frame(width: 100, height: 50)
                                .border(Color.blue)
                        }
                    }
                }
            }
            .background( GeometryReader { geo in
                Color.clear
                    .preference(key: ViewOffsetKey.self, value: geo.frame(in: .named("scroll")).origin)
            })
            .onPreferenceChange(ViewOffsetKey.self) { value in
                //print("offset >> \(value)")
                offset = value
            }
        }
    }
}


struct ViewOffsetKey: PreferenceKey {
    typealias Value = CGPoint
    static var defaultValue = CGPoint.zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value.x += nextValue().x
        value.y += nextValue().y
    }
}

@available(iOS 16.0, *)
struct ExcelView_Previews: PreviewProvider {
    static var previews: some View {
        ExcelView().environmentObject(DataManager())
    }
}

//Share Sheet
struct ShareSheet: UIViewControllerRepresentable {
    
    var urls: [Any]
    
    func makeUIViewController(context: Context) -> UIActivityViewController {
        let controller = UIActivityViewController(activityItems: urls, applicationActivities: nil)
        
        return controller
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
    }
}

extension UIScreen {
    static let screenwidth = UIScreen.main.bounds.size.width
    static let screenHeight = UIScreen.main.bounds.size.height
    static let screenSize = UIScreen.main.bounds.size
    
}

Extensions:

import SwiftUI

extension View {
    //Extracting Views Height and width with the help of Hosting Controller and ScrollView
    func converToScrollView<Content: View>(@ViewBuilder content: @escaping ()->Content) -> UIScrollView {
        
        let scrollView = UIScrollView()
        
        // Convertin SwiftUI View to UIKit View
        let hostingController = UIHostingController(rootView: content()).view!
        hostingController.translatesAutoresizingMaskIntoConstraints = false
        
        //Constraints
        let constraints = [
            
            hostingController.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            hostingController.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            hostingController.topAnchor.constraint(equalTo: scrollView.topAnchor),
            hostingController.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            
            //Width Anchor
            hostingController.widthAnchor.constraint(equalToConstant: screenBounds().width)
        ]
        scrollView.addSubview(hostingController)
        scrollView.addConstraints(constraints)
        scrollView.layoutIfNeeded()
        
        return scrollView
    }
    
    //Export to PDF
    //Completion Handler will send Status and URL
    func exportPDF<Content: View>(@ViewBuilder content: @escaping ()->Content, completion: @escaping (Bool, URL?)->()) {
        //Temp URL
        let documentDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
        //To Generate New File when ever its generated
        let outputFileURL = documentDirectory.appendingPathComponent("PDFName \(UUID().uuidString).pdf")
        
        let pdfView = converToScrollView {
            content()
        }
        
        pdfView.tag = 1009
        let size = pdfView.contentSize
        //Removing Safe Area top Value
        pdfView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
        
        
        //Attaching to Root View and redering the pdf
        getRootController().view.insertSubview(pdfView, at: 0)
        
        //Rendering PDF
        let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: size.width, height: size.height))

        do {
            try renderer.writePDF(to: outputFileURL, withActions: { context in
                context.beginPage()
                pdfView.layer.render(in: context.cgContext)
            })
            completion(true , outputFileURL)
        }
        catch {
            completion(false, nil)
            print(error.localizedDescription)
        }
        
        // Removing the added View
        getRootController().view.subviews.forEach { view in
            if view.tag == 1009 {
                print("Removed")
                view.removeFromSuperview()
            }
                
        }


    }
    
    func screenBounds() -> CGRect {
        return UIScreen.main.bounds
    }
    
    func getRootController()->UIViewController {
        guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
            return .init()
        }
        
        guard let root = screen.windows.first?.rootViewController else {
            return .init()
        }
        
        return root
    }
    
    
    func getSafeArea() -> UIEdgeInsets{
        guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
            return .zero
        }
        
        guard let safeArea = screen.windows.first?.safeAreaInsets else {
            return .zero
        }
        
        return safeArea
    }
    
}

DataManager:

import Foundation
class DataManager: ObservableObject {
    //PDF Properties
    @Published var PDFUrl: URL?
    @Published var showShareSheet: Bool = false
}

[Goal of project][1]
[1]: https://i.stack.imgur.com/H0Onr.png