Using PDFKit to Open PDF in App Automatically after Downloading - swift

Missing some connection which I am unable to figure out (also googled a lot still no success) in how to open PDF file in app. I am using PDFKit. This is the PDFKitView struct in which URL should be passed:
struct PDFKitView: View {
#State var reportId: ReportResponse
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
}
}
This is the Report Row in which I am trying to pass URL to state object pdfDonwload:
struct ReportRow: View {
var report : ReportResponse
#StateObject var pdfDownload:URL = URL(fileURLWithPath: "")
var body: some View {
VStack{
HStack{
Text(report.name)
//formatting
}.frame(maxWidth: .infinity, alignment: .leading)
HStack{
Text("P.Id:")
//formatting
Text(report.patientID)
//formatting
Spacer()
Text("Status")
//formatting
Text(report.status)
//formatting
}}
.onAppear{
Task{
do{
try await getPath()
}catch{Alert(title:"Text")}
}
}}
func getPath() async throws
{
var urlComponents = URLComponents()
//add other components
urlComponents.queryItems = [URLQueryItem(name:
"uniquePackageId", value:
uniqueReportId)]
let url = urlComponents.url
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 {
pdfDownload = url!
}
}
catch
{
print("Error")
}}
downloadTask.resume()
}}
However, no value is passed to PDFKitView() it is blank. The first issue is PDFKitView should get the updated pdfDownload:URL value from the func call. Neither the value is updating nor passing updated value to PDFKitView.
This is the list struct:
struct ReportList: View{
#ObserveObjecct var reportLink:ReportViewModel
#State var pdfDownload:URL = URL(fileURLWithPath="")
var body:some view{
NavigationView{
List{
ForEach(reportLink.trackReport)
{report in
VStack{NavigationLink
(destination:PDFKitView(url:pdfDownload,
pdfDownload:pdfDownload,report:report))
{
ReportRow(report:report)
}
}}}}}}
I want file should open automatically after downloading. Running the app in simulator.

Related

Updating specific list item after patch request Swift, SwiftUI

I'm using GitHub api to
get issues
update issue title or description
I call self.get() inside update() method, to get the updated list of issues after the successful update.
But after update when I get back to screen with list of issues, its title/description shows the previous values.
Could you please say how can I fix it? Thanks!
Here is the model:
struct Issue: Codable, Hashable {
let id: Int
let number: Int
var title: String
var body: String? = ""
}
ViewModel:
class ViewModel: ObservableObject {
#Published var issues: [Issue] = []
private let token: String = "_token_"
init() {
get()
}
func get() {
guard let url = URL(string: "https://api.github.com/repos/\(repoOwner)/\(repo)/issues") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard let data = data, error == nil else {
return
}
do {
let response = try JSONDecoder().decode([Issue].self, from: data)
DispatchQueue.main.async {
self?.issues = response
}
} catch {
print("error during get issues \(error.localizedDescription)")
}
}.resume()
}
func update(number: Int, title: String, body: String) {
guard let url = URL(string: "https://api.github.com/repos/\(repoOwner)/\(repo)/issues/\(number)") else {
return
}
let json: [String: Any] = ["title": "\(title)",
"body": "\(body)"]
let jsonData = try? JSONSerialization.data(withJSONObject: json)
var patchRequest = URLRequest(url: url)
patchRequest.httpMethod = "PATCH"
patchRequest.addValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
patchRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
patchRequest.httpBody = jsonData
URLSession.shared.dataTask(with: patchRequest) { data, response, error in
guard let data = data, error == nil else {
return
}
let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
if let responseJSON = responseJSON as? [String: Any] {
print(responseJSON)
}
self.get()
}.resume()
}
}
Views
List of issues:
struct HomeView: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
NavigationView {
List {
ForEach(viewModel.issues, id: \.id) { issue in
NavigationLink(destination: EditView(issue: issue)) {
IssueRow(issue: issue)
}
}
}
.navigationTitle("Github issues")
}
}
}
struct IssueRow: View {
var issue: Issue
var body: some View {
VStack {
HStack {
Text("#\(issue.number)")
.bold()
Text("\(issue.title)")
.bold()
.lineLimit(1)
Spacer()
}.foregroundColor(.black)
HStack {
Text(issue.body ?? "no description")
.foregroundColor(.gray)
.lineLimit(2)
Spacer()
}
Spacer()
}
}
}
Edit view:
struct EditView: View {
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var viewModel: ViewModel
var issue: Issue
#State var title: String = ""
#State var description: String = ""
func save() {
viewModel.update(number: issue.number, title: title, body: description)
dismiss()
}
func dismiss() {
presentationMode.wrappedValue.dismiss()
}
var body: some View {
Content(issue: issue, title: $title, description: $description, save: save)
}
}
extension EditView {
struct Content: View {
var issue: Issue
#Binding var title: String
#Binding var description: String
let save: () -> Void
var body: some View {
VStack {
List {
Section(header: Text("Title")) {
TextEditor(text: $title)
}
Section(header: Text("Description")) {
TextEditor(text: $description)
.multilineTextAlignment(.leading)
}
}
saveButton
}
.onAppear(perform: setTitleAndDescription)
.navigationTitle("Update issue")
}
var saveButton: some View {
Button(action: save) {
Text("Save changes")
}
}
func setTitleAndDescription() {
self.title = issue.title
self.description = issue.body ?? ""
}
}
}

