How to use local variables in closures during performDrop - ios

I'm working my way through the Stanford CS193P Course on iTunesU. Stuck on this one part - There's one section where the instructor has the following comment:
When a drop happens, you'll have to collect both the aspect ratio (from
the UIImage) and the URL before you can add an item. You could do this
straightforwardly with a couple of local variables that are captured by
the closures used to load up the drag and drop data
I assume this to mean that I have to update my model item in the performDrop method of the UICollectionViewDropDelegate protocol. Therefore I will have to call loadObject for both the NSURL item and the UIImage item. However, since the completion handlers for loadObject are off the main thread, I don't have access to the same local variable for both. Therefore if I first load the UIImage, get the aspect ratio, and save it to a local variable in the performDrop method, that variable is still empty when I call loadObject on the URL, which is when I create the model based on the URL and the aspect ratio. I tried nesting the loadObject calls, but the 2nd call was not firing. My code is below:
let placeholderContext = coordinator.drop(item.dragItem, to:
UICollectionViewDropPlaceholder(insertionIndexPath: destinationIndexPath, reuseIdentifier: placeholderCellReuseIdentifier))
var aspectRatio: [CGFloat] = []
item.dragItem.itemProvider.loadObject(ofClass: UIImage.self) { (provider, error) in
if var image = provider as? UIImage {
aspectRatio.append(image.size.width / image.size.height)
}
DispatchQueue.main.async {
item.dragItem.itemProvider.loadObject(ofClass: NSURL.self, completionHandler: { (provider, error) in
print(provider)
if let error = error {
print(error.localizedDescription)
}
print("load urls in completion")
})
}
}
print("main thread: \(aspectRatio)")
item.dragItem.itemProvider.loadObject(ofClass: NSURL.self) { (provider, error) in
print("loaded urls")
DispatchQueue.main.async {
if let url = provider as? URL {
placeholderContext.commitInsertion(dataSourceUpdates: { (insertionIndexPath) in
self.galleryItems.items.insert(GalleryItem(imageURL: url.imageURL, aspectRatio: 1.0), at: insertionIndexPath.item)
})
} else {
placeholderContext.deletePlaceholder()
}
}
}
Am I misguided here or is there something simple I am missing? How can I get the aspect ratio from loading the UIImage first, and then use that in the URL load object completion handler, in the way as is described in the homework?

Related

Attempted to read an unowned reference but object was already deallocated Swift 5

