Search code examples
iosswiftscreenshotwkwebview

How to take screenshot of portion of UIView?


I want the user to go on my app and take a screenshot of the app after pressing a button programmatically in Swift. I know that UIGraphicsGetImageFromCurrentImageContext() takes a screenshot but I don't want a picture of the entire screen. I want a rectangle to pop up (sort of like a crop tool) and the user can drag and resize the rectangle to take a screenshot of only a certain part of the screen. I want the rectangle to go over a WKWebView and crop a pic of the web view.


Solution

  • The standard snapshot technique is drawHierarchy(in:afterScreenUpdates:), drawing that to an image context. In iOS 10 and later, you can use UIGraphicsImageRenderer:

    extension UIView {
    
        /// Create image snapshot of view.
        ///
        /// - Parameters:
        ///   - rect: The coordinates (in the view's own coordinate space) to be captured. If omitted, the entire `bounds` will be captured.
        ///   - afterScreenUpdates: A Boolean value that indicates whether the snapshot should be rendered after recent changes have been incorporated. Specify the value false if you want to render a snapshot in the view hierarchy’s current state, which might not include recent changes. Defaults to `true`.
        ///
        /// - Returns: The `UIImage` snapshot.
    
        func snapshot(of rect: CGRect? = nil, afterScreenUpdates: Bool = true) -> UIImage {
            return UIGraphicsImageRenderer(bounds: rect ?? bounds).image { _ in
                drawHierarchy(in: bounds, afterScreenUpdates: afterScreenUpdates)
            }
        }
    }
    

    And you’d use that like so:

    let image = webView.snapshot(of: rect)
    

    Prior to iOS 10, you would to get portion of an image, you can use CGImage method cropping(to:). E.g.:

    extension UIView {
    
        /// Create snapshot
        ///
        /// - Parameters:
        ///   - rect: The coordinates (in the view's own coordinate space) to be captured. If omitted, the entire `bounds` will be captured.
        ///   - afterScreenUpdates: A Boolean value that indicates whether the snapshot should be rendered after recent changes have been incorporated. Specify the value false if you want to render a snapshot in the view hierarchy’s current state, which might not include recent changes. Defaults to `true`.
        ///
        /// - Returns: Returns `UIImage` of the specified portion of the view.
    
        func snapshot(of rect: CGRect? = nil, afterScreenUpdates: Bool = true) -> UIImage? {
            // snapshot entire view
    
            UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0)
            drawHierarchy(in: bounds, afterScreenUpdates: afterScreenUpdates)
            let wholeImage = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
    
            // if no `rect` provided, return image of whole view
    
            guard let image = wholeImage, let rect = rect else { return wholeImage }
    
            // otherwise, grab specified `rect` of image
    
            guard let cgImage = image.cgImage?.cropping(to: rect * image.scale) else { return nil }
            return UIImage(cgImage: cgImage, scale: image.scale, orientation: .up)
        }
    
    }
    

    Which uses this little convenient operator:

    extension CGRect {
        static func * (lhs: CGRect, rhs: CGFloat) -> CGRect {
            return CGRect(x: lhs.minX * rhs, y: lhs.minY * rhs, width: lhs.width * rhs, height: lhs.height * rhs)
        }
    }
    

    And to use it, you can do:

    if let image = webView.snapshot(of: rect) {
        // do something with `image` here
    }
    

    For Swift 2 rendition, see previous revision of this answer.