Speeding up the fragmentation of a view

286 Views Asked by At

I have the following code (within an extension of UIView) that fragments a UIView into a certain number of pieces:

public func fragment(into numberOfFragments: Int) -> [UIView] {

        var fragments = [UIView]()    

        guard let containerView = superview, let snapshot = snapshotView(afterScreenUpdates: true) else { return fragments }

        let fragmentWidth = snapshot.frame.width / CGFloat(numberOfFragments)
        let fragmentHeight = snapshot.frame.height / CGFloat(numberOfFragments)

        for x in stride(from: 0.0, to: snapshot.frame.width, by: fragmentWidth) {
            for y in stride(from: 0.0, to: snapshot.frame.height, by: fragmentHeight) {

                let rect = CGRect(x: x, y: y, width: fragmentWidth, height: fragmentHeight)

                if let fragment = snapshot.resizableSnapshotView(from: rect, afterScreenUpdates: true, withCapInsets: .zero) {        

                    fragment.frame = convert(rect, to: containerView)
                    containerView.addSubview(fragment)
                    fragments.append(fragment)

                }

            }

        }

        return fragments

    }

However, for numberOfFragments=20 this code takes about 2 seconds to complete. Is there any way of achieving this same result in a faster way? Should I be using an animation/transition instead?

Thanks a lot.

1

There are 1 best solutions below

4
On BEST ANSWER

This solution uses UIImageViews instead of UIViews. It crops a single captured screenshot instead of calling the much more expensive resizableSnapshotView 400 times. Time went from ~2.0 seconds down to 0.088 seconds if afterScreenUpdates is set to false (which works for my test case). If afterScreenUpdates is required for your purposes, then the time is about 0.15 seconds. Still - much much faster than 2.0 seconds!

public func fragment(into numberOfFragments: Int) -> [UIImageView] {

    var fragments = [UIImageView]()

    guard let containerView = superview else { return fragments }

    let renderer = UIGraphicsImageRenderer(size: containerView.bounds.size)
    let image = renderer.image { ctx in
        containerView.drawHierarchy(in: containerView.bounds, afterScreenUpdates: false)
    }

    let fragmentWidth = containerView.frame.width / CGFloat(numberOfFragments)
    let fragmentHeight = containerView.frame.height / CGFloat(numberOfFragments)

    for x in stride(from: 0.0, to: containerView.frame.width, by: fragmentWidth) {
        for y in stride(from: 0.0, to: containerView.frame.height, by: fragmentHeight) {

            let rect = CGRect(x: x, y: y, width: fragmentWidth, height: fragmentHeight)

            if let imageFrag = cropImage(image, toRect: rect) {
                let fragment = UIImageView(image: imageFrag)
                fragment.frame = convert(rect, to: containerView)
                containerView.addSubview(fragment)
                fragments.append(fragment)
            }
        }
    }

    return fragments

}


func cropImage(_ inputImage: UIImage, toRect cropRect: CGRect) -> UIImage?
{

    let imageViewScale = UIScreen.main.scale

    // Scale cropRect to handle images larger than shown-on-screen size
    let cropZone = CGRect(x:cropRect.origin.x * imageViewScale,
                          y:cropRect.origin.y * imageViewScale,
                          width:cropRect.size.width * imageViewScale,
                          height:cropRect.size.height * imageViewScale)

    // Perform cropping in Core Graphics
    guard let cutImageRef: CGImage = inputImage.cgImage?.cropping(to:cropZone)
        else {
            return nil
    }

    // Return image to UIImage
    let croppedImage: UIImage = UIImage(cgImage: cutImageRef)
    return croppedImage
}