0

Summary:

I have a list loaded from an API. Each list item have a button. On click of button, a unique ID associated with the list item is sent to server which in response provides a pdf directly there is no other response just a pdf file, the api is like :

http://myhost/api/DownloadPDF/uniqueID=67198287_239878092_8089

I have created the list and also able to download the pdf in documentDirectory by calling download task. However, I am unable to open the pdf automatically in app itself after downloading. I have created DisplayPDF struct which uses PDFKit to display as follows:

    struct DisplayPDF: View {
          var url:URL
              var body:some View
                {
                   PDFKitRepresentedView(url)
                }
               }

          struct PDFKitRepresentedView: UIViewRepresentable{
          func updateUIView(_ uiView: UIView, context: 
                 UIViewRepresentableContext<PDFKitRepresentedView>) {
                            }
              let url: URL
              init(_ url:URL)
           {
              self.url = url
           }
 func makeUIView(context:                
         UIViewRepresentableContext<PDFKitRepresentedView>) -> 
         PDFKitRepresentedView.UIViewType {
         let pdfView = PDFView()
         pdfView.document = PDFDocument(url: self.url)
         pdfView.autoScales = true
         return pdfView
         }}

I need to pass the url into the above struct. The url can be the saved location or API directly. However, the url is not passed when the DisplayPDF view is called.

What I have tried so far 1> Pass the DisplayPDF into navigationlink in ReportList(where list is loaded) struct and than either call getFile func in onAppear in DisplayPDF struct or ReportRow struct. 2> Call getFile() on ReportRow in onAppear and pass the url in DisplayPDF() there. 3> Call getFile() on DisplayPDF() onAppear and pass the url there 4> Also tried, sheet method blank sheet pops up

All failed, no value is sent to DisplayPDF(url) the moment it is called from any of the listed method.

ReportList struct

     import SwiftUI

     struct ReportList: View {

   @ObservedObject var reportLink : ReportViewModel
 
   var body: some View {
    

   List{
          ForEach(reportLink.trackReport)
                       {report in
                           VStack {
                                        ReportRow(report: report)
                                   }
                                if(reportLink.trackReport.isEmpty)
                                {
                                    Text("No Report Found")
                                    .foregroundColor(.accentColor)
                                    .fontWeight(.semibold)
                                }
                            }
         
                        }
                }
        }

ReportRow struct:

  struct ReportRow: View {
  var report : ReportResponse
  @StateObject var pdfDownload = PDFDownload()

  var body: some View {
        VStack{
            HStack{
                Text(report.name)
                    .font(.system(size: 16))
                    .foregroundColor(.black)
                    .padding(.bottom,1)
                    .padding(.top,1)
                }.frame(maxWidth: .infinity, alignment: .leading)
            
            HStack{
                    Text("P.Id:")
                        .foregroundColor(.black)
                        .font(.system(size: 14))
                    Text(report.patientID)
                        .foregroundColor(.purple)
                        .font(.system(size: 14))
            
        Spacer()
    
                    Button(action: {
                        
                        pdfDownload.uniqueReportId = report.uniqueID
                        pdfDownload.patientName = report.name
                        pdfDownload.getFile()                            
                    }, label:
                    {
                        Text("\(report.status)")
                        .foregroundColor(.blue)
                        .font(.system(size: 14))
                        .padding(.trailing,2)
                    }).frame(maxWidth: .infinity, alignment: .trailing)
                }
          }
        }}

