While trying to get a file inside a Swift UI App using a UIViewControllerRepresentable wrapping an UIDocumentPickerViewController, I ran into a very weird behaviour:
Running the app on the simulator works as expected. Running the app on a physical device however would not return the files. Instead it would throw this error: Error Domain=NSCocoaErrorDomain Code=257 "The file “grile.doc” couldn’t be opened because you don’t have permission to view it."
Debugging, I managed to find that the code block causing this error to be thrown on the physical device isn't the actual accessing of the file, instead it's trying to get the file size that throws.
What am I doing wrong here, that makes this Representable work just fine on a simulator but not work on a physical device?
I have already tried using the FileManager approach to get file size, same thing happens. I also tried copying the file to a temp location with FileManager, that also throws the same error.
import Foundation
import SwiftUI
import UniformTypeIdentifiers
struct FilePicker: UIViewControllerRepresentable {
// MARK: - Properties
var didPickFileURL: ((URL) -> Void)
var didPickFileOverFileSizeLimit: (() -> Void)
// MARK: - UIViewController Wrapper Methods
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType("public.item")!])
picker.allowsMultipleSelection = false
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {
}
func makeCoordinator() -> Coordinator {
return FilePicker.Coordinator(parent: self)
}
// MARK: - Coordinator
class Coordinator: NSObject, UIDocumentPickerDelegate {
// MARK: - Coordinator Properties
var parent: FilePicker
// MARK: - Coordinator Init
init(parent: FilePicker) {
self.parent = parent
}
// MARK: - Coordinator UIDocument Delegate
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let url = urls.first else { return }
do {
let resources = try url.resourceValues(forKeys:[.fileSizeKey])
let fileSize = Double(resources.fileSize!)
// check if the file size is bigger than the limit per attachment
if fileSize.convertBytesToMegabytes() < 10 {
self.parent.didPickFileURL(url)
} else {
self.parent.didPickFileOverFileSizeLimit()
}
} catch {
print("Error: \(error)")
}
}
}
}
The conversion extension:
extension Double {
func convertBytesToMegabytes() -> Double {
let kilobytes = self / 1024
let megabytes = kilobytes / 1024
return megabytes
}
}
I've had issues as well. If you look at the URL to your document you might try:
url.startAccessingSecurityScopedResource()
If that returns true you will also need to call:
url.stopAccessingSecurityScopedResource()
Related
Im trying to open New Contact screen in my SwiftUI app but Im not getting Save\Done button.I haven't seen anybody doing this I really don't know answer why as it seems easy at first. This is my current code.
struct ContactsVC: UIViewControllerRepresentable{
typealias UIViewControllerType = CNContactViewController
func makeUIViewController(context: Context) -> CNContactViewController {
let store = CNContactStore()
let con = CNContact()
let vc = CNContactViewController(forNewContact: con)
vc.contactStore = store
vc.delegate = context.coordinator
return vc
}
func updateUIViewController(_ uiViewController: CNContactViewController, context: Context) {
}
class Coordinator: NSObject,CNContactViewControllerDelegate,UINavigationControllerDelegate{
var parent: ContactsVC
init(_ parent: ContactsVC) {
self.parent = parent
}
func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
I have requested authorisation when sending contact now I want to add it.
I am going through the AppKit tutorial Supporting Drag and Drop Through File Promises. I downloaded the demo app.
I tried to extract the NSFilePromiseProviderDelegate functionality from the ImageCanvasController class (which is an NSViewController) into a separate class. (Please see before & after code snippets below.)
Before my changes, dragging an image from the app canvas out into Finder and Apple Notes worked fine. But after my changes, nothing happens when I drag into Notes, and I get this error when I drag into Finder:
2022-02-26 23:16:52.713742+0100 MemeGenerator[31536:1975798] *** CFMessagePort: dropping corrupt reply Mach message (0b000100)
Is there any undocumented protocol conformance that I need to add to the new class? Or is there some underlying logic for which NSFilePromiseProviderDelegate only works if it's also an NSView or an NSViewController? In all the guides I found online, it is always tied to a view, but I didn't find any warning that it has to be.
Note:
The reason I want to separate the promise-provider functionality from views is, this way I could provide multiple NSDraggingItem objects to beginDraggingSession. For example, when multiple items are selected, and there is a mouseDragged event on one of them, I could start a dragging session including all the selected items.
Code before
class ImageCanvasController: NSViewController, NSFilePromiseProviderDelegate, ImageCanvasDelegate, NSToolbarDelegate {
...
/// Queue used for reading and writing file promises.
private lazy var workQueue: OperationQueue = {
let providerQueue = OperationQueue()
providerQueue.qualityOfService = .userInitiated
return providerQueue
}()
...
func pasteboardWriter(forImageCanvas imageCanvas: ImageCanvas) -> NSPasteboardWriting {
let provider = NSFilePromiseProvider(fileType: kUTTypeJPEG as String, delegate: self)
provider.userInfo = imageCanvas.snapshotItem
return provider
}
// MARK: - NSFilePromiseProviderDelegate
/// - Tag: ProvideFileName
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
let droppedFileName = NSLocalizedString("DropFileTitle", comment: "")
return droppedFileName + ".jpg"
}
/// - Tag: ProvideOperationQueue
func operationQueue(for filePromiseProvider: NSFilePromiseProvider) -> OperationQueue {
return workQueue
}
/// - Tag: PerformFileWriting
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: #escaping (Error?) -> Void) {
do {
if let snapshot = filePromiseProvider.userInfo as? ImageCanvas.SnapshotItem {
try snapshot.jpegRepresentation?.write(to: url)
} else {
throw RuntimeError.unavailableSnapshot
}
completionHandler(nil)
} catch let error {
completionHandler(error)
}
}
}
Code after
class CustomFilePromiseProviderDelegate: NSObject, NSFilePromiseProviderDelegate {
/// Queue used for reading and writing file promises.
private lazy var workQueue: OperationQueue = {
let providerQueue = OperationQueue()
providerQueue.qualityOfService = .userInitiated
return providerQueue
}()
/// - Tag: ProvideFileName
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
let droppedFileName = NSLocalizedString("DropFileTitle", comment: "")
return droppedFileName + ".jpg"
}
/// - Tag: ProvideOperationQueue
func operationQueue(for filePromiseProvider: NSFilePromiseProvider) -> OperationQueue {
return workQueue
}
/// - Tag: PerformFileWriting
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: #escaping (Error?) -> Void) {
do {
if let snapshot = filePromiseProvider.userInfo as? ImageCanvas.SnapshotItem {
try snapshot.jpegRepresentation?.write(to: url)
} else {
throw RuntimeError.unavailableSnapshot
}
completionHandler(nil)
} catch let error {
completionHandler(error)
}
}
}
class ImageCanvasController: NSViewController, ImageCanvasDelegate, NSToolbarDelegate {
...
func pasteboardWriter(forImageCanvas imageCanvas: ImageCanvas) -> NSPasteboardWriting {
let delegate = CustomFilePromiseProviderDelegate()
let provider = NSFilePromiseProvider(fileType: kUTTypeJPEG as String, delegate: delegate)
provider.userInfo = imageCanvas.snapshotItem
return provider
}
}
Does an NSFilePromiseProviderDelegate need to be also an NSView or NSViewController?
No.
delegate is a local variable and is released at the end of pasteboardWriter(forImageCanvas:). Without a delegate the file promise provider doesn't work. Solution: keep a strong reference to the delegate.
class ImageCanvasController: NSViewController, ImageCanvasDelegate, NSToolbarDelegate {
let delegate = CustomFilePromiseProviderDelegate()
...
func pasteboardWriter(forImageCanvas imageCanvas: ImageCanvas) -> NSPasteboardWriting {
let provider = NSFilePromiseProvider(fileType: kUTTypeJPEG as String, delegate: delegate)
provider.userInfo = imageCanvas.snapshotItem
return provider
}
}
I keep receiving the error: Thread 1: Fatal error: No ObservableObject of type User found. A View.environmentObject(_:) for User may be missing as an ancestor of this view.
This is my code below for Facebook login via Swift. The specific code that causes it to crash is self.user.isActive = true. This code accesses my environment object User and changes the variable isActive to True, which triggers my Navigation Link and causes the DetailView to appear.
Given the error, I have tried multiple ways to inject the Environment variable:
I added it like this when I used login in the view: login().environmentObject(User()).frame(width: 50, height: 50)
I tried to initialize the Coordinator class with login as a parent, and then access the user.isActive variable via the parent relationship (i.e. self.parent.user.isActive = true) but this does not change the environment variable or push the screen.
I tried to use a navigation link directly in the Coordinator class but I cannot use the self.user.isActive as a binding bool.
struct login : UIViewRepresentable {
#EnvironmentObject var user: User
let userDefault = UserDefaults.standard
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<login>) -> FBLoginButton {
let button = FBLoginButton()
button.permissions = ["public_profile", "email"]
button.delegate = context.coordinator
return button
}
func updateUIView(_ uiView: FBLoginButton, context: UIViewRepresentableContext<login>) {
// if self.userDefault.bool(forKey: Constants.UserDefaults.currentUser) {self.user.isActive = true}
}
func activateLogin(){
if self.userDefault.bool(forKey: Constants.UserDefaults.currentUser)
{ self.user.isActive = true }
}
class Coordinator : NSObject, LoginButtonDelegate {
#EnvironmentObject var user: User
let userDefault = UserDefaults.standard
var parent: login
init(_ parent: login) {
self.parent = parent
}
func loginButton(_ loginButton: FBLoginButton, didCompleteWith result: LoginManagerLoginResult?, error: Error?) {
if error != nil{
print((error?.localizedDescription)!)
return
}
if AccessToken.current != nil{
let credential = FacebookAuthProvider.credential(withAccessToken: AccessToken.current!.tokenString)
Auth.auth().signIn(with: credential) { (res,er) in
if er != nil{
print((er?.localizedDescription)!)
return
}
print("success")
self.user.isActive = true
self.parent.user.isActive = true
self.userDefault.set(true, forKey: Constants.UserDefaults.currentUser)
self.userDefault.synchronize()
}
}
}
func loginButtonDidLogOut(_ loginButton: FBLoginButton) {
try! Auth.auth().signOut()
print ("User logged out")
}
}
}```
My guess is that you don't have nice #EnvironmentObject set up for you in traditional UIKit views.
Maybe you can do it in views that conform to UIViewRepresentable, since it is meant to work with UIKit.
But I don't think a class Coordinator: NSObject is able to support #EnvironmentObject.
I've tried use QLPreviewController for file(.txt, .pdf, .docx, .xlsx, etc.) preview in my iOS project, but it is not working well in iOS 11: it shows blank page whatever use device or simulator. In iOS 10.3.1(simulator) it is working well, and everything is fine.
The below is the detail informations:
Environment:
Xcode version: 8.3.3, 9.0.1
Device: iPhone 7 Plus, Simulator
iOS version: 11.0.3(iPhone 7 Plus), 10.3.1(Simulator), 11.0.1(Simulator)
Swift version: 3.1/3.2
Code
import UIKit
import QuickLook
class QuickViewController: UIViewController {
var url: String!
var filePath: URL!
var msg: Message! {
didSet {
url = msg.content
// create document folder url
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
// let tempDirectory = URL(fileURLWithPath: NSTemporaryDirectory())
filePath = documentsURL.appendingPathComponent((msg.content as NSString).lastPathComponent)
}
}
#IBOutlet weak var contentView: UIView!
// MARK: - Initialisation
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
loafFile()
}
func loafFile() {
if FileManager.default.fileExists(atPath: filePath.path) {
quickLookFile()
} else {
AppManager_Inst.download(url: "\(url!)", destinationUrl: filePath, completion: { [unowned self](succeed, msg) in
if succeed {
self.quickLookFile()
} else {
print(msg)
}
})
}
}
func quickLookFile() {
print(filePath.path)
/// Trying to read the file via FileManager to check if the filePath is correct or not
let data = FileManager.default.contents(atPath: filePath.path)
print(data)
if QLPreviewController.canPreview(filePath as NSURL) {
let QLController = QLPreviewController()
QLController.delegate = self
QLController.dataSource = self
QLController.view.frame = self.contentView.frame
self.view.addSubview(QLController.view)
} else {
print("file cannot to preview")
}
}
}
extension QuickViewController: QLPreviewControllerDataSource, QLPreviewControllerDelegate {
// QLPreviewControllerDataSource
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return filePath as NSURL
}
// QLPreviewControllerDelegate
func previewController(_ controller: QLPreviewController, shouldOpen url: URL, for item: QLPreviewItem) -> Bool {
return true
}
}
I'm not sure what I missing or something wrong here, please help.
p.s.: I'll trying the code with iPhone 5c(iOS 10.3.1) later, and the result should update to here once I've done it.
You did not add the current viewController as the parent class of the QLPreviewController()
Just add QLController.didMove(toParentViewController: self) after adding it to the self.view
I think this should solve your issue.
I have a project that has a ViewController that loads a saved NSURL from memory. This NSURL is saved using NSCoding. When I run my app initially, my print log says:
Saved URL File:
file:///private/var/mobile/Containers/Data/Application/7939335F-C909-479E-A309-5AC833069A7B/Documents/Inbox/Pizza-2.pdf
Webview started Loading
Webview did finish load
It displays the PDF file just fine. When I run my app again a few minutes later, it won't display the PDF and it says:
Saved URL File:
file:///private/var/mobile/Containers/Data/Application/7939335F-C909-479E-A309-5AC833069A7B/Documents/Inbox/Pizza-2.pdf
Webview started Loading Webview fail with error Optional(Error
Domain=NSURLErrorDomain Code=-1100 "The requested URL was not found on
this server." UserInfo={NSUnderlyingError=0x14883f210 {Error
Domain=kCFErrorDomainCFNetwork Code=-1100 "The requested URL was not
found on this server."
UserInfo={NSErrorFailingURLStringKey=file:///private/var/mobile/Containers/Data/Application/7939335F-C909-479E-A309-5AC833069A7B/Documents/Inbox/Pizza-2.pdf,
NSLocalizedDescription=The requested URL was not found on this
server.,
NSErrorFailingURLKey=file:///private/var/mobile/Containers/Data/Application/7939335F-C909-479E-A309-5AC833069A7B/Documents/Inbox/Pizza-2.pdf}},
NSErrorFailingURLStringKey=file:///private/var/mobile/Containers/Data/Application/7939335F-C909-479E-A309-5AC833069A7B/Documents/Inbox/Pizza-2.pdf,
NSErrorFailingURLKey=file:///private/var/mobile/Containers/Data/Application/7939335F-C909-479E-A309-5AC833069A7B/Documents/Inbox/Pizza-2.pdf,
NSLocalizedDescription=The requested URL was not found on this
server.})
My code for the ViewController is:
class PDFItemViewController: UIViewController, UIWebViewDelegate {
// MARK: Properties
var file: PDFFile?
var incomingURL: NSURL!
#IBOutlet weak var background: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//Set theme pic to back always
view.sendSubviewToBack(background)
let myWebView:UIWebView = UIWebView(frame: CGRectMake(0, 44, UIScreen.mainScreen().bounds.width, UIScreen.mainScreen().bounds.height))
self.view.addSubview(myWebView)
myWebView.delegate = self
if let file = file {
incomingURL = file.url
print("Saved URL File: \(incomingURL)")
let request = NSURLRequest(URL: incomingURL!)
myWebView.loadRequest(request)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: UIWebView Delegate
func webView(webView: UIWebView, didFailLoadWithError error: NSError?) {
print("Webview fail with error \(error)");
}
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
return true
}
func webViewDidStartLoad(webView: UIWebView) {
print("Webview started Loading")
}
func webViewDidFinishLoad(webView: UIWebView) {
print("Webview did finish load")
}
}
'PDFFile' is an array of PDF Files. The NSURL is saved from an incoming PDF file the user can view from mail. It looks like it might not be saving? But why is it showing the file name if it's not saving? Thank you.
Update:
In my AppDelegate I have this code:
func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool {
// Transfer incoming file to global variable to be read
if url != "" {
// Load from Mail App
incomingFileTransfer = url
incomingStatus = "Incoming"
}
return true
}
I've created a class called PDFFile.swift:
// Class for the saved PDF File, in this case a NSURL
class PDFFile: NSObject, NSCoding {
// MARK: Properties
var name: String
var url: NSURL
// MARK: Archiving Path
static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("files")
// MARK: Types
struct PropertyKey {
static let nameKey = "name"
static let urlKey = "url"
}
// MARK: Initialization
init?(name: String, url: NSURL) {
self.name = name
self.url = url
super.init()
if name.isEmpty {
return nil
}
}
// MARK: NSCoding
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: PropertyKey.nameKey)
aCoder.encodeObject(url, forKey: PropertyKey.urlKey)
}
required convenience init?(coder aDecoder: NSCoder) {
let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
let url = aDecoder.decodeObjectForKey(PropertyKey.urlKey) as! NSURL
self.init(name: name, url: url)
}
}
When I view the incoming PDF file from mail, it loads in a separate UIWebView as such:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// Incoming Emailed PDF from the 'Open-in' feature
if incomingFileTransfer != nil {
// Show incoming file
let request = NSURLRequest(URL: incomingFileTransfer!)
incomingView.loadRequest(request)
}
}
My save button points to an unwind on another View Controller as:
#IBAction func unwindToMainMenu(sender: UIStoryboardSegue) {
if let sourceViewController = sender.sourceViewController as? IncomingFileViewController, file = sourceViewController.file {
if let selectedIndexPath = fileTableNotVisible.indexPathForSelectedRow {
// Update an existing recipe.
pdfFiles[selectedIndexPath.row] = file
fileTableNotVisible.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None)
}
else {
// Add a new file
let newIndexPath = NSIndexPath(forRow: pdfFiles.count, inSection: 0)
// Add to pdf file array
pdfFiles.append(file)
// Adds new file to bottom of table
fileTableNotVisible.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
}
saveFiles()
}
}
// MARK: NSCoding
func saveFiles() {
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(pdfFiles, toFile: PDFFile.ArchiveURL.path!)
if !isSuccessfulSave {
print("Failed to save PDF file")
}
}