Memory leak when displaying a modal view and dismissing it - swift

When an AVExportSession is finished exporting, I have my app display a modal view displaying the video and an array of images. Dismissing the modal view, and making it display again over and over shows a memory increase that continuously grows. I'm suspicious of a strong reference cycle that could be occurring.
I'm setting required variables on the modal view (manageCaptureVC). fileURL is a global variable that manageCaptureVC can read from to get the video. The video is removed based on that URL when the modal view is dismissed. The leak is larger depending on the size of the media that is captured and displayed in the modal view.
I have used the Leaks Instrument. Unfortunately, it never points to any of my functions. It shows memory addresses that displays assembly language. I am also using a device.
Here is a screen shot of my leaks instrument at the point I display and dismiss my view, and the instrument indicates leaks:
Anything obvious what could cause a leak in my case?
Presenting the modal view (manageCaptureVC)
// video done exporting
guard let exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) else { return }
exporter.outputURL = mainVideoURL
exporter.outputFileType = AVFileType.mov
let manageCaptureVC = self.storyboard?.instantiateViewController(withIdentifier: "ManageCaptureVC") as! ManageCaptureVC
exporter.exportAsynchronously(completionHandler: {[weak self]
() -> Void in
let fileManagement = FileManagement()
fileManagement.checkForAndDeleteExportFile() // delete export file
self?.myTimer.invalidate()
fileURL = mainVideoURL
guard let imgCaptureModeRawVal = self?.imageCaptureMode.rawValue else { return }
manageCaptureVC.imageCaptureMode = ManageCaptureVC.imageCaptureModes(rawValue: imgCaptureModeRawVal)!
manageCaptureVC.delegate = self
DispatchQueue.main.async(){
manageCaptureVC.modalPresentationStyle = .fullScreen
self?.present(manageCaptureVC, animated: true, completion: nil)
}
})
Dismissing the view:
func goBackTask(){
// turn off manage capture tutorial if needed
if debug_ManageCaptureTutorialModeOn {
debug_ManageCaptureTutorialModeOn = false
delegate?.resetFiltersToPrime()
}
// no longer ignore interface orientation
ignoreSelectedInterfaceOrientation = false
// remove observer for the application becoming active in this view
NotificationCenter.default.removeObserver(self,
name: UIApplication.didBecomeActiveNotification,
object: nil)
if let videoEndedObs = self.videoEndedObserver {
NotificationCenter.default.removeObserver(videoEndedObs)
}
// invalidate thumb timer
thumbColorTimer.invalidate()
// empty UIImages
uiImages.removeAll()
// delete video
let fileManagement = FileManagement()
fileManagement.checkForAndDeleteFile()
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
self.enableButtons(enabled:false)
if let p = self.player, let pl = self.playerLayer {
p.pause()
pl.removeObserver(self, forKeyPath: "videoRect")
pl.removeFromSuperlayer()
p.replaceCurrentItem(with: nil)
}
group.leave()
}
let group2 = DispatchGroup()
group.notify(queue: .main) {
group2.enter()
DispatchQueue.main.async {
self.enableButtons(enabled:true)
group2.leave()
}
}
group2.notify(queue: .main) {
self.dismiss(animated: true)
}
}

I came across this problem as well. It took me days to track it down.
Setting modalPresentationStyle to .fullScreen resulted in the View Controller not being released. I was able to reproduce this on a trivially simple example.
I got round it by setting modalPresentationStyle to .currentContext.
None of the Instruments identified this retain cycle - I guess because it was in low level Apple code.

Related

Swift: Apply CIFilter to video error - unfinished AVAsynchronousVideoCompositionRequest deallocated

