You can do it sneakily† using the undocumented PHAsset.ALAssetURL property, but I'm looking for something documented.
† In Objective-C, this will help
#interface PHAsset (Sneaky)
#property (nonatomic, readonly) NSURL *ALAssetURL;
#end
Create the assetURL by leveraging the localidentifier of the PHAsset.
Example:
PHAsset.localidentifier returns 91B1C271-C617-49CE-A074-E391BA7F843F/L0/001
Now take the 32 first characters to build the assetURL, like:
assets-library://asset/asset.JPG?id=91B1C271-C617-49CE-A074-E391BA7F843F&ext=JPG
You might change the extension JPG depending on the UTI of the asset (requestImageDataForAsset returns the UTI), but in my testing the extensions of the assetURL seems to be ignored anyhow.
I wanted to be able to get a URL for an asset too. However, I have realised that the localIdentifier can be persisted instead and used to recover the PHAsset.
PHAsset* asset = [PHAsset fetchAssetsWithLocalIdentifiers:#[localIdentifier] options:nil].firstObject;
Legacy asset URLs can be converted using:
PHAsset* legacyAsset = [PHAsset fetchAssetsWithALAssetUrls:#[assetUrl] options:nil].firstObject;
NSString* convertedIdentifier = legacyAsset.localIdentifier;
(before that method gets obsoleted...)
(Thanks holtmann - localIdentifier is hidden away in PHObject.)
Here is working code tested on iOS 11 both simulator and device
PHFetchResult *result = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage options:nil];
[result enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
PHAsset *asset = (PHAsset *)obj;
[asset requestContentEditingInputWithOptions:nil completionHandler:^(PHContentEditingInput * _Nullable contentEditingInput, NSDictionary * _Nonnull info) {
NSLog(#"URL:%#", contentEditingInput.fullSizeImageURL.absoluteString);
NSString* path = [contentEditingInput.fullSizeImageURL.absoluteString substringFromIndex:7];//screw all the crap of file://
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isExist = [fileManager fileExistsAtPath:path];
if (isExist)
NSLog(#"oh yeah");
else {
NSLog(#"damn");
}
}];
}];
Read the bottom!
The resultHandler for PHImageManager.requestImage returns 2 objects: result and info.
You can get the original filename for the PHAsset (like IMG_1043.JPG) as well as its full path on the filesystem with:
let url = info?["PHImageFileURLKey"] as! URL
This should work right, but for some reason it doesn't. So basically, you have to copy your image to a file then access that then delete it.
The PHImageFileURLKey is usable to get the original file name, but you cannot actually access that file. It probably has to do with the fact that code in the background can access the file while other apps can delete it.
Here is a PHAsset extension written in Swift that will retrieve the URL.
extension PHAsset {
func getURL(completionHandler : #escaping ((_ responseURL : URL?) -> Void)){
if self.mediaType == .image {
let options: PHContentEditingInputRequestOptions = PHContentEditingInputRequestOptions()
options.canHandleAdjustmentData = {(adjustmeta: PHAdjustmentData) -> Bool in
return true
}
self.requestContentEditingInput(with: options, completionHandler: {(contentEditingInput: PHContentEditingInput?, info: [AnyHashable : Any]) -> Void in
completionHandler(contentEditingInput!.fullSizeImageURL as URL?)
})
} else if self.mediaType == .video {
let options: PHVideoRequestOptions = PHVideoRequestOptions()
options.version = .original
PHImageManager.default().requestAVAsset(forVideo: self, options: options, resultHandler: {(asset: AVAsset?, audioMix: AVAudioMix?, info: [AnyHashable : Any]?) -> Void in
if let urlAsset = asset as? AVURLAsset {
let localVideoUrl: URL = urlAsset.url as URL
completionHandler(localVideoUrl)
} else {
completionHandler(nil)
}
})
}
}
}
Related
With file access in a sandboxed osx app with swift in mind, does it work the same with URLs provided via Finder or other apps drops?
As there's no NSOpenPanel call to afford folder access as in this example, just urls - I think the folder access is implicit since the user dragged the file from the source / desktop "folder" much the same as implicit selection via the open dialog.
I have not begun the sandbox migration yet but wanted to verify my thinking was accurate, but here's a candidate routine that does not work in sandbox mode:
func performDragOperation(_ sender: NSDraggingInfo!) -> Bool {
let pboard = sender.draggingPasteboard()
let items = pboard.pasteboardItems
if (pboard.types?.contains(NSURLPboardType))! {
for item in items! {
if let urlString = item.string(forType: kUTTypeURL as String) {
self.webViewController.loadURL(text: urlString)
}
else
if let urlString = item.string(forType: kUTTypeFileURL as String/*"public.file-url"*/) {
let fileURL = NSURL.init(string: urlString)?.filePathURL
self.webViewController.loadURL(url: fileURL!)
}
else
{
Swift.print("items has \(item.types)")
}
}
}
else
if (pboard.types?.contains(NSPasteboardURLReadingFileURLsOnlyKey))! {
Swift.print("we have NSPasteboardURLReadingFileURLsOnlyKey")
}
return true
}
as no URL is acted upon or error thrown.
Yes, the file access is implicit. As the sandbox implementation is poorly documented and had/has many bugs, you want to work around URL and Filenames. The view should register itself for both types at initialisation. Code is in Objective-C, but API should be the same.
[self registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, NSURLPboardType, nil]];
Then on performDragOperation:
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
BOOL dragPerformed = NO;
NSPasteboard *paste = [sender draggingPasteboard];
NSArray *typesWeRead = [NSArray arrayWithObjects:NSFilenamesPboardType, NSURLPboardType, nil];
//a list of types that we can accept
NSString *typeInPasteboard = [paste availableTypeFromArray:typesWeRead];
if ([typeInPasteboard isEqualToString:NSFilenamesPboardType]) {
NSArray *fileArray = [paste propertyListForType:#"NSFilenamesPboardType"];
//be careful since this method returns id.
//We just happen to know that it will be an array. and it contains strings.
NSMutableArray *urlArray = [NSMutableArray arrayWithCapacity:[fileArray count]];
for (NSString *path in fileArray) {
[urlArray addObject:[NSURL fileURLWithPath:path]];
}
dragPerformed = //.... do your stuff with the files;
} else if ([typeInPasteboard isEqualToString:NSURLPboardType]) {
NSURL *droppedURL = [NSURL URLFromPasteboard:paste];
if ([droppedURL isFileURL]) {
dragPerformed = //.... do your stuff with the files;
}
}
return dragPerformed;
}
I'm converting our app over to use the Photos Framework of iOS8, the ALAsset framework is clearly a second class citizen under iOS8.
I'm having a problem is that our architecture really wants an NSURL that represents the location of the media on "disk." We use this to upload the media to our servers for further processing.
This was easy with ALAsset:
ALAssetRepresentation *rep = [asset defaultRepresentation];
self.originalVideo = rep.url;
But I'm just not seeing this ability in PHAsset. I guess I can call:
imageManager.requestImageDataForAsset
and then write it out to a temp spot in the file system but that seems awfully heavyweight and wasteful, not to mention potentially slow.
Is there a way to get this or am I going to have refactor more of my app to only use NSURLs for iOS7 and some other method for iOS8?
If you use [imageManager requestAVAssetForVideo...], it'll return an AVAsset. That AVAsset is actually an AVURLAsset, so if you cast it, you can access it's -url property.
I'm not sure if you can create a new asset out of this, but it does give you the location.
SWIFT 2.0 version
This function returns NSURL from PHAsset (both image and video)
func getAssetUrl(mPhasset : PHAsset, completionHandler : ((responseURL : NSURL?) -> Void)){
if mPhasset.mediaType == .Image {
let options: PHContentEditingInputRequestOptions = PHContentEditingInputRequestOptions()
options.canHandleAdjustmentData = {(adjustmeta: PHAdjustmentData) -> Bool in
return true
}
mPhasset.requestContentEditingInputWithOptions(options, completionHandler: {(contentEditingInput: PHContentEditingInput?, info: [NSObject : AnyObject]) -> Void in
completionHandler(responseURL : contentEditingInput!.fullSizeImageURL)
})
} else if mPhasset.mediaType == .Video {
let options: PHVideoRequestOptions = PHVideoRequestOptions()
options.version = .Original
PHImageManager.defaultManager().requestAVAssetForVideo(mPhasset, options: options, resultHandler: {(asset: AVAsset?, audioMix: AVAudioMix?, info: [NSObject : AnyObject]?) -> Void in
if let urlAsset = asset as? AVURLAsset {
let localVideoUrl : NSURL = urlAsset.URL
completionHandler(responseURL : localVideoUrl)
} else {
completionHandler(responseURL : nil)
}
})
}
}
If you have a PHAsset, you can get the url for said asset like this:
[asset requestContentEditingInputWithOptions:editOptions
completionHandler:^(PHContentEditingInput *contentEditingInput, NSDictionary *info) {
NSURL *imageURL = contentEditingInput.fullSizeImageURL;
}];
Use the new localIdentifier property of PHObject. (PHAsset inherits from this).
It provides similar functionality to an ALAsset URL, namely that you can load assets by calling the method
+[PHAsset fetchAssetsWithLocalIdentifiers:identifiers options:options]
All the above solutions won't work for slow-motion videos. A solution that I found handles all video asset types is this:
func createFileURLFromVideoPHAsset(asset: PHAsset, destinationURL: NSURL) {
PHCachingImageManager().requestAVAssetForVideo(self, options: nil) { avAsset, _, _ in
let exportSession = AVAssetExportSession(asset: avAsset!, presetName: AVAssetExportPresetHighestQuality)!
exportSession.outputFileType = AVFileTypeMPEG4
exportSession.outputURL = destinationURL
exportSession.exportAsynchronouslyWithCompletionHandler {
guard exportSession.error == nil else {
log.error("Error exporting video asset: \(exportSession.error)")
return
}
// It worked! You can find your file at: destinationURL
}
}
}
See this answer here.
And this one here.
In my experience you'll need to first export the asset to disk in order to get a fully accessible / reliable URL.
The answers linked to above describe how to do this.
Just want to post the hidden gem from a comment from #jlw
#rishu1992 For slo-mo videos, grab the AVComposition's
AVCompositionTrack (of mediaType AVMediaTypeVideo), grab its first
segment (of type AVCompositionTrackSegment), and then access its
sourceURL property. – jlw Aug 25 '15 at 11:52
In speking of url from PHAsset, I had once prepared a util func on Swift 2 (although only for playing videos from PHAsset). Sharing it in this answer, might help someone.
static func playVideo (view:UIViewController, asset:PHAsset)
Please check this Answer
Here's a handy PHAsset category:
#implementation PHAsset (Utils)
- (NSURL *)fileURL {
__block NSURL *url = nil;
switch (self.mediaType) {
case PHAssetMediaTypeImage: {
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.synchronous = YES;
[PHImageManager.defaultManager requestImageDataForAsset:self
options:options
resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
url = info[#"PHImageFileURLKey"];
}];
break;
}
case PHAssetMediaTypeVideo: {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[PHImageManager.defaultManager requestAVAssetForVideo:self
options:nil
resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {
if ([asset isKindOfClass:AVURLAsset.class]) {
url = [(AVURLAsset *)asset URL];
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
break;
}
default:
break;
}
return url;
}
#end
I had similiar problem with video files, what worked for me was:
NSString* assetID = [asset.localIdentifier substringToIndex:(asset.localIdentifier.length - 7)];
NSURL* videoURL = [NSURL URLWithString:[NSString stringWithFormat:#"assets-library://asset/asset.mov?id=%#&ext=mov", assetID]];
Where asset is PHAsset.
I'm adding a Settings.bundle file to my iOS application. It's very minimal with one or two settings. The plist file in the Settings.bundle specifies the defaults, to the Settings application. I've read that to make my iOS application aware of these defaults, I have to also code them into my application. This seems like repeating myself and leaving an opening for defaults to easily get out of sync as I modify the program.
I know I can register the defaults using the contents of plist file, like so:
[[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"Defaults" ofType:#"plist"]]];
That also seems like repeating myself, though. Would it be possible to use the plist file from the Settings.bundle as the source of these defaults, so that I only specify defaults in 1 location?
I trued adjusting that load to look something like this:
[[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"Settings.bundle/Root" ofType:#"plist"]]];
That did not work, though. Does anybody know if this is possible, and how to load that plist file?
Wanted to add this as a comment to Andy's answer, but Stack Overflow didn't let me (too long for a comment).
If you're using ObjC here's your version:
-(void)registerDefaults
{
NSString* pathString = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:#"Settings.bundle"];
NSString* rootPath = [pathString stringByAppendingPathComponent:#"Root.plist"];
NSDictionary* settingsDict = [NSDictionary dictionaryWithContentsOfFile:rootPath];
NSArray* prefSpecifiers = settingsDict[#"PreferenceSpecifiers"] ;
NSMutableDictionary* defaults = [NSMutableDictionary dictionary];
for (NSDictionary* item in prefSpecifiers) {
NSString* theKey = item[#"Key"];
NSObject* defaultValue = item[#"DefaultValue"];
if (defaultValue && theKey) {
defaults[theKey] = defaultValue;
}
}
if (defaults.count > 0) {
[[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
}
}
Yes, it is possible. Here's how you would do it in Swift:
func loadDefaults() {
let pathStr = NSBundle.mainBundle().bundlePath
let settingsBundlePath = pathStr.stringByAppendingPathComponent("Settings.bundle")
let finalPath = settingsBundlePath.stringByAppendingPathComponent("Root.plist")
let settingsDict = NSDictionary(contentsOfFile: finalPath)
let prefSpecifierArray = settingsDict?.objectForKey("PreferenceSpecifiers") as NSArray
var defaults:[NSObject:AnyObject] = [:]
for prefItem in prefSpecifierArray {
if let key = prefItem.objectForKey("Key") as String! {
let defaultValue:AnyObject? = prefItem.objectForKey("DefaultValue")
defaults[key] = defaultValue
}
}
_userDefaults.registerDefaults(defaults)
}
That will load the DefaultValue from each PreferenceSpecifier with a Key. For example:
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Frequency</string>
<key>Key</key>
<string>frequency</string>
<key>DefaultValue</key>
<integer>15</integer>
<key>Titles</key>
<array>
<string>5 minutes</string>
<string>15 minutes</string>
<string>30 minutes</string>
<string>60 minutes</string>
</array>
<key>Values</key>
<array>
<integer>5</integer>
<integer>15</integer>
<integer>30</integer>
<integer>60</integer>
</array>
</dict>
The loadDefaults function will load the default value of 15 into the user default for "frequency".
I recommend you call the loadDefaults function your app delegate, from the didFinishLaunchingWithOptions handler, like this:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
loadDefaults()
return true
}
Then later in your app, say in a UITableViewController, you can load the user default value like this:
let kFrequency = "frequency"
frequency = _userDefaults.integerForKey(kFrequency)
Based on Andy's answer above, adapted for Swift 4, and avoiding the use of force-unwrap/force-cast:
private func loadDefaults() {
let userDefaults = UserDefaults.standard
let pathStr = Bundle.main.bundlePath
let settingsBundlePath = (pathStr as NSString).appendingPathComponent("Settings.bundle")
let finalPath = (settingsBundlePath as NSString).appendingPathComponent("Root.plist")
let settingsDict = NSDictionary(contentsOfFile: finalPath)
guard let prefSpecifierArray = settingsDict?.object(forKey: "PreferenceSpecifiers") as? [[String: Any]] else {
return
}
var defaults = [String: Any]()
for prefItem in prefSpecifierArray {
guard let key = prefItem["Key"] as? String else {
continue
}
defaults[key] = prefItem["DefaultValue"]
}
userDefaults.register(defaults: defaults)
}
That is not necessary, you can simply call the data in the plist by calling the key that it was assigned to in the plist, for example,
_buttonSelection = [[NSUserDefaults standardUserDefaults] stringForKey:#"buttonAction01"]; //_buttonSelection is a NSString so that can be anything
I want to customize text for the same information but when I am sharing it on Facebook I don't want to use the twitter hash tags or #username scheme...
How can I diversify text for sharing based on which sharing service would be used?
Ofcourse I'm using UIActivityViewController:
UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:#[shareText, shareURL] applicationActivities:nil];
I took this answer and made a simple class for it. The default message will be seen by sharing outlets other than Twitter, and for Twitter words within the hashWords array will appear with hashes if they are present in the default message. I thought I would share it for anyone else who needs it. Thanks Christopher!
Usage:
TwitterHashActivityItemProvider *twit = [[TwitterHashActivityItemProvider alloc] initWithDefaultText:#"I really like stackoverflow and code"
hashWords:#[#"stackoverflow", #"code"]];
NSArray *items = #[twit];
UIActivityViewController *act = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil];
Header:
#interface TwitterHashActivityItemProvider : UIActivityItemProvider
- (id)initWithDefaultText:(NSString*)text hashWords:(NSArray*)hashItems;
#property (nonatomic,strong) NSArray *hashItems;
#end
Implementation:
#import "TwitterHashActivityItemProvider.h"
#implementation TwitterHashActivityItemProvider
- (id)initWithDefaultText:(NSString*)text hashWords:(NSArray*)hashItems;
{
self = [super initWithPlaceholderItem:text];
if ( self )
{
self.hashItems = hashItems;
}
return self;
}
- (id)item
{
if ( [self.placeholderItem isKindOfClass:[NSString class]] )
{
NSString *outputString = [self.placeholderItem copy];
// twitter gets some hash tags!
if ( self.activityType == UIActivityTypePostToTwitter )
{
// go through each potential hash item and augment the main string
for ( NSString *hashItem in self.hashItems)
{
NSString *hashed = [#"#" stringByAppendingString:hashItem];
outputString = [outputString stringByReplacingOccurrencesOfString:hashItem withString:hashed];
}
}
return outputString;
}
// else we didn't actually provide a string...oops...just return the placeholder
return self.placeholderItem;
}
#end
Instead of passing the text strings into the initWithActivityItems call, pass in your own sub-class of the UIActivityItemProvider class and when you implement the itemForActivityType method it will provide the sharing service as the 'activityType' parameter.
You can then return the customized content from this method.
Swift implementation example of an UIActivityItemProvider subclass. Copy option will use only the password, other activity types will use the full share text. Should be easy to customize for different use cases. Credit to Cristopher & NickNack for their answers.
class PasswordShareItemsProvider: UIActivityItemProvider {
private let password: String
private var shareText: String {
return "This is my password: " + password
}
init(password: String) {
self.password = password
// the type of the placeholder item is used to
// display correct activity types by UIActivityControler
super.init(placeholderItem: password)
}
override var item: Any {
get {
guard let activityType = activityType else {
return shareText
}
// return desired item depending on activityType
switch activityType {
case .copyToPasteboard: return password
default: return shareText
}
}
}
}
Usage:
let itemProvider = PasswordShareItemsProvider(password: password)
let activityViewController = UIActivityViewController(activityItems: [itemProvider], applicationActivities: nil)
How do you delete all the contents of a directory without deleting the directory itself? I want to basically empty a folder yet leave it (and the permissions) intact.
E.g. by using a directory enumerator:
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:path];
NSString *file;
while (file = [enumerator nextObject]) {
NSError *error = nil;
BOOL result = [fileManager removeItemAtPath:[path stringByAppendingPathComponent:file] error:&error];
if (!result && error) {
NSLog(#"Error: %#", error);
}
}
Swift
let fileManager = NSFileManager.defaultManager()
let enumerator = fileManager.enumeratorAtURL(cacheURL, includingPropertiesForKeys: nil, options: nil, errorHandler: nil)
while let file = enumerator?.nextObject() as? String {
fileManager.removeItemAtURL(cacheURL.URLByAppendingPathComponent(file), error: nil)
}
Try this:
NSFileManager *manager = [NSFileManager defaultManager];
NSString *dirToEmpty = ... //directory to empty
NSError *error = nil;
NSArray *files = [manager contentsOfDirectoryAtPath:dirToEmpty
error:&error];
if(error) {
//deal with error and bail.
}
for(NSString *file in files) {
[manager removeItemAtPath:[dirToEmpty stringByAppendingPathComponent:file]
error:&error];
if(error) {
//an error occurred...
}
}
in swift 2.0:
if let enumerator = NSFileManager.defaultManager().enumeratorAtPath(dataPath) {
while let fileName = enumerator.nextObject() as? String {
do {
try NSFileManager.defaultManager().removeItemAtPath("\(dataPath)\(fileName)")
}
catch let e as NSError {
print(e)
}
catch {
print("error")
}
}
}
Swift 2.1.1:
public func deleteContentsOfFolder()
{
// folderURL
if let folderURL = self.URL()
{
// enumerator
if let enumerator = NSFileManager.defaultManager().enumeratorAtURL(folderURL, includingPropertiesForKeys: nil, options: [], errorHandler: nil)
{
// item
while let item = enumerator.nextObject()
{
// itemURL
if let itemURL = item as? NSURL
{
do
{
try NSFileManager.defaultManager().removeItemAtURL(itemURL)
}
catch let error as NSError
{
print("JBSFile Exception: Could not delete item within folder. \(error)")
}
catch
{
print("JBSFile Exception: Could not delete item within folder.")
}
}
}
}
}
}
Swift 3 if anyone needs it for a quick cut/paste
let fileManager = FileManager.default
let fileUrls = fileManager.enumerator(at: folderUrl, includingPropertiesForKeys: nil)
while let fileUrl = fileUrls?.nextObject() {
do {
try fileManager.removeItem(at: fileUrl as! URL)
} catch {
print(error)
}
}
The documentation for contentsOfDirectoryAtPath:error: says:
The search is shallow and therefore does not return the contents of any subdirectories. This returned array does not contain strings for the current directory (“.”), parent directory (“..”), or resource forks (begin with “._”) and does not traverse symbolic links.
Thus:
---( file != #"." && file != #".." )---
is irrelevant.
You can extend the NSFileManager like this:
extension NSFileManager {
func clearFolderAtPath(path: String) -> Void {
for file in subpathsOfDirectoryAtPath(path, error: nil) as? [String] ?? [] {
self.removeItemAtPath(path.stringByAppendingPathComponent(file), error: nil)
}
}
}
Then, you can clear the folder like this: NSFileManager.defaultManager().clearFolderAtPath("the folder's path")
Georg Fritzsche answer for Swift did not work for me. Instead of reading the enumerated object as a String, read it as NSURL.
let fileManager = NSFileManager.defaultManager()
let url = NSURL(string: "foo/bar")
let enumerator = fileManager.enumeratorAtURL(url, includingPropertiesForKeys: nil, options: nil, errorHandler: nil)
while let file = enumerator?.nextObject() as? NSURL {
fileManager.removeItemAtURL(file, error: nil)
}
Why not deleting the whole directory and recreate afterwards? Just get the file attributes and permissions before deleting it, and then recreate it with the same attributes.