I have made this PDFDownload model in which openURL is declared a published var which should provide updated url to a view(like DisplayPDF() view):

  class PDFDownload : UIViewController, ObservableObject
       {
           @Published var uniqueReportId:String = String()
           @Published var patientName:String = String()
           @Published var isNavigate:Bool = false
           @Published var openURL:URL = URL(fileURLWithPath: "")
               
func getFile()
{
 var urlComponents = URLComponents()
 urlComponents.scheme = "http"
 urlComponents.host = "myHost"
 urlComponents.port = 80
 urlComponents.path = "/api/Reports/DownloadReport"
 urlComponents.queryItems = [URLQueryItem(name: "uniquePackageId", 
  value: uniqueReportId)]
 let url = urlComponents.url

    print(url?.absoluteString)
    
    let downloadTask = URLSession.shared.downloadTask(with: url!)
    {
        urlOrNil, responseOrNil, errorOrNil in
        guard let fileURL = urlOrNil else {return}
                do{
                let documentURL = try FileManager.default.url(for: 
              .documentDirectory, in: .userDomainMask, appropriateFor: 
               nil, create: false)
                let savedURL = documentURL.appendingPathComponent("\ 
                (self.patientName)_\(UUID().uuidString).pdf")
                print(savedURL)
                try FileManager.default.moveItem(at: fileURL, to: 
                 savedURL)
                          
                   DispatchQueue.main.async {
                       self.openURL = savedURL
                                
                          }
                      }
                    catch{
                          print("Error while writting")
                      }
                      }                 
                 downloadTask.resume()
            }}
           

So what is the correct way of solving this problem that the correct URL can be passed to DisplayPDF() view. Extra: ReportResponse model:

struct DownReport : Codable, Identifiable {
let id = UUID()
let success : Bool
let message : String
let reportResponse : [ReportResponse]

enum CodingKeys: String, CodingKey{
    case success = "IsSuccess"
    case message = "Message"
    case reportResponse = "ResponseData"
}}


    struct ReportResponse : Codable, Identifiable {
    var id:String {uniqueID}
let patientID : String
let name : String
let status : String
let crmNo : String?
let recordDate : String
let uniqueID : String
let testCount : Int

enum CodingKeys: String, CodingKey {
    case patientID = "PatientId"
    case name = "Name"
    case status = "Status"
    case crmNo = "CrmNo"
    case recordDate = "RecordDate"
    case uniqueID = "UniquePackageId"
    case testCount = "NoOfTests"
}

}

The above response is from POST request which is sent to generate list. To get pdf only unique id as Query is sent as I have posted on top.

The above structure successfully downloads the file but fail to open the file automatically. How to do that?

tintin
  • 335
  • 2
  • 8
  • Which `DisplayPDF()` view are you using? – Neck May 02 '22 at 09:14
  • Display pdf view must have 2 states : 1:pdf downloading-> display test indicating download; 2: pdf downloaded -> display contents. This state is define onAppear which should start pdf download if necessary. At the end of download, the state should change to downloaded and then the display pdf view could display the pdf. Look at async/task/completion handler. – Ptit Xav May 02 '22 at 10:34
  • @PtitXav I am not getting it....i have tried onAppear func too file downloads and blank sheet is displayed : button (:action :{ self.showPDF = true}...}.sheet(isPresented:$showPDF){ DisplayPDF(report:report, urlForD:pdfDownload.openURL).....this urlForD:pdfDownload.openURL is blank at first since it is not updated in model and it is passed to DisplaypDF so blank screen issue is how to pass url to DisplayPDF – tintin May 02 '22 at 12:00
  • @Neck i have tried in two ways pls see the Q – tintin May 02 '22 at 12:02
  • You just define url but I see no download action. – Ptit Xav May 02 '22 at 12:53
  • @PtitXav pls look updated the download module this getFile func can be used anywhere – tintin May 02 '22 at 13:02
  • You have different object of type Pdfdownload . Why one in Row and one in Display and one in report. If this are related some should be binding . – Ptit Xav May 02 '22 at 13:25
  • The different part of code you are showing does not seem coherent. You mix UIKit ViewController, ViewRepresentable and SwiftUI. You should try to simply to show a minimum reproducible example. You need to define what is your model and what you want to show. Which are the action : you have a list with row contents containing button and being at the same time navigation link to a view which may not display something that is not loaded and not connected to any state/binding/observed data. – Ptit Xav May 02 '22 at 15:23

2 Answers2

1

Updated Answer with your update question: the row will update when the file is downloaded, it will then be a navigation link to display pdf

struct DownReport : Codable, Identifiable {
    let id = UUID()
    let success : Bool
    let message : String
    let reportResponse : [ReportResponse]
    
    enum CodingKeys: String, CodingKey{
        case success = "IsSuccess"
        case message = "Message"
        case reportResponse = "ResponseData"
    }
}

struct ReportResponse : Codable, Identifiable {
    var id:String {uniqueID}
    let patientID : String
    let name : String
    let status : String
    let crmNo : String?
    let recordDate : String
    let uniqueID : String
    let testCount : Int
    // This URL is only set when report has been downloaded and it does not need to be part of the response
    var localFileUrl: URL?
    
    enum CodingKeys: String, CodingKey {
        case patientID = "PatientId"
        case name = "Name"
        case status = "Status"
        case crmNo = "CrmNo"
        case recordDate = "RecordDate"
        case uniqueID = "UniquePackageId"
        case testCount = "NoOfTests"
    }
}

class ReportViewModel: ObservableObject {
    // some dummy value
    @Published var trackReport: [ReportResponse] = [ReportResponse(patientID: "0001", name: "patient-1", status: "status", crmNo: nil, recordDate: "today", uniqueID: "010001", testCount: 1),ReportResponse(patientID: "0002", name: "patient-2", status: "status", crmNo: nil, recordDate: "today", uniqueID: "010002", testCount: 3)]
    
    // Update the report in the array ussing report unique ID
    func updateReport(withId reportId: String, url: URL) {
        guard let index = trackReport.firstIndex(where: {$0.uniqueID == reportId}) else {return}
        var report = trackReport[index]
        report.localFileUrl = url
        trackReport[index] = report
    }
}

// no need for any observation on pdfDownload object as the completion will do the jobs
class PDFDownload {
    var uniqueReportId: String
    var patientName: String
    init(uniqueReportId:String, patientName:String) {
        self.uniqueReportId = uniqueReportId
        self.patientName = patientName
    }
    
    func getFile(completion: @escaping (URL) -> ())
    {
        var urlComponents = URLComponents()
        urlComponents.scheme = "http"
        urlComponents.host = "myHost"
        urlComponents.port = 80
        urlComponents.path = "/api/Reports/DownloadReport"
        urlComponents.queryItems = [URLQueryItem(name: "uniquePackageId",value: uniqueReportId)]
        let url = urlComponents.url
        
        //        print(url?.absoluteString)
        
        let downloadTask = URLSession.shared.downloadTask(with: url!)
        {
            urlOrNil, responseOrNil, errorOrNil in
            // Simulation of downloading
            sleep(3)
            DispatchQueue.main.async {
                completion(URL(fileURLWithPath: "report\(self.patientName).pdf"))
            }
            guard let fileURL = urlOrNil else {return}
            do{
                let documentURL = try FileManager.default.url(for:
                        .documentDirectory, in: .userDomainMask, appropriateFor:
                                                                nil, create: false)
                let savedURL = documentURL.appendingPathComponent("\(self.patientName)_\(UUID().uuidString).pdf")
                print(savedURL)
                try FileManager.default.moveItem(at: fileURL, to:
                                                    savedURL)
                
                // Update the report url
                DispatchQueue.main.async {
                    completion(savedURL)
                }
            }
            catch{
                print("Error while writting")
            }
        }
        downloadTask.resume()
    }
}

struct ReportList: View {
    
    @ObservedObject var reportLink : ReportViewModel
    
    var body: some View {
        NavigationView{
            List{
                ForEach(reportLink.trackReport) { report in
                    if let url = report.localFileUrl {
                        NavigationLink {
                            DisplayPDF(url: url)
                        } label: {
                            Text(report.name)
                        }
                        
                    } else {
                        ReportRow(report: report, updateReport: updateReport)
                    }
                }
                // Moved out of ForEach
                if(reportLink.trackReport.isEmpty)
                {
                    Text("No Report Found")
                        .foregroundColor(.accentColor)
                        .fontWeight(.semibold)
                }
                
            }
        }
    }
    func updateReport(withId reportId: String, url: URL) {
        reportLink.updateReport(withId: reportId, url: url)
    }
}

struct ReportRow: View {
    var report: ReportResponse
    var updateReport: (String, URL) -> ()
    
    var body: some View {
        VStack{
            HStack{
                Text(report.name)
                    .font(.system(size: 16))
                    .foregroundColor(.black)
                    .padding(.bottom,1)
                    .padding(.top,1)
            }.frame(maxWidth: .infinity, alignment: .leading)
            
            HStack{
                Text("P.Id:")
                    .foregroundColor(.black)
                    .font(.system(size: 14))
                Text(report.patientID)
                    .foregroundColor(.purple)
                    .font(.system(size: 14))
                
                Spacer()
                
                Button(action: {
                    let pdfDownload = PDFDownload(uniqueReportId: report.uniqueID, patientName: report.name)
                    pdfDownload.getFile(completion: updateReportUrl)
                }, label:
                        {
                    Text("\(report.status)")
                        .foregroundColor(.blue)
                        .font(.system(size: 14))
                        .padding(.trailing,2)
                }).frame(maxWidth: .infinity, alignment: .trailing)
            }
        }
    }
    
    func updateReportUrl(url: URL) {
        updateReport(report.uniqueID, url)
    }
}

struct DisplayPDF: View {
    var url:URL
    var body:some View
    {
        // Stub as I can not download
        Text(url.absoluteString)
//        PDFKitRepresentedView(url)
    }
}

struct PDFKitRepresentedView: UIViewRepresentable{
    func updateUIView(_ uiView: UIView, context:
                      UIViewRepresentableContext<PDFKitRepresentedView>) {
    }
    let url: URL
    init(_ url:URL)
    {
        self.url = url
    }
    func makeUIView(context:
                    UIViewRepresentableContext<PDFKitRepresentedView>) ->
    PDFKitRepresentedView.UIViewType {
        let pdfView = PDFView()
        pdfView.document = PDFDocument(url: self.url)
        pdfView.autoScales = true
        return pdfView
    }
    
}
Ptit Xav
  • 3,006
  • 2
  • 6
  • 15
  • thanks for your time...i also thought on the same line as you posted but the issue is in Report Response pls understand the flow : user select an option to load list....from report response (API) list is loaded here is the problem and catch there is no member url in Report Response and the member status is only one value View Report......than from list user select an item and pdf is loaded...the unique id associated with the item(available in ReportResponse) is sent to API as GET request the response is a pdf file.. – tintin May 03 '22 at 05:54
  • .it is like common scenario on net we click a link and pdf is downloaded.....I am able to download the file but cant open automatically ie file should open after downloading....i am updating the entire Q ...updated the entire post upto the point where I am done with downloading file...tried many ways to open file automatically but no success so far.....pls look – tintin May 03 '22 at 05:55
  • nice soln its working as expected......i like this row changing thing after download is finished.....thank you again....both yours and workingUkraine soln are working fine... – tintin May 04 '22 at 11:57
  • @tintin : look at Stanford cs193p for good explanations of Swift, SwitfUI. – Ptit Xav May 04 '22 at 12:55
  • sure I will. Thanks for the link. – tintin May 04 '22 at 17:59
1

Here is some sample code that shows how to download a pdf document (wikipedia), copy it to a local file, and display it on the screen by passing the savedURL to the View. You should be able to adapt the sample code for your purpose.

import Foundation
import SwiftUI
import PDFKit

    struct ContentView: View {
    @StateObject var downloader = PDFDownload()
    
    var body: some View {
        VStack (spacing: 30) {
            Button("download1", action: {
                downloader.patientName = "patient-1"
                downloader.uniqueReportId = "astwiki-Homo_heidelbergensis-20200728.pdf/astwiki-Homo_heidelbergensis-20200728.pdf"
                downloader.getFile()
            }).buttonStyle(.bordered)
            
            Button("download2", action: {
                downloader.patientName = "patient-2"
                downloader.uniqueReportId = "rowiki-Biban_european-20200728.pdf/rowiki-Biban_european-20200728.pdf"
                downloader.getFile()
            }).buttonStyle(.bordered)
            
            if downloader.isDownloading { ProgressView("downloading ...") }
        }
        .fullScreenCover(item: $downloader.openURL) { siteUrl in
            DisplayPDF(url: siteUrl.url)
        }
    }
}

struct DisplayPDF: View {
    @Environment(\.dismiss) var dismiss
    let url: URL
    
    var body:some View {
        VStack {
            #if targetEnvironment(macCatalyst)
              Button("Done", action: {dismiss()})
            #endif
            PDFViewer(url: url)
        }
    }
}

struct PDFViewer: UIViewRepresentable {
    let url: URL
    
    func makeUIView(context: Context) -> PDFView {
        let pdfView = PDFView()
        pdfView.document = PDFDocument(url: url)
        pdfView.autoScales = true
        return pdfView
    }
    
    func updateUIView(_ uiView: PDFView, context: Context) {  }
}

class PDFDownload : ObservableObject {
    
    @Published var uniqueReportId = ""
    @Published var patientName = ""
    @Published var isNavigate = false
    @Published var openURL: SiteURL?
    @Published var isDownloading = false
    
    func getFile() {
        isDownloading = true
        
        var urlComponents = URLComponents()
        urlComponents.scheme = "https"
        urlComponents.host = "ia803207.us.archive.org"
        urlComponents.path = "/0/items/\(uniqueReportId)" // <-- just for testing
      //  urlComponents.port = 80
      //  urlComponents.queryItems = [URLQueryItem(name: "uniquePackageId", value: uniqueReportId)]
        
        guard let url = urlComponents.url else {return}
        
        let downloadTask = URLSession.shared.downloadTask(with: url) { urlOrNil, responseOrNil, errorOrNil in
            guard let fileURL = urlOrNil else { return }
            do {
                let documentURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
                let savedURL = documentURL.appendingPathComponent("\(self.patientName)_\(UUID().uuidString).pdf")
                try FileManager.default.moveItem(at: fileURL, to: savedURL)
                DispatchQueue.main.async {
                    self.openURL = SiteURL(url: savedURL)
                    self.isDownloading = false
                }
            }
            catch {
                print("Error \(error)")
            }
        }
        downloadTask.resume()
    }
}

struct SiteURL: Identifiable {
    let id = UUID()
    var url: URL
}
  • got it :)....your previous soln was working fine this one too..(was bit busy so late reply).....thank you very much was stuck in this madly...... – tintin May 04 '22 at 11:55