I'm building a video editor that lets you apply a CIFilter to a video. And it works well.
The only problem I'm facing is that when I dismiss the ViewController I get this error:
Unfinished AVAsynchronousVideoCompositionRequest deallocated - should
have called finishWithComposedVideoFrame:, finishWithError: or
finishCancelledRequest
This error doesn't make the app crash or slower, but when I try to edit another video the preview in the AVPlayer becomes black.
This is my current code:
var mutableComposition = AVMutableVideoComposition()
let exposureFilter = CIFilter.exposureAdjust()
override func viewDidLoad() {
updateComposition()
}
func updateComposition() {
mutableComposition = AVMutableVideoComposition(asset: player.currentItem!.asset, applyingCIFiltersWithHandler: { [weak self] request in
guard let self = self else {
return
}
self.exposureFilter.inputImage = request.sourceImage.clampedToExtent()
self.exposureFilter.ev = 5
let output = self.exposureFilter.outputImage!.cropped(to: request.sourceImage.extent)
request.finish(with: output, context: nil)
})
player.currentItem?.videoComposition = mutableComposition
}
If I remove the [weak self] no error it's printed, but it keeps the ViewController in memory when I dismiss it, creating an unwanted memory leak.

NotificationCenter obersever not called

I have UITabBarController and two ViewControllers.
In the first vc I have a video, after it ends the data transfer to another controller is triggered. But unfortunately nothing happens in the second controller.
func fetchCaloriesData() {
refWorkout = workout!.title
Database.database().reference().child("programs").child(refWorkout).child("calories").observeSingleEvent(of: .value) { (snapshot) in
self.caloriesData = snapshot.value as! String
print(self.caloriesData)
self.dictionary["calories"] = self.caloriesData
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "sendCalories"), object: nil, userInfo: self.dictionary)
print("Заполнили словарь")
print(self.dictionary["calories"])
//self.caloriesDelegate?.update(newText: caloriesData)
// self.dietVC.BurnedLabel.text? = caloriesData
}
}
Everything is work. When I try print I can see: Optional ("...")
In second vc in viewDidLoad I have next code.
NotificationCenter.default.addObserver(self, selector: #selector(gotNotification), name: NSNotification.Name("sendCalories"), object: nil)
and next
#objc func gotNotification(notification: Notification) {
guard let userInfo = notification.userInfo else {return}
guard let calories = userInfo["calories"] as? String else {return}
print("Наблюдатель")
print(calories)
BurnedLabel.text! = calories
}
Nothing happens. What am I doing wrong ?
NotificationCenter.default.addObserver(self, selector: #selector(gotNotification)
this will be called only if view is loaded before notification is triggered,
so you need to pass data in different way when you tapped to open second view controller. After that observer is in on state, so he is monitoring changes. Basically if you downloading something asynchronous and that is not finished while you open second view controller, after that if downloading is done, your second view will be updated, be sure to update UI on main thread. In your case make variable with calories object in second controller, then from first controller update main tab bar controller(add calories property there as well) with delegate method(make protocol to pass calories) so you don't shoot notification everywhere and from there update second controller.
That is how you will have fresh data always.
guard let firstController = controllers[0] as? FirstController else {return}
firstController.caloriesDelegate = self
guard let secondViewController = controllers[0] as? SecondController else {return}
secondViewController.calories = self.calories // in main tab bar after updating

Autolayout images inside cells in tableview. Correct layout but only once scrolling down and back up?

Im trying to get tableview cells with auto resizing images to work. Basically I want the image width in the cell to always be the same, and the height to change in accordance with the aspect ratio of the image.
I have created a cell class, which only has outlets for a label, imageView and a NSLayoutConstraint for the height of the image. I have some async methods to download an image and set it as the image for the cell imageView. Then the completion handle gets called and I run the following code to adjust the height constraint to the correct height:
cell.cellPhoto.loadImageFromURL(url: photos[indexPath.row].thumbnailURL, completion: {
// Set imageView height to the width
let imageSize = cell.cellPhoto.image?.size
let maxHeight = ((self.tableView.frame.width-30.0)*imageSize!.height) / imageSize!.width
cell.cellPhotoHeight.constant = maxHeight
cell.layoutIfNeeded()
})
return cell
And here is the UIImageView extension I wrote which loads images:
func loadImageFromURL(url: String, completion: #escaping () -> Void) {
let url = URL(string: url)
makeDataRequest(url: url!, completion: { data in
DispatchQueue.main.async {
self.image = UIImage(data: data!)
completion()
}
})
}
And the makeDataRequest function which it calls:
func makeDataRequest(url: URL, completion: #escaping (Data?) -> Void) {
let session = URLSession.shared
let task = session.dataTask(with: url, completionHandler: { data, response, error in
if error == nil {
let response = response as? HTTPURLResponse
switch response?.statusCode {
case 200:
completion(data)
case 404:
print("Invalid URL for request")
default:
print("Something else went wrong in the data request")
}
} else {
print(error?.localizedDescription ?? "Error")
}
})
task.resume()
}
This works for all the cells out of frame, but the imageviews in the cells in the frame are small. Only when I scroll down and then back up again do they correctly size. How do I fix this? I know other people have had this issue but trying their fixes did nothing.
I had to sorta recreate the problem to understand what was going on. Basically you need to reload the tableview. I would do this when a picture finishes downloading.
In the view controller that has the table view var. Add this to the viewDidLoad() function.
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
//Create a notification so we can update the list from anywhere in the app. Good if you are calling this from an other class.
NotificationCenter.default.addObserver(self, selector: #selector(loadList), name: NSNotification.Name(rawValue: "loadList"), object: nil)
}
//This function updates the cells in the table view
#objc func loadList(){
//load data here
self.tableView.reloadData()
}
Now, when the photo is done downloading, you can notify the viewcontroller to reload the table view by using the following,
func loadImageFromURL(url: String, completion: #escaping () -> Void) {
let url = URL(string: url)
makeDataRequest(url: url!, completion: { data in
DispatchQueue.main.async {
self.image = UIImage(data: data!)
completion()
//This isn't the best way to do this as, if you have 25+ pictures,
//the list will pretty much freeze up every time the list has to be reloaded.
//What you could do is have a flag to check if the first 'n' number of cells
//have been loaded, and if so then don't reload the tableview.
//Basically what I'm saying is, if the cells are off the screen who cares.
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "loadList"), object: nil)
}
})
}
Heres something I did to have better Async, see below.
My code as follows, I didn't do the resizing ratio thing like you did but the same idea applies. It's how you go about reloading the table view. Also, I personally don't like writing my own download code, with status code and everything. It isn't fun, why reinvent the wheel when someone else has done it?
Podfile
pod 'SDWebImage', '~> 5.0'
mCell.swift
class mCell: UITableViewCell {
//This keeps track to see if the cell has been already resized. This is only needed once.
var flag = false
#IBOutlet weak var cellLabel: UILabel!
#IBOutlet weak var cell_IV: UIImageView!
override func awakeFromNib() { super.awakeFromNib() }
}
viewController.swift (Click to see full code)
I'm just going to give the highlights of the code here.
//Set the image based on a url
//Remember this is all done with Async...In the backgorund, on a custom thread.
mCell.cell_IV.sd_setImage(with: URL(string: ViewController.cell_pic_url[row])) { (image, error, cache, urls) in
// If failed to load image
if (error != nil) {
//Set to defult
mCell.cell_IV.image = UIImage(named: "redx.png")
}
//Else we got the image from the web.
else {
//Set the cell image to the one we downloaded
mCell.cell_IV.image = image
//This is a flag to reload the tableview once the image is done downloading. I set a var in the cell class, this is to make sure the this is ONLY CALLED once. Otherwise the app will get stuck in an infinite loop.
if (mCell.flag != true){
DispatchQueue.main.asyncAfter(deadline: .now() + 0.025){ //Nothing wrong with a little lag.
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "loadList"), object: nil)
mCell.flag = true
}
}
}
}

