How can I run a for-loop with completion handler methods? - ios

I have some image files I need to get from a drone using the DJI SDK, but the way I've done it doesn't seem the most ideal. I was wondering how I would be able to do this, but inside of a for-loop instead. Here's the code:
func getImages(with file: DJIMediaFile?) {
guard let file = file else { return }
file.fetchPreview { (error) in
if error != nil {
print(String(describing: error?.localizedDescription))
return
}
guard let image = file.preview else { print("No preview"); return }
self.images.append(image)
self.imageView.image = image
self.index += 1
if self.index < self.mediaList.count {
self.getImages(with: self.mediaList[self.index])
} else {
// [...]
}
}
}
}
Any help would be appreciated by someone who's familiar with the DJI SDK and Swift (perhaps I should have used some other API method?). Thanks!

This is mostly a swift question rather than a DJI SDK issue and more related to code review. But its simply
func getNewImages() {
for file in self.mediaList where file != nil {
self.getImages(with: file!) { image, error in //not ideal to force unwarp but i don't know if the where clause supports if let binding, or if file is really nil, you'll have to check yourself in code
guard let newImage = image else { return }
self.cachedImages.append(newImage)
}
}
self.setImage(at: 0)
}
func getImages(with file: DJIMediaFile, completion: (UIImage?, Error?)->()) {
file.fetchPreview { (error) in // is this async? If its' async you may want to use dispatch groups in the for in loop to run the setImage at the correct time
if let newError = error {
print(String(describing: newError.localizedDescription))
completion(nil, error)
return
}
guard let image = file.preview else {
print("No preview")
completion(nil, nil)
return
}
completion(image, nil)
}
}
func setImage(at row: Int) {
guard row < self.cachedImages.count - 1 else { return }
self.imageView.image = self.cachedImages[row]
}
Your recursion may result in errors you don't want. This is kinda what you want to do but it should be straightforward to change based on what I can see here.
Edit: You may also be intending to replace the file as people are loading their images from the drone itself in which case the code is more like this:
func getNewImages() {
for file in self.mediaList where file != nil {
self.getImages(with: file!) { image, error in //not ideal to force unwarp but I don't know if the where clause supports if let binding, or if file is really nil, you'll have to check yourself in code
guard let newImage = image else { return }
self.imageView.image = newImage
}
}
}
func getImages(with file: DJIMediaFile, completion: (UIImage?, Error?)->()) {
file.fetchPreview { (error) in
if let newError = error {
print(String(describing: newError.localizedDescription))
completion(nil, error)
return
}
guard let image = file.preview else {
print("No preview")
completion(nil, nil)
return
}
completion(image, nil)
}
}
edit2: Also note, this is not necessarily the best way to do this, but previously is how you do it in a for loop. I think a better way would be to use map or a forEach on the mediaList array itself like so. (same getImages method, nothing changes). This could be further refined as well between protocols, extensions and what not but without knowing your program, its hard to do better recommendations.
func getNewImages() {
self.mediaList.forEach { file in
guard let newFile = file else { return }
self.getImages(with: newFile) { image, error in
guard let newImage = image else { return }
self.imageView.image = newImage
}
}
}

Related

Filehandle class loading whole file into memory

I am trying to read data from big files sequentially. Since the files can be bigger than the available memory, I don't want the file to be completely loaded into memory.
class RecordImporter {
func `import`(url: URL) {
guard let reader = RecordReader(url: url) else { return }
self.resume(from: reader)
}
private func resume(from reader: RecordReader) {
while let _ = reader.nextData() {
}
//Checking memory here is about 300Mb, the test file size.
return
}
}
class RecordReader {
private var fileHandle: FileHandle
init?(url: URL) {
do {
self.fileHandle = try FileHandle(forReadingFrom: url)
}
catch {
return nil
}
}
func nextData() -> Data? {
let data = self.fileHandle.readData(ofLength: 10000)
guard !data.isEmpty else { return nil }
return data
}
}
After the while loop is done, the memory is about 300Mb, even though I am reading the data in 10Kb chunks.
For some reason the chunks are not being released.
Any idea what might be happening?
I had tried before wrapping my use of the data in a autoreleasepool, like:
func nextData() -> Data? {
let data = autoreleasepool  {
let data = self.fileHandle.readData(ofLength: 10000)
guard !data.isEmpty else { return nil }
return data
}
return data
}
}
What I didn't realize is that is the reading action the one that needs to be inside:
autoreleasepool  {
while let data = reader.nextData() {
print(data.count)
}
}
This works correctly. Thanks Martin for the hint.

