0

In my app I have an option to print delivery note. When I tap this option, I receive a response from server with a url (url of a PDF file). As soon as the response is received, the app navigate to a screen which contains a button to perform a printing action. Everything works ok until here. But when I tap the print button, the view gets frozen. I receive warning, that it is a synchronous url loading which causes app's ui unresponsive. It suggests to use some Asynchronous url loading like URLSession. But I have no idea how to use it in this case. Any help would be appreciated. I have provided code sample below.

public enum PrintingResult {
  case success
  case failure(Error)
  case userCancelled
}

public func presentPrintInteractionController(url: URL?, jobName: String? = nil, completion: ((PrintingResult) -> Void)? = nil) {
  let printController = UIPrintInteractionController()
  let printInfo = UIPrintInfo.printInfo()
  if let jobName = jobName {
    printInfo.jobName = jobName
  }
  printController.printInfo = printInfo

  if let url = url {
    printController.printingItem = url
  }
  printController.present(animated: true) { _, completed, error in
    guard let completion = completion else { return }
    if completed {
        completion(.success)
    } else {
        if let error = error {
            completion(.failure(error))
        } else {
            completion(.userCancelled)
        }
    }
  }
}

View:

import SwiftUI

struct ReportView: View {
   var reportUrl: String

   init(reportUrl: String) {
      self.reportUrl = reportUrl
   }

   var body: some View {
      VStack {
          Button {
            presentPrintInteractionController(url: URL(string: reportUrl), jobName: "Print a pdf report") { result in
                switch result {
                case .success:
                    print("Print Successful")
                case .failure(let error):
                    print("Error: \(error)")
                case .userCancelled:
                    print("Printing job cancelled.")
                }
            }
        } label: {
            Text("Print")
        }
     }
  }
}

struct ReportView_Previews: PreviewProvider {
   static var previews: some View {
       ReportView(reportUrl: "")
   }
}

This is the warning

Shawkath Srijon
  • 739
  • 1
  • 8
  • 17
  • @ArashEtemad I tried wrapping printController.present closer with DispatchQueue.main.async {}. But it is still same. Doesn't work. – Shawkath Srijon Mar 19 '23 at 06:55
  • Does this answer your question? [In Swift how to call method with parameters on GCD main thread?](https://stackoverflow.com/questions/24985716/in-swift-how-to-call-method-with-parameters-on-gcd-main-thread) – Max Play Mar 20 '23 at 07:43

1 Answers1

1

It seems like UIPrintInteractionController is trying to fetch data from the remote URL synchronously. One way to solve this would be to fetch the data asynchronously yourself, and the give the Data to the UIPrintInteractionController.

The basic idea is:

URLSession.shared.dataTask(with: URLRequest(url: url)) { data, response, error in
    // do some error handling...
    
    printController.printingItem = data
    
    printController.present(animated: true) { _, completed, error in
        // call your completion handler here...
    }
}.resume()

I'd suggest turning presentPrintInteractionController into an async throws function, and get rid of the completion handler parameter.

It is much less nested this way.

// you don't need to wrap the built-in errors that can be thrown
// so the only case left is userCancelled.
// perhaps consider renaming this 
public enum PrintingResult: Error {
  case userCancelled
}

// assuming this is a global function, you'd need @MainActor
@MainActor
public func presentPrintInteractionController(url: URL?, jobName: String? = nil) async throws {
    let printController = UIPrintInteractionController()
    let printInfo = UIPrintInfo.printInfo()
    if let jobName = jobName {
        printInfo.jobName = jobName
    }
    printController.printInfo = printInfo

    if let url = url {
        // fetching the data asynchronously
        let (data, response) = try await URLSession.shared.data(from: url)
        // you might add some error handling here by inspecting response
        printController.printingItem = data
    }
    
    try await withCheckedThrowingContinuation { continuation in
        printController.present(animated: true) { _, completed, error in
            if completed {
                continuation.resume()
            } else if let e = error {
                continuation.resume(with: .failure(e))
            } else {
                continuation.resume(with: .failure(PrintingResult.userCancelled))
            }
        }
    }
}

You can then use do...catch in your Button code to handle the three cases:

Button {
    Task {
        do {
            try await presentPrintInteractionController(url: URL(string: reportUrl), jobName: "Print a pdf report")
            print("Print Successful")
        } catch PrintingResult.userCancelled {
            print("Printing job cancelled.")
        } catch {
            print("Error: \(error)")
        }
    }
} label: {
    Text("Print")
}
Sweeper
  • 213,210
  • 22
  • 193
  • 313