Best practice for wrapping `AVPlayerView` in `SwiftUI`?

I want to wrap AVPlayerView into SwiftUI. Here are my codes(playground):
import PlaygroundSupport
import SwiftUI
import AVKit
class RecorderPlayerModel: ObservableObject {
#Published var playerView: AVPlayerView
init() {
self.playerView = AVPlayerView()
self.playerView.player = AVPlayer()
}
func reload(url: URL) {
let asset = AVAsset(url: url)
let item = AVPlayerItem(asset: asset)
self.playerView.player?.replaceCurrentItem(with: item)
}
}
struct RecorderPlayerView: NSViewRepresentable {
typealias NSViewType = AVPlayerView
var playerView: AVPlayerView
func makeNSView(context: Context) -> AVPlayerView {
return playerView
}
func updateNSView(_ nsView: AVPlayerView, context: Context) {}
}
struct ContentView: View {
#StateObject var playerViewModel: RecorderPlayerModel = .init()
var body: some View {
VStack {
RecorderPlayerView(playerView: playerViewModel.playerView)
.clipShape(RoundedRectangle(cornerRadius: 8))
.onAppear {
let fileManager = FileManager.default
if let url = URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4") {
Task {
do {
let (data, _) = try await URLSession.shared.data(from: url)
let fileUrl = fileManager.temporaryDirectory.appendingPathComponent("sample")
.appendingPathExtension(for: .mpeg4Movie)
try? fileManager.removeItem(at: fileUrl)
fileManager.createFile(atPath: fileUrl.path, contents: data)
playerViewModel.reload(url: fileUrl)
} catch {
print(error)
}
}
}
}
Button {
if playerViewModel.playerView.canBeginTrimming {
Task {
await playerViewModel.playerView.beginTrimming()
}
}
} label: {
Text("trim")
}
}.frame(width: 500, height: 500, alignment: .center)
}
}
PlaygroundPage.current.setLiveView(ContentView())
Since I want to trim the video, I cannot directly use VideoPlayer. But after wrapping AVPlayerView to NSViewRepresentable View, the trim view always lose interactivity.
Reproduce way: just double click at anywhere when trimming.
supplement
when losing interactivity, the console will log
-[AVTrimIndicatorAccessibilityElement accessibilityHitTest:]: unrecognized selector sent to instance 0x600001916ce0
updated
It is not triggered on all models of Mac.
I figure out the problem. It seems that applying .clipShape(...) on RecorderPlayerView will cause the problem.
Just remove the line .clipShape(RoundedRectangle(cornerRadius: 8)) will solve this problem.
RecorderPlayerView(playerView: playerViewModel.playerView)
// .clipShape(RoundedRectangle(cornerRadius: 8)) <-- remove this line
.onAppear {
...
}

SwiftUI Network Image show different views on loading and error

