I have a program in which the user chooses a photo to put on the screen and the code puts it into a custom album automatically. But whenever they choose a picture, it resaves it to the camera roll, creating duplicates. How do I make it stop doing this?
func fetchAssetCollectionForAlbum() -> PHAssetCollection? {
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "title = %#", albumName)
// fetch the asset for the album
let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
var picturePlaceHolder: PHObjectPlaceholder? = nil
if let _: AnyObject = collection.firstObject {
return collection.firstObject
}
return nil
}
func save(image: UIImage) {
if assetCollection == nil {
return
}
PHPhotoLibrary.shared().performChanges({
let assetChangeRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
let assetPlaceHolder = assetChangeRequest.placeholderForCreatedAsset
let albumChangeRequest = PHAssetCollectionChangeRequest(for: self.assetCollection)
let enumeration: NSArray = [assetPlaceHolder!]
albumChangeRequest!.addAssets(enumeration)
}, completionHandler: nil)
}
I posted a similar question:
Swift 3 or 4 Saving to custom album creates duplicate images
But I got nothing but crickets as well. Luckily, I think I found the answer. I'll answer my own question as well.
The code you have (which was the same code I had) is to CREATE A NEW ASSET. It is useful only for the saving the image to your custom album after the user has taken a picture with the camera. It is for brand new assets.
However, for existing assets, you do not want to create a new asset. Instead, you want to add the existing asset to the custom album. To do this, you need a different method. Here is the code I created and it seems to be working. Keep in mind that you will have to get the asset ID FIRST, so that you can send it to your method and access the existing asset.
So, in your imagePickerController, you have to determine whether the user chose an existing image or whether the method is being called from a new camera action.
let pickerSource = picker.sourceType;
switch(pickerSource){
case .savedPhotosAlbum, .photoLibrary:
if(let url = info[UIIMagePickerControllerReferenceURL] as? NSURL{
let refURLString = refURL?.absoluteString;
/* value for refURLString looks something like assets-library://asset/asset.JPG?id=82A6E75C-EA55-4C3A-A988-4BF8C7F3F8F5&ext=JPG */
let refID = {function here to extract the id query param from the url string}
/*above gets you the asset ID, you can get the asset directly, but it is only
available in ios 11+.
*/
MYPHOTOHELPERCLASS.transferImage(toAlbum: "myalbumname", withID: refID!, ...)
}
break;
case .camera:
...
break;
}
Now, in your photohelper class (or in any function anywhere, whatever), to EDIT the asset instead of create a new one, this is what I have. I am assuming the changeRequest variable can be ommitted. I was just playing around until I got this right. Going through the completely ridiculous apple docs I was able to at least notice that there were other methods to play with. I found that the NSFastEnumeration parameter can be an NSArray of PHAssets, and not just placeholder PHObjectPlaceholder objects.
public static func transferImage(toAlbum albumName:String, withID imageID:String, onSuccess success:#escaping(String)->Void, onFailure failure:#escaping(Error?)->Void){
guard let album = self.getAlbum(withName: albumName) else{
... failure here, albumNotFoundError
return;
}
if(self.hasImageInAlbum(withIdentifier: imageID, fromAlbum: albunName)){
... failure here, image already exists in the album, do not make another
return;
}
let theAsset = self.getExistingAsset(withLocalIdentifier: imageID);
if(theAsset == nil){
... failure, no asset for asset id
return;
}
PHPhotoLibrary.shared().performChanges({
let albumChangeRequest = PHAssetCollectionChangeRequest(for: album);
let changeRequest = PHAssetChangeRequest.init(for: theAsset!);
let enumeration:NSArray = [theAsset!];
let cnt = album.estimatedAssetCount;
if(cnt == 0){
albumChangeRequest?.addAssets(enumeration);
}else{
albumChangeRequest?.inserAssets(enumeration, at: [0]);
}
}){didSucceed, error) in
OperationQueue.main.addOperation({
didSucceed ? success(imageID) : failure(error);
})
}
}
So, it is pretty much the same, except instead of creating an Asset Creation Request and generating a placeholder for the created asset, you instead just use the existing asset ID to fetch an existing asset and add the existing asset to the addasset/insertasset NSArray parameter instead of a newly created asset placeholder.
Related
I am trying to save an image to the user's photo library using PHPhotoLibrary and set the image file name at the time of saving suing the code below.
This is working the first time, but if I then try to save the same image again with a different file name, it saves with the same file name as before.
Is there something I need to add to let the system know to save a new version of the image with a new file name?
Thank you
PHPhotoLibrary.shared().performChanges ({
let assetType:PHAssetResourceType = .photo
let request:PHAssetCreationRequest = .forAsset()
let createOptions:PHAssetResourceCreationOptions = PHAssetResourceCreationOptions()
createOptions.originalFilename = "\(fileName)"
request.addResource(with: assetType, data: image.jpegData(compressionQuality: 1)!, options: createOptions)
}, completionHandler: { success, error in
if success == true && error == nil {
print("Success saving image")
} else {
print("Error saving image: \(error!.localizedDescription)")
}
})
I want to programmatically navigate chapters of a mp4 video.
The chapters work in QuickTime, so I assume the video format isn't the issue.
The code from this page should return an array of the chapters but only returns an empty one instead:
https://developer.apple.com/documentation/avfoundation/media_playback/presenting_chapter_markers
let asset = AVAsset(url: <# Asset URL #>)
let chapterLocalesKey = "availableChapterLocales"
asset.loadValuesAsynchronously(forKeys: [chapterLocalesKey]) {
var error: NSError?
let status = asset.statusOfValue(forKey: chapterLocalesKey, error: &error)
if status == .loaded {
let languages = Locale.preferredLanguages
let chapterMetadata = asset.chapterMetadataGroups(bestMatchingPreferredLanguages: languages)
// Process chapter metadata.
}
else {
// Handle other status cases.
}
}
Has anyone an idea how to do it?
I've been trying to get this code to work for the past few hours. But I have been getting mixed results.
static func uploadDataToServer(data: Data, secondData: Data? = nil, thirdData: Data? = nil, firstVideoURL: URL? = nil, secondVideoURL: URL? = nil, thirdVideoURL: URL? = nil, caption: String, onSuccess: #escaping () -> Void) {
if let firstVideoURL = firstVideoURL {
SingleVideoHandler.uploadOneVideoToFirebase(firstVideoURL: firstVideoURL, onSuccess: { (vidURL) in
uploadImagesToFirebaseStorage(data: data, secondData: secondData!, thirdData: thirdData!, onSuccess: { (firstImage, secondImage, thirdImage) in
sendDataToDatabase(firstPhotoURL: firstImage, secondPhotoURL: secondImage, thirdPhotoURL: thirdImage ,firstVideoURL: vidURL, caption: caption, onSuccess: onSuccess)
})
})
} else {
if let firstVideoURL = firstVideoURL, let secondVideoURL = secondVideoURL {
print("Got two pics!")
MultipleVideoHandler.uploadTwoVideosToFirebase(firstVideoURL: firstVideoURL, secondVideoURL: secondVideoURL, onSuccess: { (firstVidURL, secondVidURL) in
uploadImagesToFirebaseStorage(data: data, secondData: secondData!, thirdData: thirdData!, onSuccess: { (firstImage, secondImage, thirdImage) in
sendDataToDatabase(firstPhotoURL: firstImage, secondPhotoURL: secondImage, thirdPhotoURL: thirdImage, firstVideoURL: firstVidURL, secondVideoURL: secondVidURL, caption: caption, onSuccess: onSuccess)
})
})
} else {
if let secondImageData = secondData, let thirdImageData = thirdData {
uploadThreeImagesToFirebaseStorage(data: data, secondData: secondImageData, thirdData: thirdImageData) { (firstPhotoURL, secondPhotoURL, thirdPhotoURL) in
self.sendDataToDatabase(firstPhotoURL: firstPhotoURL, secondPhotoURL: secondPhotoURL, thirdPhotoURL: thirdPhotoURL, caption: caption, onSuccess: onSuccess)
}
}
}
}
}
Basically, I'm trying to detect wether I have a firstVideoURL, a secondVideoURL and a thirdVideoURL.
However, in order to have a secondVideoURL you must have a firstVideoURL. Like wise with regards to having a thirdVideoURL you must have both a firstVideoURL and a secondVideoURL.
What I'm doing in essence it allowing for optional videos, where users can select up to three videos to upload and images. Only have 3 parameters as of now, due to to testing.
I'm trying to detect how many videoURLs I have, and based of that, upload however many videos have been selected and additional content.
My results so far:
Only the first if-let statement is called and executed. Even thought I have more than one videoURL.
The code is called multiple times, creating the same instance of a post, however, the first instance will only contain the firstVideoURL and the second instance will have both the first and second video URL. The second instance is what I want. But I do not want the first instance.
I cannot figure out as to why this is happening?
Could someone please help me out?
Thank you.
In essence this is what I want-
I've got 2 video URL
The code is executed
Checks the conditions
Skips the first if-let statement
Executes the second if-let statement.
In terms of uploading and create posts, the server/api code works as expected. Just this conditional operation is not working as I anticipated.
The first if condition (if let firstVideoURL = firstVideoURL) passes if there's firstVideoURL, it doesn't care about secondVideoURL and/or thirdVideoURL. If thats what you need you should consider constructing your if-else like below:
if let firstVideoURL = firstVideoURL{
if let secondVideoURL = secondVideoURL{
if let thirdVideoURL = thirdVideoURL{
//Do what you would if there are all the three video urls.
} else {
//Do what you would if there are firstVideoURL and secondVideoURL but not the thirdVideoURL
}
} else {
//Do what you would if there is only the firstVideoURL
}
} else {
//Handle the case when there are no video urls.
}
Do you want to allow it to be called with NO video URLS? If so, why? You've made all 3 videoURL parameters optional, which doesn't seem to make sense. It seems to me that you should allow one, 2, or 3.
In any case, your code doesn't make sense as written. Once the first if clause fails, you know that firstVideoURL is nil, so the second if clause is also going to fail.
If you're going to continue with this approach, I think #CodeDifferent has the right idea with their comment:
if let firstVideoURL = firstVideoURL,
let secondVideoURL = secondVideoURL,
let thirdVideoURL = thirdVideoURL {
//upload 3 videos
} else if let firstVideoURL = firstVideoURL,
let secondVideoURL = secondVideoURL {
//upload 2 videos
} else if let firstVideoURL = firstVideoURL {
//upload 1 video
} else {
//No videos. Why is this possible?
}
(I don't really understand your use of both video URLs and data objects, so I ignored that part.)
However, lets take a step back. Why are you using 3 separate sets of optional parameters and dealing with a messy construct of if let optional bindings? Why not pass in an array of video URLS?
func uploadDataToServer( data: [Data], urls: [URL]) {
//Loop through the array of data objects/URLs and upload each one.
}
Here is the code I use to download an image from an AWS S3 server and assign it to be the image displayed in an image view:
let s3BucketName = "bucketName"
let fileName = Globals.currAuthorName.filter { $0 != Character(" ") } + ".jpg"
let downloadFilePath = NSTemporaryDirectory().stringByAppendingPathComponent(fileName)
let downloadingFileURL = NSURL.fileURLWithPath(downloadFilePath)
// Create a credential provider for AWS requests
let credentialsProvider = AWSCognitoCredentialsProvider(
regionType: AWSRegionType.USEast1,
identityPoolId: "us-east-1:********-****-****-****-************")
// Create a service configuration for AWS requests
let defaultServiceConfiguration = AWSServiceConfiguration(
region: AWSRegionType.USEast1,
credentialsProvider: credentialsProvider)
// Create a new download request to S3, and set its properties
AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = defaultServiceConfiguration
let downloadRequest = AWSS3TransferManagerDownloadRequest()
downloadRequest.bucket = s3BucketName
downloadRequest.key = "folderName/" + fileName
downloadRequest.downloadingFileURL = downloadingFileURL
let transferManager = AWSS3TransferManager.defaultS3TransferManager()
transferManager.download(downloadRequest)
// Set the UIImageView to show the file that was downloaded
let image = UIImage(contentsOfFile: downloadFilePath)
authorImage.contentMode = UIViewContentMode.ScaleAspectFit
authorImage.image = image
The problem is that when I run the iOS Simulator and navigate to the appropriate page, the image is never loaded the first time I visit the page. However, it is always loaded when I navigate away from the page and then return the page, and every subsequent time I visit the page. The image is still loaded when I stop running the app and then start it again and visit the page again. The image is only not loaded the very first time I visit the page for a certain iOS Simulator (i.e. it happens once in the iPhone 6 simulator, the iPhone 5 simulator, etc)
I tried to fix the problem by adding this bit of code:
while (image == nil) {
transferManager.download(downloadRequest)
let image = UIImage(contentsOfFile: downloadFilePath)
}
But that simply resulted in an infinite while loop the first time I visited the page. Also the console eventually output this error:
2015-07-06 23:26:28.529 CCBF[19839:92323] 19839: CFNetwork internal error (0xc01a:/SourceCache/CFNetwork_Sim/CFNetwork-711.3.18/Foundation/NSURLRequest.mm:798)
2015-07-06 23:26:28.564 AuthorProject[19839:92323] AWSiOSSDKv2 [Error] AWSURLSessionManager.m line:240 | __41-[AWSURLSessionManager taskWithDelegate:]_block_invoke222 | Invalid AWSURLSessionTaskType.
EDIT:
I tried to implement the following continuewithblock:
var image : UIImage!
let task = transferManager.download(downloadRequest)
task.continueWithBlock({
if task.error != nil {
println(task.error)
} else {
dispatch_async(dispatch_get_main_queue(), {
image = UIImage(contentsOfFile: downloadFilePath)
self.authorImage.contentMode = UIViewContentMode.ScaleAspectFit
self.authorImage.image = image
})
}
return nil
}())
However, when I ran it and tried to load the image, I got a runtime error that pointed me to the code for a "continueWithExecutor" method. Specifically, the line of code:
result = block(self);
With the message being:
EXC_BAD_ACCESS(code: 1, address=0x10)
EDIT2:
I seem to have found a solution that works:
var image : UIImage!
let taskTODO = transferManager.download(downloadRequest)
taskTODO.continueWithBlock{ (task: AWSTask!) -> AnyObject! in
if task.error != nil {
println(task.error)
} else {
dispatch_async(dispatch_get_main_queue(), {
image = UIImage(contentsOfFile: downloadFilePath)
self.authorImage.contentMode = UIViewContentMode.ScaleAspectFit
self.authorImage.image = image
})
}
return nil
}
Does this seem like the correct way to do it?
transferManager.download(downloadRequest) is asynchronous, meaning it takes about 20 miliseconds to download the image. However, the first time into the screen you are trying to assign the image immediately (which has not yet been downloaded). In subsequent screen loads - the image has already been downloaded and cached - so it's immediately ready to be displayed, . What you need is a completion block that indicates when the image is has finished downloading and is ready for display and in that block you shall assign the image. See example below:
let readRequest1 : AWSS3TransferManagerDownloadRequest = AWSS3TransferManagerDownloadRequest()
readRequest1.bucket = "shrikar-picbucket"
readRequest1.key = "bingo"
readRequest1.downloadingFileURL = downloadingFileURL1
let task = transferManager.download(readRequest1)
task.continueWithBlock { (task) ->; AnyObject! in
println(task.error)
if task.error != nil {
} else {
dispatch_async(dispatch_get_main_queue()
, { () ->; Void in
self.selectedImage.image = UIImage(contentsOfFile: downloadingFilePath1)
self.selectedImage.setNeedsDisplay()
self.selectedImage.reloadInputViews()
})
println("Fetched image")
}
return nil
}
Scenario:
I wish to reduce the size of individual videos from my iTouch photo library.
1. Collect videoAssets from library.
2. Get a thumbnail of the PHAsset - works.
3. Get the actual video from the library.
4. Request the AVAssetForVideo from the library.
5. Convert the video via ExportSessions... loading assorted parameters.
6. Attempt to run the export into a tmp directory for use.
* FAILS *
Here's the debug output:
Here's the error message:
func getVideoFromPhotoLibrary() {
let videoAssets = PHAsset.fetchAssetsWithMediaType(.Video, options:nil)
videoAssets.enumerateObjectsUsingBlock {
(obj:AnyObject!, index:Int, stop:UnsafeMutablePointer<ObjCBool>) in
let mySize = CGSizeMake(120,120)
let myAsset = obj as! PHAsset
let imageManager = PHImageManager.defaultManager()
var myVideo:BlissMedium?
// Request the poster frame or the image of the video
imageManager.requestImageForAsset(myAsset, targetSize:mySize, contentMode: .AspectFit, options: nil) {
(imageResult, info) in
let thumbnail = UIImage(named:"videoRed")
myVideo = BlissMedium(blissImage: imageResult, creationDate:myAsset.creationDate)
myVideo!.mediumType = .video
}
// Actual Video:
imageManager.requestAVAssetForVideo(myAsset, options: nil, resultHandler: {result, audio, info in
let asset = result as! AVURLAsset
let mediaURL = asset.URL
let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetMediumQuality)
let filename = "composition.mp4"
session.outputURL = NSURL(string: NSTemporaryDirectory());
session.outputFileType = AVFileTypeQuickTimeMovie;
session.exportAsynchronouslyWithCompletionHandler({ () -> Void in
dispatch_async(dispatch_get_main_queue(), {
if session.status == AVAssetExportSessionStatus.Completed {
println("Success")
}
else {
println(session.error?.localizedDescription)
//The requested URL was not found on this server.
}
})
})
})
if nil != myVideo {
self.gBlissVideoMedia.append(myVideo!)
}
}
}
I checked to be sure the target path/file exist; then I added the 'AVFileTypeMPEG4' output type to match the intended .mp4:
let targetDir = createTempDirectory("bliss/composition.mp4") as String?
if NSFileManager.defaultManager().fileExistsAtPath(targetDir!) {
println("*** file exists! ***")
} else {
return
}
session.outputURL = NSURL(string: targetDir!);
session.outputFileType = AVFileTypeMPEG4
I'm still having problems:
* file exists! *
Optional("The operation could not be completed")
What am I doing wrong; what's missing?
Update:
I'm able to successfully run the export to my NSHomeDirectory() vs NSTemporaryDictory() in Objective-C.
However... the same code written in Swift fails.
I notice a change in absolute path to the target output in Swift, not found in Objective-C:
Perhaps it's a Swift 1.2 bug???
I am not sure if you can save in the root of the temp directory, I normally use this function to create a new temp directory that I can use:
func createTempDirectory(myDir: String) -> String? {
let tempDirectoryTemplate = NSTemporaryDirectory().stringByAppendingPathComponent(myDir)
let fileManager = NSFileManager.defaultManager()
var err: NSErrorPointer = nil
if fileManager.createDirectoryAtPath(tempDirectoryTemplate, withIntermediateDirectories: true, attributes: nil, error: err) {
return tempDirectoryTemplate
} else {
return nil
}
}
Try to make your conversion in the directory returned by this function.
I hope that helps you!
I didn't quite understand what that last part of code did, where you find out if a file exists or not. Which file is it you are locating?
Since I didn't understand that then this might be irrelevant, but in your topmost code I notice that you set the filename to composition.mp4, but let the outputURL be NSURL(string: NSTemporaryDirectory()). With my lack of Swiftness I might be missing something, but it seems to me as if you're not using the filename at all, and are trying to write the file as a folder. I believe setting a proper URL might fix the problem but I'm not sure. An Objective-c-example of this could be:
NSURL * outputURL = [[NSURL alloc]
initFileURLWithPath:[NSString pathWithComponents:
#[NSTemporaryDirectory(), #"composition.mp4"]]];
The outputURL is supposed to point to the actual file, not the folder it lies in. I think..
Anyway, if that doesn't work I do have a few other thoughts as well.
Have you tried it on an actual device? There may be a problem with the simulator.
Also, sadly, I have gotten the error -12780 countless times with different root-problems, so that doesn't help very much.
And, I see you check if session.status == AVAssetExportSessionStatus.Completed, have you checked what the actual status is? Is it .Failed, or perhaps .Unknown? There are several statuses.
This might be a long shot, but in one of my apps I am using the camera to capture video/audio, then encode/convert it using AVAssetExportSession. There were strange errors when starting to record, as well as after recording(exporting). I found out that I could change the AVAudioSession, which apparently has something to do with how the device handles media.
I have no idea how to Swift, but here's my code (in viewDidAppear of the relevant view)
NSError *error;
AVAudioSession *aSession = [AVAudioSession sharedInstance];
[aSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
[aSession setMode:AVAudioSessionModeVideoRecording error:&error];
[aSession setActive: YES error: &error];
The category PlayAndRecord allowed me to start the camera much faster, as well as getting rid of the occasional hanging AVAssetExportSessionStatus.Unknown and the occasional crash .Failed (which also threw the -12780-error).