Search code examples
swiftuigeometryreaderzstack

GeometryReader and ZStack to product screenshot


My goal: To use geometry reader and a screenshot function (already written) to grab just the small portion of an image that falls behind a close button.

Setup: I have a ZStack with an Image and a smaller button overtop of it (it is a system image for this example). Currently, I have the close button aligned to the top of the background image and everything looks good at first glance.

However, when the close button is tapped, I need to grab a screenshot of the close button itself (matching frame and size).

Everything worked when my ZStack had a .center alignment, but now that I have set alignment: .top, my screenshot is still occurring in the center of the image.

How can I shift the GeometryReader that encompasses the close button to allow for the screenshot to be of the close button (and a bit of extra background behind it) when it is tapped?

My Code:

struct ImageView: View {
    var body: some View {
        ZStack(alignment: .top) {
            Image("ZoomedBuzz")
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: UIScreen.main.bounds.width)
                
            GeometryReader { geo in
                Image(systemName: "xmark.circle.fill")
                    .font(.system(size: 80))
                    .border(Color.blue)
                    .onTapGesture {
                        let globalImage = takeScreenshot(origin: geo.frame(in: .global).origin,
                                                         size: geo.size)
                        print(globalImage)
                    }
            }
            .frame(width: 80, height: 80, alignment: .top)
        }
    }
}

Taking the screenshot:

extension UIView {
    var renderedImage: UIImage {
        // rect of capture
        let rect = self.bounds
        // create the context of bitmap
        UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
        let context: CGContext = UIGraphicsGetCurrentContext()!
        self.layer.render(in: context)
        // get a image from current context bitmap
        let capturedImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return capturedImage
    }
}

extension View {
    func takeScreenshot(origin: CGPoint, size: CGSize) -> UIImage {
        let window = UIWindow(frame: CGRect(origin: origin, size: size))
        let hosting = UIHostingController(rootView: self)
        hosting.view.frame = window.frame
        window.addSubview(hosting.view)
        window.makeKeyAndVisible()
        return hosting.view.renderedImage
    }
}

Simulator Screen

Desired Result (but taken at the top of the ZStack):

Desired Result

Current Result (screenshot being taken in the middle of the background image centered with the ZStack):

Current Result


Solution

  • If you want the GeometryReader to be close around the button instead of capturing the whole screen you can set it as the buttons background.

    Unfortunately I don't know how your screenshot method works therefore I couldn't verify that it's working with your setup, but now you can simply screenshot the whole GeometryReader's frame.

    struct ImageView: View {
        
        @State private var geo: GeometryProxy!
        
        var body: some View {
            ZStack(alignment: .top) {
                Image("ZoomedBuzz")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: UIScreen.main.bounds.width)
                    
                Image(systemName: "xmark.circle.fill")
                    .font(.system(size: 80))
                    .border(Color.blue)
                    .background(rectReader())
                    .onTapGesture {
                        let globalImage = takeScreenshot(origin: geo.frame(in: .global).origin,
                                                         size: geo.size)
                        print(globalImage)
                    }
            }
        }
        
        private func rectReader() -> some View {
            return GeometryReader { geometry -> AnyView in
                DispatchQueue.main.async {
                    self.geo = geometry
                }
                return AnyView(Rectangle().fill(Color.clear))
            }
        }
    }