I have a problem with a deallocation of a variable: cache
This is from the tutorial Reusable Image Cache in Swift
Error:
Fatal error: Attempted to read an unowned reference but object 0x280208080 was already deallocated Fatal error: Attempted to read an unowned reference but object 0x280208080 was already deallocated
Code:
final class ImageLoader {
private let cache = ImageCache()
func loadImage(from url: URL) -> AnyPublisher<UIImage?, Never> {
if let image = cache[url] {
return Just(image).eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.catch { error in return Just(nil) }
.handleEvents(receiveOutput: {[unowned self] image in
guard let image = image else { return }
self.cache[url] = image
})
.subscribe(on: DispatchQueue.global(qos: .background))
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}
This error has a pretty simple explanation:
by the time URLSession is finishing its work, the instance of ImageLoader does not exist, because nobody keeps a reference to it. It may happen when you just create that instance in function scope variable. (Maybe in some function like viewDidLoad).
This crash is useful, as it says that loader is using in the wrong way. In case of usage of weak self or capturing the whole instance, the crash will not happen, but you will have a lot of ImageLoaders with its own caches with one image there. Thus there would be no caching in its meaning.
To solve that, after the creation of ImageLoader instance, you should keep the reference to it in a class/struct variable which is consuming it and pass it to another consumer, who needs the same cache. (a dependency injection technic is a good approach for that). Thus one cache with some amount of items will exist and works.
Or the simplest way is to make a shared instance of ImageLoader and use it only, thus it also guaranties one instance of it with one filled cache.
replace unowned to weak
.handleEvents(receiveOutput: {[weak self] image in
guard let image = image else { return }
self.cache[url] = image
})
I faced this type of issue I just removed [unowned self] to [self]
example:
.handleEvents(receiveOutput: {[self] image in
guard let image = image else { return }
self.cache[url] = image
})

On iOS, Will there be race condition for two UIDragItem.itemProvider.loadObject methods?

I am implementing dragging and dropping images from, let say, Safari into a collectionView of my own app. I need the both the UIImage and image url from the dragItem.
The way to get any of them is to use UIDragItem.itemProvider.loadObject. But since loadObject(ofClass:completionHandler:) runs asynchronously, how can I make sure there will be no race condition with the code below?
(note: the two are in the performDropWith func, I want to make sure they execute in order because I need to do work immediately after the second one finishes)
var imageARAndURL = [String: Any]()
_ = item.dragItem.itemProvider.loadObject(ofClass: UIImage.self) { (provider, error) in
if let image = provider as? UIImage {
let aspectRatio = image.size.height / image.size.width
print(aspectRatio)
imageARAndURL["aspectRatio"] = aspectRatio
}
}
_ = item.dragItem.itemProvider.loadObject(ofClass: URL.self) { (provider, error) in
if let url = provider {
print(url.imageURL)
imageARAndURL["imageURL"] = url.imageURL
print(imageARAndURL)
}
}

Maintaining order when requesting AVAssets

I am attempting to build a video merging app that allows users to select several short clips from a collection view and then generates a preview of the videos all merged into one. I am using the Photos framework (PHCachingImageManager) to populate the collection view and am passing an array of the selected PHAssets to the function below in order to request low quality AVAssets (for merging & generating the preview).
The problem is, I need to keep the AVAssets in the order in which the user selected them, but the "requestAVAsset" function is asynchronous and the completion handler is often called multiple times. I've never used Dispatch Groups before, but attempted to use them below...and the AVAssets are still out of order sometimes.
func requestAVAssets(assets: [PHAsset]) -> [AVAsset] {
var videoArray: [AVAsset] = []
let dispatchGroup = DispatchGroup()
let videoOptions = PHVideoRequestOptions()
videoOptions.isNetworkAccessAllowed = true
videoOptions.deliveryMode = .fastFormat
for asset in assets {
dispatchGroup.enter()
self.imageManager.requestAVAsset(forVideo: asset, options: videoOptions, resultHandler: { (video, audioMix, info) in
guard video != nil else { return }
videoArray.append(video!)
dispatchGroup.leave()
})
}
dispatchGroup.wait()
return videoArray
}
I'm guessing I've either misplaced some code or am approaching this in entirely the wrong way! Any suggestions are appreciated.
If you capture the current index while you're iterating the AVAssets, you can insert rather than append. That's how I do it, at least.
func requestAVAssets(assets: [PHAsset]) -> [AVAsset] {
var videoArray = [AVAsset?](repeating: nil, count: assets.count)
let videoOptions = PHVideoRequestOptions()
videoOptions.isNetworkAccessAllowed = true
videoOptions.deliveryMode = .fastFormat
for (i, asset) in assets.enumerated() {
self.imageManager.requestAVAsset(forVideo: asset, options: videoOptions, resultHandler: { (video, audioMix, info) in
guard let video = video else { return }
videoArray.remove(at: i)
videoArray.insert(video, at: i)
})
}
return videoArray.flatMap { $0 }
}
Giving the array the desired number of items as nil will stop it from erroring when inserting items, and then when the download is complete, remove the existing nil value and replace it with the actual AVAsset.
Finally, flatMap the resulting array to unpack the optionals (and optionally check that you have the desired number of items by comparing it with the incoming assets array).
Dodging the dispatch question entirely because it's late and I've had a bad day, but what if you kept the "correct" index associated with the video, and then sorted on that? I think something like this would work.
struct SelectedVideo {
let index: Int
let asset: AVAsset
}
func requestAVAssets(assets: [PHAsset]) -> [AVAsset] {
var videoArray: [SelectedVideo] = []
let dispatchGroup = DispatchGroup()
let videoOptions = PHVideoRequestOptions()
videoOptions.isNetworkAccessAllowed = true
videoOptions.deliveryMode = .fastFormat
for (index, asset) in assets.enumerated() {
dispatchGroup.enter()
self.imageManager.requestAVAsset(forVideo: asset, options: videoOptions, resultHandler: { (videoMb, audioMixMb, infoMb) in
guard let video = videoMb else return
videoArray.append(SelectedVideo(index, video))
dispatchGroup.leave()
})
}
dispatchGroup.wait()
return videoArray.sort { $0.index < $1.index}.map({$0.video})
}
This is kind of a hack (haven't even tried compiling it), but like I said, it's been a bad day.
A couple minor changes to note: I changed the params to the closure to say "Mb" which means "maybe" and is a nice convention I've seen for naming optionals passed to closures. Also, instead of "guard video != nil" followed by force-unwrapping, it's much preferred to do a "guard let video = videoMb", and then video is non-optional.

Return image from asynchronous call

I have a picker view that has images as it's scrollable items. I need to pull those images from my database so I'm getting the Unexptected non-void return value in void function error. Here is my code:
func pickerView(_ pickerView: AKPickerView, imageForItem item: Int) -> UIImage {
let imgRef = FIRStorage.storage().reference().child("profile_images").child(pets[item])
imgRef.data(withMaxSize: 1 * 1024 * 1024) { (data, error) -> Void in
// Create a UIImage, add it to the array
let pic = UIImage(data: data!)
return pic
}
}
So I understand why this doesn't work but I'm having a hard time finding out what's the best way to get around this. One solution I can think of is to just set the images to some generic photo until the callback happens, then update the picker view's image to the retrieved image. However, I don't know how to access individual picker view items in order to update its image.
If anybody with some experience can give me advice on how I can achieve my goal of setting these items to the data from an asynchronous call I'd greatly appreciate it!
Your function here is an asynchronous function. You have to make use of callbacks in this case. You can rewrite the function in the following way to achieve desired results.
func pickerView(_ pickerView:AKPickerView, imageForeItem item:Int, completion:(_ resultImg: UIImage)->Void) {
let imgRef = FIRStorage.storage().reference().child("profile_images").child(pets[item])
imgRef.data(withMaxSize: 1 * 1024 * 1024) { (data, error) -> Void in
// Create a UIImage, add it to the array
if let pic:UIImage = UIImage(data: data!) {
completion(pic)
}
}
}
This can be called as follows:
self.pickerView(pickerView, imageForeItem: 0) { (image) in
DispatchQueue.main.async {
// set resulting image to cell here
}
}
Feel free to suggest edits to make this better :)

Managing profile picture of subclassed PFUser with PFFile - Swift / iOS

I've subclassed PFUser in my iOS app and I'm using this function to grab the profile picture. profilePicture is the #NSManaged PFFile and profilePictureImage is a UIImage.
This works great except for the fact that getData() and fetchIfNeeded() are potential long running operations on the main thread.
Can anyone think of a good way to implement this method so the scary parts run on a background thread?
Thanks!
func image() -> UIImage!
{
if !(self.profilePictureImage != nil)
{
if self.profilePicture != nil
{
self.fetchIfNeeded()
if let data = self.profilePicture!.getData() {
self.profilePictureImage = UIImage(data: data)
return self.profilePictureImage
}
}else {
return UIImage(named: "no_photo")!
}
}
return self.profilePictureImage
}
Change the method so that rather than returning an image it takes a closure which is called when the image is available and passes it as a parameter. This may be called immediately or after some delay if the image needs to be downloaded.
Just do it as you say, run the task in the background using: fetchIfNeededInBackgroundWithBlock. Also, your image function should look something like this:
func imageInBackgroundWithBlock(block: ((UIImage?, NSError?) -> Void)?) {
var image: UIImage?
self.fetchIfNeededInBackgroundWithBlock({ (user, error) -> Void in
if error == nil {
// load the picture here
image = ...
} else {
println(error!.userInfo)
image = UIImage(named: "no_photo")!
}
// return after fetching the user and the image
block?(image, error)
})
}

Resources