1

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.

ajrlewis
  • 2,968
  • 3
  • 33
  • 67
  • If you are looking at a decent amount (and 20 is clearly that) amount to"break up an image", you should not be using nothing but `UIKit` and `UIView`. You're hitting a threshold to look into using the CPU. It'll require one refactoring but *at least* CoreGraphics, maybe more. (It does depend on more details.) –  Aug 20 '18 at 00:59
  • @dfd Thanks for your advice. To confirm, are you suggesting that I look into using CoreGraphics, or that this is a CPU limitation? – ajrlewis Aug 20 '18 at 07:35

1 Answers1

2

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
}
ChrisF
  • 134,786
  • 31
  • 255
  • 325
Jeshua Lacock
  • 5,730
  • 1
  • 28
  • 58
  • Thanks for this solution, works well. However, `containerView.drawHierarchy` should just be `drawHierarchy` (and likewise in a few other places) as it's really the `UIView` itself that I'd like to fragment (rather than the `superview`). Thanks anyway for your help! – ajrlewis Sep 05 '18 at 10:26
  • Cheers, glad it works for you. Sorry about that, guess I must have been a bit confused! ;) – Jeshua Lacock Sep 05 '18 at 10:29
  • BTW: just curious, but is this for some kind of screen exploding animation? – Jeshua Lacock Sep 05 '18 at 10:30
  • Yep exactly - so pass it a `UICollectionViewCell` that you want to delete say, ( `let fragments cell.fragment ....`) and then remove the `fragments` from the `superview` after some sort of animation of the `fragment`s `x` and `y` positions. :-) – ajrlewis Sep 05 '18 at 11:31