Nch Nch - 7 months ago 20
Swift Question

UI freezes for 5 sec after I dismiss UIViewController

I have a UICollectionView of images and a full image view that I open when any image is selected.
opening the full image works fine, but once I dismiss it, the whole app freezes for more than 5 sec. I read that the main thread might be overloaded which would explain the lag but I'm not sure which threads to move to and where, if that is the issue.

import UIKit

class FullImageViewController: UIViewController , UIScrollViewDelegate {

var scroller: UIScrollView!
var width = CGFloat()
var images = [Image]()
var imageArray = [UIImage]()
var imgs: [UIImage] = [UIImage(named: "pug7")!, UIImage(named: "pug6")!, UIImage(named: "pug7")!, UIImage(named: "pug6")!]
var index = Int()
var fullImagesAdded = Bool()

init(width: CGFloat, images: [Image], index: Int, imageArray: [UIImage]) {

super.init(nibName: nil, bundle: nil)
self.width = width
self.images = images
self.index = index
self.imageArray = imageArray
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()

let downSwipe = UISwipeGestureRecognizer(target: self, action: #selector(FullImageViewController.swipeDown))
downSwipe.direction = .Down
view.addGestureRecognizer(downSwipe)

self.scroller = UIScrollView()
self.scroller.delegate = self
var i = 0
for img in images {
let aimageDetailViewController = ImageDetailViewController(index: self.index, image: img)
self.scroller.addSubview(aimageDetailViewController.view)
var aimageDetailViewFrame: CGRect = aimageDetailViewController.view.frame
aimageDetailViewFrame.origin.x = (super.view.frame.width) * CGFloat(i)
aimageDetailViewController.view.frame = aimageDetailViewFrame

i += 1
}

self.scroller.backgroundColor = UIColor.blackColor()
self.scroller.contentSize = CGSizeMake(self.view.frame.width * CGFloat(images.count), self.view.frame.height)
self.scroller.bounces = false
self.scroller.showsHorizontalScrollIndicator = false
self.scroller.setContentOffset(CGPoint(x: self.view.frame.width * CGFloat(index), y: 0), animated: false)
self.scroller.pagingEnabled = true

self.view.addSubview(scroller)
}

func swipeDown(){
dispatch_async(dispatch_get_main_queue()) {
self.dismissViewControllerAnimated(true, completion: nil)
}
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scroller.frame = view.bounds
}
}


This is where I call my full image view in my UICollectionView:

func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let fullImage = FullImageViewController(width: CGFloat(imageURLSCache.count), images: imageURLSCache, index: Int(indexPath.row), imageArray: images)
self.presentViewController(fullImage, animated: true, completion: nil)
}


Thank you!

This is the extension I use to download the images of both the full image view and the collection view

extension UIImageView {

func downloadFrom(link link:String?, cache: NSCache ) {
image = UIImage(named: "logo")
self.alpha = 0.3
let data: NSData? = cache.objectForKey(link!) as? NSData


if let cachedData = data {
let image = UIImage(data: cachedData)
dispatch_async(dispatch_get_main_queue(), {() in
self.image = image
self.alpha = 1
images.append(self.image!)
})
return
}


if link != nil, let url = NSURL(string: link!) {
NSURLSession.sharedSession().dataTaskWithURL(url) { data, response, error in
guard let data = data where error == nil else {
print("\nerror on download \(error)")
return
}
if let httpResponse = response as? NSHTTPURLResponse where httpResponse.statusCode != 200 {
print("statusCode != 200; \(httpResponse.statusCode)")
return
}
dispatch_async(dispatch_get_main_queue()) {
print("\ndownload completed \(url.lastPathComponent!)")
self.image = UIImage(data: data)
self.alpha = 1
cache.setObject(data, forKey: link!)
}
}.resume()
} else {
dispatch_async(dispatch_get_main_queue(), {
self.image = UIImage(named: "logo")
})

}
}


}

Answer
func swipeDown(){
    dispatch_async(dispatch_get_main_queue()) {
        self.dismissViewControllerAnimated(true, completion: nil)
    }
}

This method has been set as the action to your UISwipeGestureRecognizer. It will be called on the main thread by the gesture recognizer, so you don't need to dispatch it to the main queue. That adds a delay depending on how busy that queue already is.

Change it to just:

func swipeDown() {
    self.dismissViewControllerAnimated(true, completion: nil)
}

Update regarding the additional info you've added:

That extension on UIImageView isn't great (i.e., it assumes link is not nil when using it as a key for the cache, but then later checks if it's nil, which it can't be or you would have already crashed). But it at least does do the download in the background using the dataTaskWithURL method.

I would try calling it inside a dispatch to background call as shown below to ensure you're not overloading the main thread with the cache checks etc. I assume you're making a cache for this extension safely, and of course reusing the cache for all calls, not making a new cache for each call (which I've seen done).

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
    imageView.downloadFrom(....)
}

Here's an alternate extension to use. This is one I've used a lot. It handles creating the cache for you, so you don't have to worry about doing that outside of the extension. It also has the benefit of confirming another URL hasn't been set on the image since it started downloading. This is important when the image view is on a reusable cell for example. Create cell, set image url to x, while it's still downloading, cell is scrolled off screen and reused, the image url is set to y, then the x download completes and shows the image that was meant for the old cell.

Here's the code:

//  UIImageView+XCGAdditions.swift
//  Cerebral Gardens
//
//  Created by Dave Wood
//  Copyright © Cerebral Gardens. All rights reserved.

import UIKit

extension UIImageView {

    func setImageWithLink(link: String) {
        guard let url = NSURL(string: link) else { return }
        setImageWithURL(url: url)
    }

    func setImageWithURL(url url: NSURL) {
        let checkInt = Int(arc4random_uniform(100000))
        tag = checkInt

        struct Statics {
            static var onceToken: dispatch_once_t = 0
            static var imageCache: NSCache!
        }
        dispatch_once(&Statics.onceToken) { () -> Void in
            Statics.imageCache = NSCache()
        }

        if let cachedImage = Statics.imageCache.objectForKey(url) as? UIImage {
            self.image = cachedImage
            return
        }

        NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data: NSData?, response: NSURLResponse?, error: NSError?) -> () in
            guard let data = data where error == nil,
              let image = UIImage(data: data) where self.tag == checkInt else { return }

            dispatch_async(dispatch_get_main_queue()) { () -> () in
                Statics.imageCache.setObject(image, forKey: url)
                self.image = image
            }
        }).resume()
    }
}
Comments