Refreshing AVCaptureSession...?

I am having some difficulty with AVCaptureSession when popping view controllers. I have a view controller in a navigation controller where a user takes a photo. After the photo is captured, I segue to a "preview photo" view controller. If the user doesn't like the photo, they can go back and re take it. When I pop the preview photo view controller, the app crashes with error "Multiple audio/video AVCaptureInputs are not currently supported'"
I thought that maybe I can remove/ refresh the input session but it's still crashing.
Any support/ advice is greatly appreciated!
segue:
#IBAction func cancelPressed(_ sender: UIButton) {
_ = self.navigationController?.popViewController(animated: true)
}
camera config (which works fine):
func setupCaptureSessionCamera() {
//this makes sure to get full res of camera
captureSession.sessionPreset = AVCaptureSession.Preset.photo
var devices = AVCaptureDevice.devices(for: .video)
//query available devices
for device in devices {
if device.position == .front {
frontFacingCamera = device
} else if device.position == .back {
backFacingCamera = device
}
}//end iteration
//set a default device
currentDevice = backFacingCamera
//configure session w output for capturing still img
stillImageOutput = AVCaptureStillImageOutput()
stillImageOutput?.outputSettings = [AVVideoCodecKey : AVVideoCodecType.jpeg]
do {
let captureDeviceInput = try AVCaptureDeviceInput(device: currentDevice!)
captureSession.addInput(captureDeviceInput)
captureSession.addOutput(stillImageOutput!)
//setup camera preview layer
cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
//add the preview to our specified view in the UI
view.layer.addSublayer(cameraPreviewLayer!)
cameraPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
cameraPreviewLayer?.frame = cameraView.frame
captureSession.startRunning()
} catch let error {
print(error)
}//end do
}
What I tried (remove inputs in view will appear if the sender is preview photo controller):
func refreshCamera() {
captureSession.beginConfiguration()
for input in captureSession.inputs {
captureSession.removeInput(input as! AVCaptureDeviceInput)
}
captureSession.commitConfiguration()
}
It was much simpler than I was imagining. All that is needed is to first check if there is already input or not before calling the setupCameraSession method:
if captureSession.inputs.isEmpty {
setupCaptureSessionCamera()
}

