Search code examples
iosswiftuiviewsplitsnapshot-view

Speeding up the fragmentation of a view


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.


Solution

  • 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
    }