Are there any approaches for getting the URL of a PHAsset that does not involve asynchronous requests/processes?
I realize that blocking the main thread when making requests like this is a no-no, but if you have a small number of assets to query, is it possible to get them any other way?
So far, the only ways I've found to get asset URLs seem to be things like the following:
somePHAssetObject.requestContentEditingInput(with: nil, completionHandler: {
input, info in
guard let input = input
else { fatalError("can't get info: \(info)") }
let assetURL = input.audiovisualAsset as! AVURLAsset
})
Or
PHCachingImageManager().requestAVAsset(forVideo: SomePHAssetObject, options: nil, resultHandler: {
(asset: AVAsset?, audioMix: AVAudioMix?, info: [AnyHashable: Any]?) in
let vidURL = asset as! AVURLAsset
})
Are there any ways to do something like this without an async approach? I'm just not seeing a URL property anywhere on a PHAsset, but could be missing something really obvious.
I don't believe that there is an API like that, but you can force the thread to wait for the handler using semaphore, e.g., for the second example you have presented:
let semaphore = DispatchSemaphore(value: 0)
PHCachingImageManager().requestAVAsset(forVideo: SomePHAssetObject, options: nil, resultHandler: { (asset: AVAsset?, audioMix: AVAudioMix?, info: [AnyHashable: Any]?) in
let vidURL = asset as! AVURLAsset
semaphore.signal()
})
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
Although I really don't understand why you would want to do it synchrously.
Related
I'm trying to implement the functionality of:
Recording a video (it's automatically saved to the apps memory)
Moving video file from apps memory to gallery (in this step we also save path to newly created video in gallery)
Displaying video from gallery
Steps 1. and 3. are written in Flutter, step 2 was implemented natively in Swift.
For some reason this feature sometimes works and sometimes not. When it doesn't work in step 3. I receive an error PlatformException(VideoError, Failed to load video: The requested URL was not found on this server., null, null).
I've also tried using path retrieved from step 2. to simply create File with it File(path) but then I also receive an error that file was not found (OS Error: No such file or directory, errno = 2).
I suspect, that on iOS it's caused by the whole App Sandbox thing is that correct? But if it is, why it sometimes does work and sometimes doesn't? Maybe there is something in my code which I could fix?
For step 1. I use camera package with basically the same code as in example. Then, after receiving XFile I run native iOS code, to save video to gallery and get it's path:
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let videoSavingChannel = FlutterMethodChannel(name: "app.package.name/camera/video_to_gallery",
binaryMessenger: controller.binaryMessenger)
videoSavingChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: #escaping FlutterResult) -> Void in
// This method is invoked on the UI thread.
guard call.method == "saveVideoToGallery" else {
result(FlutterMethodNotImplemented)
return
}
let args = call.arguments as? Dictionary<String, Any>
guard let fileUrl = args?["fileURL"] else {
result(nil)
return
}
self?.saveVideoToGallery(fileURL: fileUrl as! String, result: result)
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func saveVideoToGallery(fileURL: String, result: #escaping FlutterResult) {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: fileURL))
}) { saved, error in
if saved {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
PHAsset.fetchAssets(with: .video, options: fetchOptions).firstObject?.getURL { urlFromGallery in
guard let absoluteUrl = urlFromGallery?.absoluteString else {
result(nil)
return
}
result(absoluteUrl)
}
}
}
}
getting a video path:
extension PHAsset {
func getURL(completionHandler : #escaping ((_ responseURL : URL?) -> Void)){
if self.mediaType == .image {
let options: PHContentEditingInputRequestOptions = PHContentEditingInputRequestOptions()
options.canHandleAdjustmentData = {(adjustment: 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)
}
})
}
}
}
And then in flutter to display a video I use video_player again with pretty basic implementation:
VideoPlayerController controller =
VideoPlayerController.file(File(_videoPathFromGallery));
controller.initialize().then((_) {
//...someStuffHere
}
It's probably more of an iOS question rather than a flutter one.
It turned out that I've updated the video_player library from 2.4.6 to 2.5.1 and after that it stopped working. After downgrading it back to 2.4.6 it started working again.
I've also found how to make it work on 2.5.1 library. Videos from gallery on iOS have path similar to: file:///var/mobile/Media/DCIM/100APPLE/IMG_0103.MP4. To make it work we have to remove file:/// prefix.
My app as the functionality of choosing multiple images from the app main screen, and save the selected images to the user gallery.
As an example (image from google):
After the user clicking "save" I am doing the following in order to save the chosen images to the user's gallery.
Running through all of the images and saving each image that on clicked.
func saveSelectedImagesToDevice() {
for imageList in imagesListCells {
for image in imageList.images {
if image.selectionState == .onClicked {
downloadImage(from: image.url)
}
}
}
}
Downloading each image
func downloadImage(from url: String) {
guard let url = URL(string: url) else {return}
getData(from: url) { data, response, error in
guard let data = data, error == nil else { return }
guard let image = UIImage(data: data) else {return}
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
}
}
private func getData(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
#objc func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
if let _ = error {
self.delegate?.savedImage(proccessMsg: "Error adding images to the gallery, pleast make sure you enabled photos permissions in phone setting")
}
}
The thing is, because the saving process is asynchronies, in case error occurs in one of the process of downloading an image, I would like to stop all of the other asynchronies processes that running on the background.
At the moment, in case of error, the error been called for each one of the images.
Any ideas how can I manage it different in order to keep the process asynchronies but to be able to stop all processes in case of error?
You would have to change completely the architecture of the download to make it cancellable. A data task is cancellable, but yours is not because you have not retained any way of referencing it.
Apple suggests to not using the shared instance if you want to create multiple sessions. You could try to achieve this by creating a single session instance and invalidate it as soon as you receive an error.
Keep in mind that if you want to re-start the session you need to re instantiate a new one.
e.g.
let session = URLSession(configuration: .default)
func downloadImage(from url: String) {
guard let url = URL(string: url) else {return}
session.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
if let error = error {
print("You have an error: ",error.localizedDescription)
self.session.invalidateAndCancel()
}else if let data = data,
let image = UIImage(data: data) {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
}
}.resume()
}
I am using this code to convert a PHAsset to a full size UIImage:
func getOriginalAsset(asset: PHAsset, onComplete: #escaping (UIImage?, [AnyHashable: Any]?) -> ()) {
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
option.isNetworkAccessAllowed = true
manager.requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: option) { (image, info) in
onComplete(image, info)
}
}
However, I'm getting this warning from the console every time I try to run this code:
[ImageManager] First stage of an opportunistic image request returned a non-table format image, this is not fatal, but it is unexpected
Question
Why do I get this warning?
How do I resolve this?
Update
Everything works fine when I run this code despite the warning. I just can't stand any warnings/errors in my app (Maybe I'll have to learn to accept it sooner or later)
try use this code , I'm add isSynchronous = true you can read this document isSynchronous
I will explain to you the cause of the problem quickly This problem occurs because the opatation is shown until the end of the image and you request large size images may take time so you should be Thread in the waiting period until the process of fetching the image
func getOriginalAsset(asset: PHAsset, onComplete: #escaping (UIImage?, [AnyHashable: Any]?) -> ()) {
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
option.isNetworkAccessAllowed = true
option.isSynchronous = true
manager.requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: option) { (image, info) in
onComplete(image, info)
}
}
I have a problema with this method:
func DownloadImages(uid: String, indice: Int) {
DispatchQueue.global(qos: .background).async {
let refBBDD = FIRDatabase.database().reference().child("users").child(uid)
refBBDD.observeSingleEvent(of: .value, with: { snapshot in
let snapshotValue = snapshot.value as? NSDictionary
let profileImageUrl = snapshotValue?.value(forKey: "profileImageUrl") as! String
let storage = FIRStorage.storage()
var reference: FIRStorageReference!
if(profileImageUrl == "") {
return
}
print("before")
reference = storage.reference(forURL: profileImageUrl)
reference.downloadURL { (url, error) in
let data = NSData(contentsOf: url!)
let image = UIImage(data: data! as Data)
print("image yet dowload ")
self.citas[indice].image = image
DispatchQueue.main.async(execute: { () -> Void in
self.tableView.reloadRows(at: [IndexPath(row: indice, section: 0)], with: .none)
//self.tableView.reloadData()
print("image loaded")
})
}
print("after")
})
}
}
I want to download images in background mode. I want follow using app, but the UI has frozen until methods not entry in reloadRows.
Is it possible run in true background mode and can i follow using the app??
Trace program:
before
after
before
after
...
before
after
before
after
image yet dowload --> here start UI frozen
image loaded
image yet dowload
image yet dowload
...
image yet dowload
image yet dowload
image loaded
image loaded
...
image loaded
image loaded
image yet dowload ------> here UI is not frozen
image loaded
The problem is caused by this line: let data = NSData(contentsOf: url!).
That constructor of NSData should only be used to read the contents of a local URL (a file path on the iOS device), since it is a synchronous method and hence if you call it to download a file from a URL, it will be blocking the UI for a long time.
I have never used Firebase, but looking at the documentation, it seems to me that you are using the wrong method to download that file. You should be using func getData(maxSize size: Int64, completion: #escaping (Data?, Error?) -> Void) -> StorageDownloadTask instead of func downloadURL(completion: #escaping (URL?, Error?) -> Void), since as the documentation states, the latter only "retrieves a long lived download URL with a revokable token", but doesn't download the contents of the URL itself.
Moreover, you shouldn't be force unwrapping values in the completion handler of a network request. Network requests can often fail for reasons other than a programming error, but if you don't handle those errors gracefully, your program will crash.
Your problem is in this line let data = NSData(contentsOf: url!) and let's now see what apple says about this method below
Don't use this synchronous method to request network-based URLs. For
network-based URLs, this method can block the current thread for tens
of seconds on a slow network, resulting in a poor user experience, and
in iOS, may cause your app to be terminated.
So it clearly states that this method will block your User Interface.
I'm currently building a share extension that accepts URLs. As part of this I've customised my share screen as outlined in a previous question to create a full screen view controller. This is all working fine. However in the default share composer view I noticed there was a preview image of the web page. I'm trying to access this in my extension but I can't seem to get hold of it.
Specifically I've been trying to use the method
loadPreviewImage
https://developer.apple.com/reference/foundation/nsitemprovider/1403925-loadpreviewimage
You will note in the docs that this says the following for the completion handler
completionHandler
A completion handler block to execute with the results. The first parameter of this block must be a parameter of type NSData, NSURL, UIImage (in iOS), or NSImage (in macOS) for receiving the image data. For more information about implementing the block, see CompletionHandler.
However if I try to set this as a UIImage in my completion block I get an error of
Cannot convert value of type '(UIImage, _) -> ()' to expected argument
type 'NSItemProvider.CompletionHandler!'
example code where itemProvider is confirmed to be an instance of NSItemProvider via guard statements
itemProvider.loadPreviewImage(options: nil) { (image: UIImage, error) in
}
The docs for the completion Handler say to set this to what type you want and it will attempt to coerce the data to the type you specify. Has anyone seen this before? I'm not sure what to do here as I can't see what I'm doing wrong.
https://developer.apple.com/reference/foundation/nsitemprovider/completionhandler
If all else fails I'll look at using some Javascript to get an image from the dom but I would have liked the preview image that Apple seemed to provide
I don't know why the code in
itemProvider.loadPreviewImage(options: nil) { (image: UIImage, error) in
}
not called when Post button tapped.
My round way is saving the preview image in method
override func configurationItems() -> [Any]! {
}
as
let inputItem: NSExtensionItem = self.extensionContext?.inputItems[0] as! NSExtensionItem
let itemProvider = inputItem.attachments![0] as! NSItemProvider
if (itemProvider.hasItemConformingToTypeIdentifier("public.url")) {
itemProvider.loadPreviewImage(options: nil, completionHandler: { (item, error) in // 画像を取得する
if let image = item as? UIImage {
if let data = UIImagePNGRepresentation(image) {
self.photoNSURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("preview.png") as NSURL!
do {
try data.write(to: self.photoNSURL as URL, options: .atomic)
} catch {
print("\(#file)#\(#function)(\(#line)): error: \(error.localizedDescription)")
}
}
}
})
}