I would like to make sure that when the image is being downloaded the ProgressView is visible and if the url being passed is invalid (empty), a placeholder is put.
How can I do this?
Code:
import Foundation
import SwiftUI
// Download image from URL
struct NetworkImage: View {
public let url: URL?
var body: some View {
Group {
if let url = url, let imageData = try? Data(contentsOf: url),
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
}
else {
ProgressView()
}
}
}
}
struct NetworkImage_Previews: PreviewProvider {
static var previews: some View {
let url = [
"",
"http://google.com",
"https://yt3.ggpht.com/a/AATXAJxAbUTyYnKsycQjZzCdL_gWVbJYVy4mVaVGQ8kRMQ=s176-c-k-c0x00ffffff-no-rj"
]
NetworkImage(url: URL(string: url[0])!)
}
}
You can create a ViewModel to handle the downloading logic:
extension NetworkImage {
class ViewModel: ObservableObject {
#Published var imageData: Data?
#Published var isLoading = false
private var cancellables = Set<AnyCancellable>()
func loadImage(from url: URL?) {
isLoading = true
guard let url = url else {
isLoading = false
return
}
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] in
self?.imageData = $0
self?.isLoading = false
}
.store(in: &cancellables)
}
}
}
And modify your NetworkImage to display a placeholder image as well:
struct NetworkImage: View {
#StateObject private var viewModel = ViewModel()
let url: URL?
var body: some View {
Group {
if let data = viewModel.imageData, let uiImage = UIImage(data: data) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else if viewModel.isLoading {
ProgressView()
} else {
Image(systemName: "photo")
}
}
.onAppear {
viewModel.loadImage(from: url)
}
}
}
Then you can use it like:
NetworkImage(url: URL(string: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png"))
(Note that the url parameter is not force unwrapped).

SwiftUI Image from URL not showing

I have a problem when I want to show an Image from a URL. I created a class for downloading data and publishing the data forward - ImageLoader:
class ImageLoader: ObservableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
func loadData(from urlString: String?) {
if let urlString = urlString {
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
}
}
task.resume()
}
}
}
Therefore, I use it inside a ImageView struct which I use inside my screen.
struct ImageView: View {
var urlString: String
#ObservedObject var imageLoader: ImageLoader = ImageLoader()
#State var image: UIImage = UIImage(named: "homelessDogsCats")!
var body: some View {
ZStack() {
Image(uiImage: image)
.resizable()
.onReceive(imageLoader.didChange) { data in
self.image = UIImage(data: data) ?? UIImage()
}
}.onAppear {
self.imageLoader.loadData(from: urlString)
}
}
}
My problem is that if I just run my project, the image doesn't change and by default appears only image UIImage(named: "homelessDogsCats").
If I add a breakpoint inside
onAppear {
self.imageLoader.loadData(from: urlString)
}
and just step forward, the image is showing.
I have the same problem in another view which usually doesn't display the Image from URL, but sometimes it does.
Try using #Published - then you don't need a custom PassthroughSubject:
class ImageLoader: ObservableObject {
// var didChange = PassthroughSubject<Data, Never>() <- remove this
#Published var data: Data?
...
}
and use it in your view:
struct ImageView: View {
var urlString: String
#ObservedObject var imageLoader = ImageLoader()
#State var image = UIImage(named: "homelessDogsCats")!
var body: some View {
ZStack() {
Image(uiImage: image)
.resizable()
.onReceive(imageLoader.$data) { data in
guard let data = data else { return }
self.image = UIImage(data: data) ?? UIImage()
}
}.onAppear {
self.imageLoader.loadData(from: urlString)
}
}
}
Note: if you're using SwiftUI 2, you can use #StateObject instead of #ObservedObject and onChange instead of onReceive.
struct ImageView: View {
var urlString: String
#StateObject var imageLoader = ImageLoader()
#State var image = UIImage(named: "homelessDogsCats")!
var body: some View {
ZStack() {
Image(uiImage: image)
.resizable()
.onChange(of: imageLoader.data) { data in
guard let data = data else { return }
self.image = UIImage(data: data) ?? UIImage()
}
}.onAppear {
self.imageLoader.loadData(from: urlString)
}
}
}
Working version for iOS 13, 14 (AsyncImage 1 liner is introduced in iOS 15, and the solution below is for the versions prior to that in case your min deployment target is not iOS 15 yet ) and with the latest property wrappers - Observed, Observable and Publisher ( without having to use PassthroughSubject<Data, Never>()
Main View
import Foundation
import SwiftUI
import Combine
struct TransactionCardRow: View {
var transaction: Transaction
var body: some View {
CustomImageView(urlString: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png") // This is where you extract urlString from Model ( transaction.imageUrl)
}
}
Creating CustomImageView
struct CustomImageView: View {
var urlString: String
#ObservedObject var imageLoader = ImageLoaderService()
#State var image: UIImage = UIImage()
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:100, height:100)
.onReceive(imageLoader.$image) { image in
self.image = image
}
.onAppear {
imageLoader.loadImage(for: urlString)
}
}
}
Creating a service layer to download the Images from url string, using a Publisher
class ImageLoaderService: ObservableObject {
#Published var image: UIImage = UIImage()
func loadImage(for urlString: String) {
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.image = UIImage(data: data) ?? UIImage()
}
}
task.resume()
}
}