Function Calling After ViewController Is Dismissed

I have a func that calls itself over and over again for a simple little slideshow.
func animateImage(no:Int)
{
println("ANIMATE")
var imgNumber:Int = no
let t:NSTimeInterval = 1;
let t1:NSTimeInterval = 0;
let url = NSURL(string: self.itemss[imgNumber][0])
let data = NSData(contentsOfURL: url!) //make sure your image in this url does exist, otherwise unwrap in a if let check
imgView!.alpha = 0.4
self.imgView?.image = UIImage(data: data!)
//code to animate bg with delay 2 and after completion it recursively calling animateImage method
UIView.animateWithDuration(2.0, delay: 0, options:UIViewAnimationOptions.CurveEaseOut, animations: {() in
self.imgView!.alpha = 1.0;
},
completion: {(Bool) in
imgNumber++;
if imgNumber>self.itemss.count-1 //only for 4 image
{
imgNumber = 0
}
self.animateImage(imgNumber);
})
}
The problem I'm having is that the func seems to continue to call and doesn't seem to be stopping when i dismiss the view controller ? Why is this func continuing to call after the view controller is dismissed.
#IBAction func dismissMe(sender: AnyObject) {
if((self.presentingViewController) != nil){
self.dismissViewControllerAnimated(false, completion: nil)
println("done")
}
}
The reason that your function is continuing to be called after the view controller is dismissed is because there's no logic that should prevent it from discontinuing to call itself once it is dismissed.
A dismissed view controller should cease to have a parent view controller, however, so we could put an end to this loop by checking if there's a parent view controller at the top of the method:
if self.parentViewController == nil {
return;
}
If this is the first thing your method does, then the method should only continue to execute (and line up another call to itself) if the view controller still has a parent.