Can't get UIButton to call block of code in Swift 4

I am trying to call a large block of code to run once a button (GoButton) is clicked, I have tried doing it this way but it just crashes, I also sometimes get the error that my button is equal to nil. any advice? All of the code below works just fine without the button being there, I want it to run once pressed. Also I'm new to swift programming so sorry if this is really easy. Thanks
Code Below:
override func viewDidLoad() {
super.viewDidLoad()
func GoButton(_ sender: Any) {
guard let APIUrl = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=Crowland&appid=***APIKEY***&units=Metric") else { return }
URLSession.shared.dataTask(with: APIUrl) { data, response, error in
guard let data = data else { return }
let decoder = JSONDecoder()
do {
let weatherData = try decoder.decode(MyWeather.self, from: data)
if (self.MainLabel != nil)
{
if let gmain = weatherData.weather?.description {
print(gmain)
DispatchQueue.main.async {
self.MainLabel.text! = "Current Weather in: " + String (gmain)
}
}
}
if (self.LocationLabel != nil)
{
if let gmain = weatherData.name {
print(gmain)
DispatchQueue.main.async {
self.LocationLabel.text! = "Current Weather in: " + String (gmain)
}
}
}
if (self.HumidityLabel != nil)
{
if let ghumidity = weatherData.main?.humidity
{
print(ghumidity, "THIS IS HUMIDITY")
DispatchQueue.main.async {
self.HumidityLabel.text! = String (ghumidity)
}
}
}
if (self.WindLabel != nil)
{
if let gspeed = weatherData.wind?.speed {
print(gspeed, "THIS IS THE SPEED")
DispatchQueue.main.async {
self.WindLabel.text! = String(gspeed) + " mph"
}
}
}
if (self.TempLabel != nil)
{
if let ggtemp = weatherData.main?.temp {
print(ggtemp, "THIS IS THE TEMP")
DispatchQueue.main.async {
self.TempLabel.text! = String (ggtemp) + " c"
}
}
}
} catch {
print(error.localizedDescription)
}
}.resume()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
It seems like your GoButton function is somehow inside your viewDidLoad method? This should definitely not be the case as nested functions are hidden from the outside world by default.
If you are using storyboard, the correct way to link a button click to an action is to do it through the storyboard and IBActions. There are very many video/text tutorials showing you how to do so. Here is a video tutorial that I personally liked because of its clarity given that you are new to Swift - https://www.youtube.com/watch?v=Opz3juSW43Q. (I neither own nor have any affiliation with the video content in this link, I am merely linking it here as I think it might be helpful).

How to get an AVAssetReader to loop?

Hi I have been trying to figure out how to implement movie looping in GPUImage2, but have been unsuccessful so far. The MovieInput class in GPUImage2 uses AVAssetReader to playback the movie files, so I researched ways to loop AVAssetReader. I found this question on StackOverFlow dealing with this topic. AVFoundation to reproduce a video loop
The best answer was
"AVAssetReader doesn't support seeking or restarting, it is essentially a sequential decoder. You have to create a new AVAssetReader object to read the same samples again."
I tried to figure out how to connect the old assetReader to a new one one and I was not very successful and it crashed every time.
I was recommended to try something like this, but I am not exactly sure how to write the function generateAssetReader.
public func start() {
self.assetReader = generateAssetReader(asset: asset, readAudio: readAudio, videoOutputSettings: videoOutputSettings, audioOutputSettings: audioOutputSettings)
asset.loadValuesAsynchronously(forKeys:["tracks"], completionHandler:{
DispatchQueue.global(priority:DispatchQueue.GlobalQueuePriority.default).async(execute: {
guard (self.asset.statusOfValue(forKey: "tracks", error:nil) == .loaded) else { return }
guard self.assetReader.startReading() else {
print("Couldn't start reading")
return
}
var readerVideoTrackOutput:AVAssetReaderOutput? = nil;
for output in self.assetReader.outputs {
if(output.mediaType == AVMediaTypeVideo) {
readerVideoTrackOutput = output;
}
}
while (self.assetReader.status == .reading) {
self.readNextVideoFrame(from:readerVideoTrackOutput!)
}
if assetReader.status == .completed {
assetReader.cancelReading()
self.assetReader = nil
if self.loop {
self.start()
} else {
self.endProcessing()
}
}
}
Would anyone have a clue into solving this looping problem? This is a link to entire code of the MovieInput class.
https://github.com/BradLarson/GPUImage2/blob/master/framework/Source/iOS/MovieInput.swift
I found the answer in case anyone is wondering.
public func createReader() -> AVAssetReader
{
var assetRead:AVAssetReader!
do{
assetRead = try AVAssetReader.init(asset: self.asset)
let outputSettings:[String:AnyObject] = [(kCVPixelBufferPixelFormatTypeKey as String):NSNumber(value:Int32(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange))]
let readerVideoTrackOutput = AVAssetReaderTrackOutput(track:self.asset.tracks(withMediaType: AVMediaTypeVideo)[0], outputSettings:outputSettings)
readerVideoTrackOutput.alwaysCopiesSampleData = false
assetRead.add(readerVideoTrackOutput)
}catch{
}
return assetRead
}
public func start() {
self.assetReader = createReader()
asset.loadValuesAsynchronously(forKeys:["tracks"], completionHandler:{
DispatchQueue.global(priority:DispatchQueue.GlobalQueuePriority.default).async(execute: {
guard (self.asset.statusOfValue(forKey: "tracks", error:nil) == .loaded) else { return }
guard self.assetReader.startReading() else {
print("Couldn't start reading")
return
}
var readerVideoTrackOutput:AVAssetReaderOutput? = nil;
for output in self.assetReader.outputs {
if(output.mediaType == AVMediaTypeVideo) {
readerVideoTrackOutput = output;
}
}
while (self.assetReader.status == .reading) {
self.readNextVideoFrame(from:readerVideoTrackOutput!)
}
if (self.assetReader.status == .completed) {
self.assetReader.cancelReading()
if (self.loop) {
// TODO: Restart movie processing
self.start()
} else {
self.endProcessing()
}
}
})
})
}

I am calling a url of the image from the webservice. The image is not loaded to uitableview

During the debugging of the code I tested the URL, that url works in the browser and the image is displayed in the browser. But the below code is not loading the image to the image wrapper.
let row = indexPath.row
cell.NewsHeading.font =
UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.NewsHeading.text = SLAFHeading[row]
if let url = NSURL(string: SLAFImages[indexPath.row]) {
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
if error != nil {
print("thers an error in the log")
} else {
dispatch_async(dispatch_get_main_queue()) {
cell.NewsImage.image = UIImage(data: data!)
}
}
}
task.resume()
}
return cell
As explained in the comments, you can't return from an asynchronous task - you can't know when the task will be complete and when the data will be available.
The way to handle this in Swift is to use callbacks, often called by convention "completion handlers".
In this example I create a function to run the network task, and this function has a callback for when the image is ready.
You call this function with a syntax named "trailing closure" and there you handle the result.
Here's an example for you.
The new function:
func getNewsImage(stringURL: String, completion: (image: UIImage)->()) {
if let url = NSURL(string: stringURL) {
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
if error != nil {
print(error!.localizedDescription)
} else {
if let data = data {
if let image = UIImage(data: data) {
completion(image: image)
} else {
print("Error, data was not an image")
}
} else {
print("Error, no data")
}
}
}
task.resume()
}
}
Your elements from your example:
let row = indexPath.row
cell.NewsHeading.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.NewsHeading.text = SLAFHeading[row]
And how you call the new function:
getNewsImage(SLAFImages[indexPath.row]) { (image) in
dispatch_async(dispatch_get_main_queue()) {
cell.NewsImage.image = image
// here you update your UI or reload your tableView, etc
}
}
It's just an example to show how it works, so you might have to adapt to your app, but I believe it demonstrates what you need to do.

Swift iOS -Jump out of condition early if error is found

I'm using CoreML and Vision to analyze a photo taken with the camera or imported from the library. Once the photo is obtained I run some code to make sure the photo is valid and if it is it returns true otherwise it returns false. I use it like so:
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if let error = error {
// display alert there is a problem
return
}
guard let imageData = photo.fileDataRepresentation(), let previewImage = UIImage(data: imageData) else {
// display alert there is a problem
return
}
if useVisionAndCoreMLToCheckIfIsImageValid(image: previewImage) {
tableData.append(previewImage)
} else {
// display alert image was not valid
}
}
The problem is there are 4 points inside the useVisionAndCoreMLToCheckIfIsImageValid function that can go wrong and I need to return false so I can jump out of the function and if it is valid there is 1 point where it can go right and I need to return true. But since the function returns a Bool I keep getting errors when trying to return true or false at those points:
How can I get rid of the above errors?
func useVisionAndCoreMLToCheckIfIsImageValid(image: UIImage) -> Bool {
if let cgImage = image.cgImage {
let foodModel = CustomFoodModel()
guard let model = try? VNCoreMLModel(for: foodModel.model) else {
return false
}
let request = VNCoreMLRequest(model: model) { [weak self](request, error) in
if let error = error {
// 1st point - if there is an error return false
return false
}
guard let results = request.results as? [VNClassificationObservation], let topResult = results.first else {
// 2nd point - if there is a nil value here return false
return false
}
if topResult.confidence > 0.8 {
// 3rd point - if confidence is greater then 80% return true
return true
} else {
// 4th point - if confidence is less then 80% return false
return false
}
}
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
do {
try handler.perform([request])
} catch let err as NSError {
// 5th point - if there is a try error return false
return false
}
}
}
// if the cgImage is nil return false
return false
}
The return statements you get errors for are indeed return statements for the closures e.g. - VNCoreMLRequest(model: model) { - return for this block - },
VNCoreMLRequest(model: model) { -return for this block- } and not for the useVisionAndCoreMLToCheckIfIsImageValid function itself.
You need to rework your strategy and understand the async picture in your case.
I strongly recommend to first get some knowledge on the topic rather than copy-paste a solution you don't truly understand.
Some blog post on the async programming:
https://ashfurrow.com/blog/comparative-asynchronous-programming/
I was initially going to use completionHandlers instead of a bool but I thought there was an easier way which there isn't. #vadian sent me a link in the comments to a similar SO question that basically said what I'm trying to do cannot be done because as DenislavaShentova's answer said the return statements with error are for the blocks and not the function itself.
Here's the useVisionAndCoreMLToCheckIfIsImageValid code signature using 2 completionHandlers instead of returning a Bool
func useVisionAndCoreMLToCheckIfIsImageValid(image: UIImage, falseCompletion: #escaping ()->(), trueCompletion: #escaping ()->()) {
if let cgImage = image.cgImage {
let foodModel = CustomFoodModel()
guard let model = try? VNCoreMLModel(for: foodModel.model) else {
falseCompletion()
return
}
let request = VNCoreMLRequest(model: model) { [weak self](request, error) in
if let error = error {
// 1st point - run code for false
falseCompletion()
return
}
guard let results = request.results as? [VNClassificationObservation], let topResult = results.first else {
// 2nd point - run code for false
falseCompletion()
return
}
if topResult.confidence > 0.8 {
// 3rd point - run code for false
trueCompletion()
} else {
// 4th point - run code for false
falseCompletion()
}
}
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
do {
try handler.perform([request])
} catch let err as NSError {
// 5th point - run code for false
falseCompletion()
}
}
}
// if the cgImage is nil run code for false
falseCompletion()
}
and here is the function is use:
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if let error = error {
// display alert there is a problem
return
}
guard let imageData = photo.fileDataRepresentation(), let previewImage = UIImage(data: imageData) else {
// display alert there is a problem
return
}
useVisionAndCoreMLToCheckIfIsImageValid(image: previewImage, falseCompletion: thereIsAProblem, trueCompletion: imageIsValid)
}
func imageIsValid() {
tableData.append(previewImage)
}
func thereIsAProblem() {
// do something
}

Resources