I’m writing a sharing extension that will accept images and perform some action with them. Within a method of my UIViewController subclass, I can access URLs to a particular representation of the files by writing this:
guard let context = self.extensionContext else {
return
}
guard let items = context.inputItems as? [NSExtensionItem] else {
return
}
for item in items {
guard let attachments = item.attachments else {
continue
}
for attachment in attachments {
guard attachment.hasItemConformingToTypeIdentifier("public.jpeg") else {
continue
}
attachment.loadFileRepresentation(forTypeIdentifier: "public.jpeg") { (url, error) in
if let url = url {
// How long is this "url" valid?
}
}
}
}
In the block I pass to loadFileRepresentation(forTypeIdentifier:completionHandler:), I’m given the URL to a file—in this case, a JPEG. Can I assume anything about how long this URL is valid? Specifically, is it safe to write the URL itself to some shared storage area so that my app can access the pointed-to file later? Or should I assume that the URL is ephemeral and that, if I want access to the file it points at, I should make my own copy of that file within this block?
The documentation for loadFileRepresentation states:
This method writes a copy of the file’s data to a temporary file, which the system deletes when the completion handler returns.
So url is valid to the closing curly brace of the completion handler.
You need to copy the file to a known location with the sandbox before you are done in the completion handler if you need access to the file beyond the completion handler.
Related
I currently have a view controller where the user uploads an image and that image is stored in the Firebase Storage, with folders of their user id and in there their uploaded image. I want to fetch that image's url and display it on the view controller. The question I have is, do i need to store that uploaded image's download url into the realtime database ie; Users - > User Id - > Media -> Image Title -> Download URL? Or is there a way for me to refer an image view to Firebase storage and accordingly into the User Id's folder containing all the images and display this? Would appreciate it a lot if someone could help me out. Thank you!
This is my code:
let imageName = (Auth.auth().currentUser?.uid)!+"/\(imageTitle.text!)"
let imageReference = Storage.storage().reference().child(MyKeys.imagesFolder).child(imageName)
imageReference.putData(data, metadata: nil) { (metadata, err) in
if let err = err {
print ("Error")
return
}
imageReference.downloadURL(completion: { (url, err) in
if let err = err {
print ("Error")
return
}
guard let url = url else {
print ("Error")
return
}
let dataReference = Firestore.firestore().collection(MyKeys.imagesCollection).document()
let documentUid = dataReference.documentID
let urlString = url.absoluteString
let data = [
MyKeys.uid:documentUid,
MyKeys.imageUrl: urlString,
MyKeys.imageTitle: self.imageTitle.text!,
] as [String : Any]
dataReference.setData(data) { (err) in
if let err = err {
print ("Error")
return
}
}
})
}
There are two ways to read the data from Storage:
Through the Firebase SDK by calling getData or write on a reference, as shown in the documentation on downloading data.
Through a download URL for the reference, which provides public read-only access.
To call getData, write or to get a download URL, you need to have a reference to the file as shown in creating a reference.
Since you store the files under the UID of the user, you can always create a reference to any for for a user if you known their UID. So you can always perform one of the two methods above to read the data for the file, even if you didn't store the download URL.
That said, it is fairly common to store the download URL in a database, as it means you can then treat it like any other image URL. By doing so the rest of your code won't have to know anything about Cloud Storage.
So: it can work without storing the download URL in the database, but it's also fine (and common) if you do store the download URL. The choice is really up to you.
I've never really gotten the nuances of async operations so time and again, I get stymied. And I just can't figure it out.
I'm trying to do some very simple web scraping.
My local volleyball association has a page (verbose HTML, not responsive, not mobile-friendly, yaddah, yaddah, yaddah) which shows the refs assigned to each game of the season. I'm trying to write a silly little app which will scrape that page (no API, no direct access to db, etc.) and display the data in a grouped table. The first group will show today's matches (time, home team, away team). The second group will show tomorrow's matches. Third group shows the entire season's matches.
Using code I found elsewhere, my viewDidLoad loads the page, scrapes the data and parses it into an array. Once I've parsed the data, I have three arrays: today, tomorrow, and matches, all are [Match].
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: urlString)!
let request = NSMutableURLRequest(url: url)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if let error = error {
print (error)
} else {
if let unwrappedData = data {
// scrape, scrape, parse, parse
matchRow = ...
self.matches.append(matchRow)
if matchRow.date == todaysDate {
self.today.append(matchRow)
} else if matchRow.date == tomorrowsDate {
self.tomorrow.append(matchRow)
}
}
}
}
task.resume()
}
As I'm sure is no surprise to anyone who understands async operations, my table is empty. I've checked and I see the the data is there and properly parsed, etc. But I can't for the life of me figure out how get the data in my table. The way I have it now, the data is not ready when numberOfSections or numberOfRowsInSection is called.
I've found the Ray Wenderlich tutorial on URLSession and I also have a Udemy course (Rob Percival) that builds an app to get the weather using web scraping, but in both those instances, the app starts and waits for user input before going out to the web to get the data. I want my app to get the data immediately upon launch, without user interaction. But I just can't figure out what changes I need to make so that those examples work with my program.
Help, please.
You can simply reload the tableviews once the data arrays are getting populated from the URLSession completion block. Have you tried that. Sample snippet may be like the one follows.
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if let error = error {
print (error)
} else {
if let unwrappedData = data {
// scrape, scrape, parse, parse
matchRow = ...
self.matches.append(matchRow)
if matchRow.date == todaysDate {
self.today.append(matchRow)
} else if matchRow.date == tomorrowsDate {
self.tomorrow.append(matchRow)
}
}
DispatchQueue.main.async { [weak self] in
self?.todayTableView.reloadData()
self?.tomorrowTableView.reloadData()
}
}
}
I have an S3Service which is a singleton that manages all the S3 related uploads and downloads.
When I upload the first image it works fine but if I try to upload an Image consecutively It gives me this warning and the completion block never gets called.
A background URLSession with identifier com.amazonaws.AWSS3TransferUtility.Identifier.TransferManager already exists.
This is how I upload method looks:
if let data = image.jpegData(compressionQuality: 0.5) {
let transferUtility = AWSS3TransferUtility.s3TransferUtility(forKey: S3Service.TRANSFER_MANAGER_KEY)
transferUtility.uploadUsingMultiPart(data: data, bucket: EnvironmentUtils.getBucketName(), key: filename, contentType: "image/jpg", expression: nil, completionHandler: { task,error in
if let error = error {
print(error.localizedDescription)
} else {
print("Image upload success")
}
})
}
The call to register transfer utility AWSS3TransferUtility.register(with: serviceconfig, forKey: KEY) was causing the above issue. There are two things that should be kept in mind.
The AWSS3TransferUtility should be registered only once per Application session. Then we can use AWSS3TransferUtility.S3TransferUtilityForKey to get the instance wherever needed.
If these are for different users within the app, ( e.g. sign-up) and if we want to keep AWSS3TransferUtility separate for each user, register AWSS3TransferUtility with a different key (preferably the same key for the same user) and look up using that key.
I'm using this code to give the user the choice to specify a name and a location where to save a plain text file on disk. All seems to work but the saved file hasn't any extension. Actually I have not specify an extension in any part of my code, I read NSSavePanel documentation without notice the part where explained this option.
Here is the code I'm using:
let textToExport = mainTextField.textStorage?.string
if textToExport != "" {
let mySave = NSSavePanel()
mySave.begin { (result) -> Void in
if result == NSFileHandlingPanelOKButton {
let filename = mySave.url
do {
try textToExport?.write(to: filename!, atomically: true, encoding: String.Encoding.utf8)
} catch {
// failed to write file (bad permissions, bad filename etc.)
}
} else {
NSBeep()
}
}
}
Add the line
mySave.allowedFileTypes = ["txt"]
before presenting the panel.
From the documentation:
The value of this property specifies the file types the user can save
the file as. A file type can be a common file extension, or a UTI. The
default value of this property is nil, which indicates that any file
type can be used. (Note that if the array is not nil and the array
contains no items, an exception is raised.)
If no extension is given by the user, the first item in the
allowedFileTypes array will be used as the extension for the save
panel. If the user specifies a type not in the array, and
allowsOtherFileTypes is true, they will be presented with another
dialog when prompted to save.
I have a desktop app that receives e-mail URLs ("message://" scheme) from the drag&drop pasteboard and I want to get the Subject from the relevant message. The only clue I have, so far, is that the QuickLook library might give me an information object where I can retrieve this info from.
Since the QuickLook API seems to be rather in flux at the moment and most examples show how to use it in iOS, I simply cannot find a way to set up my "Preview" object using a URL and get the information from there.
I would like to avoid setting up my project as a QuickLook plugin, or setting up the whole preview pane / view scaffolding; at the moment I just want to get out what QuickLook loads before it starts displaying, but I can't comprehend what paradigm Apple wants me to implement here.
XCode 7.3.1.
It turns out I misinterpreted the contents of draggingInfo.draggingPasteboard().types as a hierarchical list containing only one type of info (URL in this case).
Had to subscribe to dragged event type kUTTypeMessage as String and retrieve the e-mail subject from the pasteboard with stringForType("public.url-name")
EDIT: Note that the current Mail.app will sometimes create a stack of mails when you drag an e-mail thread. Although the method above still works to get the subject of the stack, there is no URL in the dragging info then and since there's no list of Message-IDs available either, I had to resort to scraping the user's mbox directory:
// See if we can resolve e-mail message meta data
if let mboxPath = pboard.stringForType("com.apple.mail.PasteboardTypeMessageTransfer") {
if let automatorPlist = pboard.propertyListForType("com.apple.mail.PasteboardTypeAutomator") {
// Get the latest e-mail in the thread
if let maxID = (automatorPlist.allObjects.flatMap({ $0["id"]! }) as AnyObject).valueForKeyPath("#max.self") as? Int {
// Read its meta data in the background
let emailItem = draggingEmailItem
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
// Find the e-mail file
if let path = Util.findEmlById(searchPath: mboxPath, id: maxID) {
// Read its contents
emailItem.properties = Util.metaDataFromEml(path)
dispatch_async(dispatch_get_main_queue(), {
// Update UI
});
}
}
}
}
}
Util funcs:
/* Searches the given path for <id>.eml[x] and returns its URL if found
*/
static func findEmlById(searchPath searchPath: String, id: Int)-> NSURL? {
let enumerator = NSFileManager.defaultManager().enumeratorAtPath(searchPath)
while let element = enumerator?.nextObject() as? NSString {
switch (element.lastPathComponent, element.pathExtension) {
case (let lpc, "emlx") where lpc.hasPrefix("\(id)"):
return NSURL(fileURLWithPath: searchPath).URLByAppendingPathComponent(element as String)!
case (let lpc, "eml") where lpc.hasPrefix("\(id)"):
return NSURL(fileURLWithPath: searchPath).URLByAppendingPathComponent(element as String)!
default: ()
}
}
return nil
}
/* Reads an eml[x] file and parses it, looking for e-mail meta data
*/
static func metaDataFromEml(path: NSURL)-> Dictionary<String, AnyObject> {
// TODO Support more fields
var properties: Dictionary<String, AnyObject> = [:]
do {
let emlxContent = try String(contentsOfURL: path, encoding: NSUTF8StringEncoding)
// Parse message ID from "...\nMessage-ID: <...>"
let messageIdStrMatches = emlxContent.regexMatches("[\\n\\r].*Message-ID:\\s*<([^\n\r]*)>")
if !messageIdStrMatches.isEmpty {
properties["messageId"] = messageIdStrMatches[0] as String
}
}
catch {
print("ERROR: Failed to open emlx file")
}
return properties
}
Note: If your app is sandboxed you will need the com.apple.security.temporary-exception.files.home-relative-path.read-only entitlement set to an array with one string in it: /Library/