Updating image view in SwiftUI after downloading image

On the click of a button I am trying to download a new random image and update the view. When the app loads it displays the downloaded image. When the button is clicked the image seems to download but the view is never updated and displays the place holder image. Am I missing something here, any ideas? Here is a simplified version.
import SwiftUI
struct ContentView : View {
#State var url = "https://robohash.org/random.png"
var body: some View {
VStack {
Button(action: {
self.url = "https://robohash.org/\(Int.random(in:0 ..< 10)).png"
}) {
Text("Get Random Robot Image")
}
URLImage(url: url)
}
}
}
class ImageLoader: BindableObject {
var downloadedImage: UIImage?
let didChange = PassthroughSubject<ImageLoader?, Never>()
func load(url: String) {
guard let imageUrl = URL(string: url) else {
fatalError("Image URL is not correct")
}
URLSession.shared.dataTask(with: imageUrl) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
self.didChange.send(nil)
}
return
}
self.downloadedImage = UIImage(data: data)
DispatchQueue.main.async {
print("downloaded image")
self.didChange.send(self)
}
}.resume()
}
}
import SwiftUI
struct URLImage : View {
#ObjectBinding private var imageLoader = ImageLoader()
var placeholder: Image
init(url: String, placeholder: Image = Image(systemName: "photo")) {
self.placeholder = placeholder
self.imageLoader.load(url: url)
}
var body: some View {
if let uiImage = self.imageLoader.downloadedImage {
print("return downloaded image")
return Image(uiImage: uiImage)
} else {
return placeholder
}
}
}
The problem seems to be related to some kind of lost synchronization between the ContentView and the ImageURL (that happens after the button click event).
A possible workaround is making the ImageURL a #State property of the ContentView.
After that, inside the scope of the button click event, we can call the image.imageLoader.load(url: ) method. As the download of the image ends, the publisher (didChange) will notify the ImageURL and then the change is correctly propagated to the ContentView.
import SwiftUI
import Combine
enum ImageURLError: Error {
case dataIsNotAnImage
}
class ImageLoader: BindableObject {
/*
init(url: URL) {
self.url = url
}
private let url: URL */
let id: String = UUID().uuidString
var didChange = PassthroughSubject<Void, Never>()
var image: UIImage? {
didSet {
DispatchQueue.main.async {
self.didChange.send()
}
}
}
func load(url: URL) {
print(#function)
self.image = nil
URLSession.shared.dataTask(with: url) { (data, res, error) in
guard error == nil else {
return
}
guard
let data = data,
let image = UIImage(data: data)
else {
return
}
self.image = image
}.resume()
}
}
URLImage view:
struct URLImage : View {
init() {
self.placeholder = Image(systemName: "photo")
self.imageLoader = ImageLoader()
}
#ObjectBinding var imageLoader: ImageLoader
var placeholder: Image
var body: some View {
imageLoader.image == nil ?
placeholder : Image(uiImage: imageLoader.image!)
}
}
ContentView:
struct ContentView : View {
#State var url: String = "https://robohash.org/random.png"
#State var image: URLImage = URLImage()
var body: some View {
VStack {
Button(action: {
self.url = "https://robohash.org/\(Int.random(in: 0 ..< 10)).png"
self.image.imageLoader.load(url: URL(string: self.url)!)
}) {
Text("Get Random Robot Image")
}
image
}
}
}
Anyway I will try to investigate the problem and if I will know something new I will modify my answer.