Document Picker and Document Preview#13
Conversation
added a view modifier for selecting a file (pdf, image) on the device and expose its url to the app Document Viewer added a view modifier for previewing a selected file (pdf, image) that's availble in the app. It could be a local downladed file or a file selected with the document picker ReadMe updated the readme to describe the two new view modifiers
Fixed a variable name after a refactoring
marcprux
left a comment
There was a problem hiding this comment.
Looks great! Just a couple small changes and one question in the review.
Sources/SkipKit/DocumentPicker.swift
Outdated
|
|
||
| let sourceURL = URL(string: uri.toString())! | ||
| let inputStream = resolver.openInputStream(uri)! | ||
| let bufferedInputStream = java.io.BufferedInputStream(inputStream) |
There was a problem hiding this comment.
This can all be simplified with inputStream.copyTo(outputStream). See https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io/copy-to.html
There was a problem hiding this comment.
Even simpler, you could use File.copyTo(File, overwrite = true)
There was a problem hiding this comment.
Thanks for the suggestion! I'm going to to try it out.
There was a problem hiding this comment.
Ok, I've tested both methods, File.copyTo(File, overwrite = true) is not working, I guess because the uri returned from Activity result is not directly readable and creating a file from that returns a file instance that's not usable.
I've simplified the code using your first suggestion, removing the loop and using the direct copy on the input stream.
Sources/SkipKit/DocumentPicker.swift
Outdated
|
|
||
| return onChange(of: isPresented.wrappedValue) { oldValue, presented in | ||
| if presented == true { | ||
| let mimeTypes = kotlin.arrayOf("application/pdf", "image/*") |
There was a problem hiding this comment.
Do we need to limit this to PDF and images? Could we instead have another parameter to withDocumentPicker that is a set of mime types that can be accepted (or empty to accept all files)?
There was a problem hiding this comment.
You're right, the thing is that Android and iOS uses different way to specify mime types. On iOS is an array of UTType, while on Android is an array of strings. Do you think the better approach is to use two different parameters?
There was a problem hiding this comment.
I think the best thing might be to create a stub of UTType for the Android side with just enough of the API to get it to build. Like:
#if SKIP
public struct UTType: Equatable, Hashable {
var identifier: String
var preferredMIMEType: String?
public init?(mimeType: String, conformingTo supertype: UTType = .data) {
self.identifier = identifier
self. preferredMIMEType = preferredMIMEType
}
}
public extension UTType {
public static let data = UTType(mimeType: "data/…")
// ... other common extensions as needed
}
#endifthen your function can accept a set of UTTypes on both sides, and on Android you will just grab . preferredMIMEType and use that.
There was a problem hiding this comment.
Thanks I'll work on that!
There was a problem hiding this comment.
Ok, I've tried to follow your suggestion, but I'm not able to make it work. I've created a UTType stub like that:
public struct UTType: Equatable, Hashable {
var identifier: String
var preferredMIMEType: String?
public init?(mimeType: String, conformingTo supertype: UTType? = .data) {
self.identifier = mimeType
self.preferredMIMEType = mimeType
}
}
public extension UTType {
public static let data = UTType(mimeType: "data/...")
public static let pdf = UTType(mimeType: "application/pdf")
public static let image = UTType(mimeType: "image/*")
public static let text = UTType(mimeType: "text/*")
public static let doc = UTType(mimeType: "application/msword")
}and added inside the #if SKIP compile directive. Then I've changed the viewBuilder function signature like that:
@ViewBuilder public func withDocumentPicker(isPresented: Binding<Bool>, allowedContentTypes: [UTType], selectedDocumentURL: Binding<URL?>, selectedFilename: Binding<String?>, selectedFileMimeType: Binding<String?> ) -> some View {to make it compile correctly I also had to include under the #if !SKIP block the following import
#import UniformTypeIdentifiersDoing that I've been able to comipile, but when I try to use it in a project I'm facing the same problem again, for example when using it like that:
.withDocumentPicker(isPresented: $isPresented, allowedContentTypes: [.image, .pdf], selectedDocumentURL: $documentURL, selectedFilename: $selectedFileName, selectedFileMimeType: $selectedFileType)I have the following error:
Argument type mismatch: actual type is 'skip.lib.Array<Element>', but 'skip.lib.Array<skip.kit.UTType>' was expected.
which brings me in a sort of a loop. I can't use the skip UTType I've created, because it has no ties with the original type, but on the other hand the transpiler don't know how to use it.
Do you think I'm doing something wrong?
There was a problem hiding this comment.
Well, If I don't add the import it still says that it can't find UTType in scope and suggest to import UniformTypeIdentifiers, that I have to import in the #if !SKIP block.
After that I get the following error: Argument type mismatch: actual type is 'skip.lib.Array<Element>', but 'skip.lib.Array<skip.kit.UTType>' was expected. when compiling.
There was a problem hiding this comment.
Can you commit and push what you have so I can see the error? It seems like it should work.
For the UniformTypeIdentifiers header issue, I think SkipKit.swift could have:
#if !SKIP
@_exported import UniformTypeIdentifiers
#endif
and then anyone importing SkipKit will be able to use the UTType.
There was a problem hiding this comment.
Hi!
I've just pushed the latest modifications. I've added the import in the SkipKit.swift which allows me to remove the import from the DocumentPicker.swift file.
The Package compile just fine, you can see the problem when trying to use it in a project.
Here a simple View I used in a project to test it:
import SwiftUI
import SkipKit
struct DocumentPickerTestView: View {
@State private var isPresented: Bool = false
@State private var documentURL: URL?
@State private var selectedFileName: String?
@State private var selectedFileType: String?
@State private var showPreview: Bool = false
@State private var takePicture: Bool = false
@State private var pictureURL: URL?
var body: some View {
VStack(spacing: 32) {
Spacer()
Button("Pick Document") {
isPresented = true
}
.withDocumentPicker(isPresented: $isPresented, allowedContentTypes: [UTType.image, UTType.pdf], selectedDocumentURL: $documentURL, selectedFilename: $selectedFileName, selectedFileMimeType: $selectedFileType)
if let documentURL {
VStack {
if let selectedFileName {
Text(selectedFileName)
.font(.caption)
.multilineTextAlignment(.center)
} else {
Text("\(documentURL.absoluteString)")
.font(.caption)
.multilineTextAlignment(.center)
}
Button {
showPreview = true
} label: {
Text("Mostra preview")
}
.withDocumentPreview(isPresented: $showPreview, documentURL: documentURL, filename: selectedFileName, type: selectedFileType)
}
} else {
Text("nessun documento selezionato")
}
Spacer()
}
}
}There was a problem hiding this comment.
The error message was misleading, but the issue is that the UTType static constants were Optional<UTType> because that init is optional. Force-unwrapping them fixed the issue.
I've move it out into its own separate UTType.swift file added some more constants. Once CI passes, I'll merge the PR.
| /// - isPresented: binding della variabile di stato che indica se il compoemente è visualizzato o meno | ||
| /// - documentURL: URL del file che si intente visualizzare | ||
| /// - Returns: su iOS un QLPreviewController mentre su Android viene lanciato in Intent ACTION_VIEW per la scelta dell'App da utilizzare. | ||
| @ViewBuilder public func withDocumentPreview(isPresented: Binding<Bool>, documentURL: URL?, filename: String? = nil, type: String? = nil) -> some View { |
There was a problem hiding this comment.
Need to add // SKIP @nobridge to prevent bridging until we can implement support for view extensions. Like at https://github.com/skiptools/skip-kit/blob/main/Sources/SkipKit/MediaPicker.swift#L48C5-L48C22
simplified the implementation of cache copyiing using the BufferedInputStream copyTo method added skip nobridge
Added output and input close at the end of the file copy as stated in : https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io/copy-to.html
fixed a bug that when using for the first time after showing the permission request dialog on Andoroid users wont be able to tap on the button to activate the picker unless they reload the view, since the `isPresented` flag was still set to true.
| filename.wrappedValue = name | ||
| mimeType.wrappedValue = type | ||
|
|
||
| // To be able to access the file from another part of the app it needs to be copied in tha cached directory: |
There was a problem hiding this comment.
Reading a little more about this, could you call context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) to get persistent access to the URI, and thereby avoid needing to perform the copy operation? This would be preferable, since it would also allow a user to edit the file in the same way on both platforms, and also prevents the buildup of cached copies of selected files over time when they might not be needed.
There was a problem hiding this comment.
Hi,
I've tried your suggestion but I can't make it work. Getting the permissions in fact seems to make the uri readable, but the problem is that it's still a content:// type uri that can't be accessed without a content resolver. Doing so it means that the user will need to implement a different logic to handle the file on Android when it's returned, because on iOS it will be a directly accessible file while on Android it stil need to rely on a content resolver.
There was a problem hiding this comment.
Oh right, I forgot it would be a content://. OK, let's stick with copying the file.
Created a UTType stub for Android for letting user choose what kind of files select with the document picker. Note: it compiles fine, but when using the view extension `.withDocumentPicker` in an actual project it gives a compilation error `Argument type mismatch: actual type is 'skip.lib.Array<Element>', but 'skip.lib.Array<skip.kit.UTType>'`
|
This will be a great feature to have! Thanks very much for the contribution! |
added a view modifier for selecting a file (pdf, image) on the device and expose its url to the app
Document Viewer
added a view modifier for previewing a selected file (pdf, image) that's availble in the app. It could be a local downladed file or a file selected with the document picker
ReadMe
updated the readme to describe the two new view modifiers
Thank you for contributing to the Skip project! Please use this space to describe your change and add any labels (bug, enhancement, documentation, etc.) to help categorize your contribution.
Skip Pull Request Checklist:
swift test