I have a project that is associated with opening PDF files. This is set in the Info.plist. When I get a PDF attachment in email, I can hold my finger on the PDF attachment and then 'Open in' in my app. In my AppDelegate, I have the following added:
func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool {
incomingTransfer = URL
return true
}
incomingTransfer is a Global Variable declared in another ViewController as an NSURL. This ViewController also has a UIWebView and the incomingTransfer loads into it and I'm able to see the new PDF file. My goal is to have a button that allows the user to save the incoming PDF as a PDF. I'm having trouble with this. I thought I had it all figured out, but it wasn't saving as a PDF at all, but rather as a String. Can someone help me please? My goal is to save the incoming PDF file as a PDF to the app memory, preferably in DocumentDirectory. I have a hard time trying to convert Objective C to Swift. My original code to save it was:
let html = String(incomingFileTransfer)
let fmt = UIMarkupTextPrintFormatter(markupText: html)
let render = UIPrintPageRenderer()
render.addPrintFormatter(fmt, startingAtPageAtIndex: 0)
let page = CGRect(x: 0, y: 0, width: 595.2, height: 841.8) // A4, 72 dpi
let printable = CGRectInset(page, 0, 0)
render.setValue(NSValue(CGRect: page), forKey: "paperRect")
render.setValue(NSValue(CGRect: printable), forKey: "printableRect")
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, CGRectZero, nil)
for i in 1...render.numberOfPages() {
UIGraphicsBeginPDFPage();
let bounds = UIGraphicsGetPDFContextBounds()
render.drawPageAtIndex(i - 1, inRect: bounds)
}
UIGraphicsEndPDFContext();
recipeFileName = fileName.text!
print("File Name Entered: \(recipeFileName)")
let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
pdfData.writeToFile("\(documentsPath)/\(recipeFileName).pdf", atomically: true)
I figured it out. I created a class called 'PDFFile'. Within 'PDFFile' are two variables, named var name: String and var url: NSURL. Within my 'IncomingTransfer' ViewController, I have the 'save' button create and save the new file with a typed name from the UITextField and the incomingURL specified in my AppDelegate is assigned to the url variable. Both are then saved to the PDFFile class using NSCoding. I then set a UITableView for it's dataSource from the PDFFile Class array data. I created a segue when the user clicks on the UITableViewCell and that goes to a new ViewController with a UIWebView. This WebView loads the PDF from a urlRequest using the specified url variable, saved from the NSCoding.
AppDelegate Code:
// Allows incoming file access (PDF)
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"
} else {
// Regular Load
print("App Delegate: No incoming file")
incomingFileTransfer = nil
}
return true
}
IncomingFile code and save button code:
// MARK: Properties
var file: PDFFile?
// MARK: Actions
// Save Button
let name = fileName.text ?? ""
let url = incomingFileTransfer
file = PDFFile(name: name, url: url)
// MARK: NSCoding
func saveFiles() {
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(pdfFiles, toFile: PDFFile.ArchiveURL.path!)
if !isSuccessfulSave {
print("Failed to save PDF file")
}
}
Viewing Saved Incoming PDF Later ViewController code:
// MARK: Properties
#IBOutlet weak var pdfItemWebView: UIWebview!
var incomingURL: NSURL!
// Within ViewDidLoad
if let file = file {
pdfFileName.text = file.name
incomingURL = file.url
print("Saved URL: \(incomingURL)")
print("Pending load...")
let request = NSURLRequest(URL: incomingURL!)
pdfItemWebView.loadRequest(request)
}
It was complicated but worked for me. There may be an easier way, but this is what I figured out and its works for my needs.
Related
I want to store a couple of images locally in my app on the user's device.
What I was using until now (it's still in development):
static func filePath(forKey key: String) -> URL? {
let fileManager = FileManager.default
guard let documentURL = fileManager.urls(for: .documentDirectory,
in: FileManager.SearchPathDomainMask.userDomainMask).first else { return nil }
return documentURL.appendingPathComponent(key + ".png")
}
static func savePhoto(imageKey: String) {
if let filePath = Helpers.filePath(forKey: imageKey) {
do {
try Constants.PHOTO_DATA.write(to: filePath, options: .atomic)
} catch {
print("error")
}
} else {
print(" >>> Error during saving photo. Filepath couldn't be created.")
}
}
static func getPhoto(imageKey: String) -> (image: UIImage, placeholder: Bool) {
if let filePath = Helpers.filePath(forKey: imageKey),
let fileData = FileManager.default.contents(atPath: filePath.path),
let image = UIImage(data: fileData) {
// Retrieve image from device
return (image, false)
}
return (UIImage(named: "placeholder")!, true)
}
Now, during testing I realized that it is not working (but I'm almost 100% sure it was working until now, strange..). It is changing the App's container directory upon every launch.
E.g.
Path:
/var/mobile/Containers/Data/Application/1F3E812E-B128-481C-9724-5E39049D6C81/Documents/D5F14199-CFBF-402A-9894-3487976C4C74.png
Restarting the app, then the path it gives (and where it does not find the image):
/var/mobile/Containers/Data/Application/0A9FCE45-1ED4-46EB-A91B-3ECD56E6A31B/Documents/D5F14199-CFBF-402A-9894-3487976C4C74.png
I read a bit and as far as I see it is 'expected' that it is not working, as the app's directory can change any time the user restarts the app. I should use bookmarkData of the URL class.
My problem is that I couldn't get it working with bookmarkData as I don't really see how should I use it, and couldn't understand its behavior based on some example codes/articles I found. Until now I was simply using URLs to store/retrieve the photo but now I should go with this bookmarkData which is a Data type, which confuses me.
I'm not sure what you want your code means, since both Helper and Constants.PHOTO_DATA are unknown. The code that will definitely will save a UIImage in the documents directory is here:
class ImageSaver {
private let imageStore = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first
//Make this static variable to allow access from all objects without instantiating this class
static var shared : AuxiliaryObjects {
return AuxiliaryObjects()
}
/**
Declaration: save(image : UIImage, with fileName: String, and imageName: String?)
Description: This method saves the received image to the persistent store in the documents directory of the user.
- Parameter image: The UIImage object that must be stored in the documents directory.
- Parameter fileName: A string with the name under which the image must be stored.
- Parameter imageName: The name of the image if needed.
*/
func save(image: UIImage, with fileName: String, and imageName: String?) {
let fileStore = imageStore?.appendingPathComponent(fileName)
let imageData = UIImagePNGRepresentation(image)
do {
try imageData?.write(to: fileStore!)
} catch {
print("Couldn't write the image to disk.")
}
}
/**
Declaration: getImage(with fileName: String, with rectangle: CGRect) -> UIImage?
Description: This method retrieves the image with the specified file name and a given size.
- Parameter fileName: a string with the file name to retrieve.
- Parameter rectangle: the size of the image to return.
- Returns: UIImage?, the image retrieved from the documents directory.
*/
func getImage(with fileName: String, with rectangle: CGRect) -> UIImage? {
var returnImage : UIImage?
var imageRectangle = rectangle
do {
imageStoreArray = try FileManager.default.contentsOfDirectory(at: imageStore!, includingPropertiesForKeys: resourceKeys, options: .skipsHiddenFiles) as [NSURL]
} catch {
return returnImage
}
for url in imageStoreArray {
let urlString = url.lastPathComponent
if urlString == fileName {
let retrievedImage = UIImage(contentsOfFile: url.path!)
//When there is no size set, the original size image is returned
if (rectangle.size.height > 0) || (rectangle.size.width > 0) {
let imageWidth = retrievedImage?.size.width
let imageHeight = retrievedImage?.size.height
if imageWidth! > imageHeight!
{
//The picture is wider than it is high
imageRectangle.size.height *= (imageHeight! / imageWidth!)
} else {
imageRectangle.size.width *= (imageWidth! / imageHeight!)
}
UIGraphicsBeginImageContextWithOptions(imageRectangle.size, false, UIScreen.main.scale)
retrievedImage?.draw(in: imageRectangle)
returnImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
} else {
returnImage = retrievedImage
}
}
}
return returnImage
}
}
Let me know if this works for you.
Kind regards,
MacUserT
I am trying to build and iOS app with similar behaviour to Pages / Numbers / Keynote. Each of these apps is a Document Based App, where the user is first presented with a UIDocumentBrowserViewController where the user choses a document to open in the app. In Numbers for example a user can select a .numbers file and it will open, or a user can select a .csv and it will import this csv file into a numbers file which is saved along side the original csv in the same location.
In my app I want the user to select a .csv file, and then I'll import it into my own document format (called .pivot) and save this alongside the csv file (just like numbers.) This works fine in the simulator but when I run my code on a device I get an error when calling save(to:for:completionHandler:) on my custom Pivot document.
My document browser code is as follows.
class DocumentBrowserViewController: UIDocumentBrowserViewController, UIDocumentBrowserViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
allowsDocumentCreation = false
allowsPickingMultipleItems = false
}
func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentsAt documentURLs: [URL]) {
guard let sourceURL = documentURLs.first else { return }
if sourceURL.pathExtension == "csv" {
// Create a CSV document so we can read the CSV data
let csvDocument = CSVDocument(fileURL: sourceURL)
csvDocument.open { _ in
guard let csv = csvDocument.csvData else {
fatalError("CSV is nil upon open")
}
// Create the file at the same location as the csv, with the same name just a different extension
var pivotURL = sourceURL.deletingLastPathComponent()
let pivotFilename = sourceURL.lastPathComponent .replacingOccurrences(of: "csv", with: "pivot")
pivotURL.appendPathComponent(pivotFilename, isDirectory: false)
let model = PivotModel()
model.csv = csv
let document = PivotDocument(fileURL: pivotURL)
document.model = model
document.save(to: pivotURL, for: .forCreating, completionHandler: { success in
// `success` is false here
DispatchQueue.main.async {
self.performSegue(withIdentifier: "presentPivot", sender: self)
}
})
}
}
}
}
My first UIDocument subclass to load a csv file is as follows.
import SwiftCSV // This is pulled in using SPM and works as I expect, so is unlikely causing this problem
class CSVDocument: UIDocument {
var csvData: CSV?
override func contents(forType typeName: String) throws -> Any {
return Data()
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
guard let data = contents as? Data else {
fatalError("No file data")
}
guard let string = String(data: data, encoding: .utf8) else {
fatalError("Cannot load data into string")
}
csvData = try CSV(string: string)
}
}
My second UIDocument subclass for my custom Pivot document is as follows. By overriding the handleError() function I can see the save fails with an error in the NSCocoaErrorDomain, with code of 513.
class PivotDocument: UIDocument {
var model: PivotModel!
var url: URL!
override func contents(forType typeName: String) throws -> Any {
let encoder = JSONEncoder()
return try encoder.encode(model)
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
guard let data = contents as? Data else {
fatalError("File contents are not Data")
}
let decoder = JSONDecoder()
model = try decoder.decode(PivotModel.self, from: data)
}
override func handleError(_ error: Error, userInteractionPermitted: Bool) {
let theError = error as NSError
print("\(theError.code)") // 513
print("\(theError.domain)") // NSCocoaErrorDomain
print("\(theError.localizedDescription)") // “example.pivot” couldn’t be moved because you don’t have permission to access “CSVs”.
super.handleError(error, userInteractionPermitted: userInteractionPermitted)
}
}
The fact that this works in the simulator (where my user has access to all the file system) but doesn't on iOS (where user and app permissions are different) makes me think I have a permission problem. Do I need to declare some entitlements in my Xcode project for example?
Or am I just misusing the UIDocument API and do I need to find a different implementation?
I found the function I was looking for that replicates the functionality of the iWork apps!
UIDocumentBrowserViewController has this function importDocument(at:nextToDocumentAt:mode:completionHandler:). From the docs:
Use this method to import a document into the same file provider and directory as an existing document.
For example, to duplicate a document that's already managed by a file provider:
Create a duplicate of the original file in the user's temporary directory. Be sure to give it a unique name.
Call importDocument(at:nextToDocumentAt:mode:completionHandler:), passing in the temporary file's URL as the documentURL parameter and the original file's URL as the neighborURL parameter.
So documentBrowser(_:didPickDocumentsAt:) is now:
let pivotFilename = sourceURL.lastPathComponent .replacingOccurrences(of: "csv", with: "pivot")
let path = FileManager.default.temporaryDirectory.appendingPathComponent(pivotFilename)
if FileManager.default.createFile(atPath: path.path, contents: nil, attributes: nil) {
self.importDocument(at: path, nextToDocumentAt: sourceURL, mode: .copy) { (importedURL, errorOrNil) in
guard let pivotURL = importedURL else {
fatalError("No URL for imported document. Error: \n \(errorOrNil?.localizedDescription ?? "NO ERROR")")
}
let model = PivotModel()
model.csv = csv
let document = PivotDocument(fileURL: pivotURL)
document.model = model
DispatchQueue.main.async {
self.performSegue(withIdentifier: "presentPivot", sender: self)
}
}
}
else {
fatalError("Could not create local pivot file in temp dir")
}
No more permissions errors. Hope this helps someone else in the future.
I got the following function which got created from swagger code gen:
open class func uploadFile(firstname: String, lastname: String, timestamp: Date, file: URL, completion: #escaping ((_ data: ApiResponse?,_ error: Error?) -> Void)) {
In the app you can make an image with the camera and the image got converted to a pdf file:
let document = PDFDocument()
let pdfPage = PDFPage(image: unwrapImage)
document.insert(pdfPage!,at: 0)
So now I want to upload this document. But document.documentURL is always nil. Although I can display the pdf docuemnt on the display. Am I supposed to save the pdf document to a temp directory to use the function with the url parameter?
PDFDocument's property documentURL is get only. If you do not use the url initializer it will always return nil. What you need is to get your PDFDocument dataRepresentation and write the pdf data to a url at a temporary or permanent location. Then you can upload its URL.
let document = PDFDocument()
let image = UIImage(named: "imageName.jpg")!
if let pdfPage = PDFPage(image: image) {
document.insert(pdfPage,at: 0)
do {
print(FileManager.default.temporaryDirectory.path)
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent("pdfName.pdf")
try document.dataRepresentation()?.write(to: fileURL)
// upload your fileURL or copy to a permanent location
} catch {
print(error)
}
}
I created a stop button that can collect data, which will be saved to the defined path after clicking the stop button. However, if I want to continue collecting after clicking the stop button, the data will be added to the original text file. (This makes senses as I only know how to define one path)
My question is: Would it be possible to ask the user and input a new file name and save as a new text file after each stop so that the data is not added to the original file?
Below is what I have for one defined path and stacking up the data:
#IBAction func stopbuttonTapped(_ btn: UIButton) {
do {
let username:String = user_name.text!
fpsTimer.invalidate() //turn off the timer
let capdata = captureData.map{$0.verticesFormatted}.joined(separator:"") //convert capture data to string
let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last! as URL
let url = dir.appendingPathComponent("testing.txt") //name the file
try capdata.appendLineToURL(fileURL: url as URL)
let result = try String(contentsOf: url as URL, encoding: String.Encoding.utf8)
}
catch {
print("Could not write to file")
}
}
And the extension I use for string and data:
extension String {
func appendLineToURL(fileURL: URL) throws {
try (self).appendToURL(fileURL: fileURL)
}
func appendToURL(fileURL: URL) throws {
let data = self.data(using: String.Encoding.utf8)!
try data.append(fileURL: fileURL)
}
func trim() -> String
{
return self.trimmingCharacters(in: CharacterSet.whitespaces)
}
}
extension Data {
func append(fileURL: URL) throws {
if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
}
else {
try write(to: fileURL, options: .atomic)
}
}
}
Do I need to set a default file name (maybe texting.txt) and then popped up a user input for saving the text file? (That's where I am not too sure how to integrate to what I already have). I thank you so much for your time and suggestions in advance.
You could generate unique names.
For example:
let url = dir.appendingPathComponent("testing-\(Date.timeIntervalSinceReferenceDate).txt")
or
let url = dir.appendingPathComponent("testing-\(UUID().uuidString).txt")
I have created an app group group.com.example.FoodTracker-qg, In the main view controller of the app I am downloading an image and storing inside the shared container but I am unable to use the same image in image view. I am getting the following error
fatal error: 'try!' expression unexpectedly raised an error: Error Domain=NSCocoaErrorDomain Code=256 "The file “EC700C58-E9C9-480D-8EE2-B88A570A5728image.jpg” couldn’t be opened." UserInfo={NSURL=/private/var/mobile/Containers/Shared/AppGroup/EC700C58-E9C9-480D-8EE2-B88A570A5728image.jpg}: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-800.0.63/src/swift/stdlib/public/core/ErrorType.swift, line 178
Below is my code for writing and reading from shared container
// ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var mealName: UILabel!
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
let urlString = "https://static.pexels.com/photos/3247/nature-forest-industry-rails.jpg"
try? Data(contentsOf: URL(string: urlString)!).write(to: getSharedFileUrl("image.jpg"))
imageView.image = UIImage(data: try! Data(contentsOf: getSharedFileUrl("image.jpg")))
}
func getSharedFileUrl(_ fileName: String) -> URL {
let fileManager = FileManager.default
let url = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.com.example.FoodTracker-qg")
return URL(string: url!.path.appending(fileName))!
}
}
Code seems to be correct, your problem might be due to file name itself....
check url in error
/private/var/mobile/Containers/Shared/AppGroup/EC700C58-E9C9-480D-8EE2-B88A570A5728image.jpg
there's no "/"...it should look like 728/image.jpg
In getSharedFileUrl(_ fileName: String) -> URL why deconstruct the newly retrieved url from containerURL(.. just to create a new one?
You could do something like this instead:
return url!.appending(fileName)
This should fix your problem with the missing /.
I'm not sure I agree with those force wraps, or even with possibly blocking the Main Thread with Data(contentsOf: ..) though!
You append the filename. As a result, you are missing the slash / in between the path components, as JJAAXX44 correctly pinpointed.
Use appendingPathComponentinstead:
func getSharedFileUrl(_ fileName: String) -> URL {
let fileManager = FileManager.default
guard let url = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.com.example.FoodTracker-qg")
else {
fatalError() // handle error appropriate to your needs instead
}
return url.appendingPathComponent(fileName)
}
Your write action is not correct.So you can not be read you image form your shared container.
let appGroupIdentifier = "group.mark3"
extension Data {
func writeToGroupContainer(filePath: String, completeHandle: #escaping (Bool, String) -> Void) {
let url = Data.appGroupContainerURL(filePath: filePath)
let result = FileManager.default.createFile(atPath: url.path, contents: self, attributes: nil)
completeHandle(result, url.path)
}
static func appGroupContainerURL(filePath: String) -> URL {
let fileManager = FileManager.default
let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
let url = groupURL!.appendingPathComponent(filePath)
try? FileManager.default.createDirectory(atPath: url.deletingLastPathComponent().path, withIntermediateDirectories: true, attributes: nil)
return url
}
}
us this extension to write a data to your disk correct.
override func viewDidLoad() {
super.viewDidLoad()
let filePath = "images/group/mark3/avatar.png"
let data: Data = UIImagePNGRepresentation(UIImage(named: "image.png")!)!
data.writeToGroupContainer(filePath: filePath) { (done, file) in
print(filePath, done)
}
}