0

I'm using this Extension to take an image of a SwiftUI view (for sharing) but the only problem is my view has a cornerRadius of 10.0 so round the corners, so this image ends up with black non rounded corners, how can I get rid of it?

extension View {
  func takeScreenshot(origin: CGPoint, size: CGSize) -> UIImage? {

    // Get the main window.
    guard let window = UIApplication.shared.windows.first else {
      print("View.takeScreenshot: No main window found")
      return nil
    }

    // Create an image of the entire window. Note how we're using `window.bounds` for this
    // to capture the entire window.
    UIGraphicsBeginImageContextWithOptions(window.bounds.size, false, 0.0)
    let renderer = UIGraphicsImageRenderer(bounds: window.bounds, format: UIGraphicsImageRendererFormat())
    let image = renderer.image { (context) in
      window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
    }
    UIGraphicsEndImageContext()

    /*
    At this point we have a screenshot of the entire window.
    Now we're going to crop it to just include the part of the screen
    we want.
    */

    // Scale is the pixel density of the screen. E.g. 3.0 on iPhone 12 Pro which has a 3x display.
    // This will be used in the UIImage extension below.
    let scale = UIScreen.main.scale
    let rect = CGRect(x: origin.x, y: origin.y, width: size.width, height: size.height)
    let croppedImage = image.cropped(boundingBox: rect, scale: scale)
    
      
    return croppedImage
  }
}

extension UIImage {
  func cropped(boundingBox: CGRect, scale: CGFloat) -> UIImage? {

  /*
  To crop UIImage we must first convert it to a CGImage.
  UIImage uses points, which are independent of pixels.

  Therefore, we need to take the scaling factor of the screen into account
  when cropping.

  For example, if we want to crop a 100x50pt square starting at (75, 90) from a UIImage
  on a device with a 2x scaling factor, we would multiple everything by 2 and crop a
  200x100px square starting at (150, 180).
  */

    let x = boundingBox.origin.x * scale
    let y = boundingBox.origin.y * scale
    let width = boundingBox.width * scale
    let height = boundingBox.height * scale

    let adjustedBoundingBox = CGRect(x: x, y: y, width: width, height: height)

    guard let cgImage = self.cgImage?.cropping(to: adjustedBoundingBox) else {
      print("UIImage.cropped: Couldn't create cropped image")
      return nil
    }

    return UIImage(cgImage: cgImage)
  }
}

The View that I am taking a screenshot of:

var shareCard: some View {
        VStack {
            Spacer()
            GeometryReader { geometry in
                if backgroundImage != UIImage() {
                    Image(uiImage: backgroundImage)
                        .resizable()
                        .scaledToFill()
                        .frame(width: geometry.size.width, height: geometry.size.height)
                        .clipped() //needed to add clipped otherwise the picture would go outside of the frame.  From https://sarunw.com/posts/how-to-resize-swiftui-image-and-keep-aspect-ratio/
                        .cornerRadius(10.0)
                        .overlay(
                            Color.black
                                .cornerRadius(10.0)
                                .opacity(0.45) //keep at 0.45?
                        )
                } else {
                    Image("sampleBackground")
                        .resizable()
                    //.scaledToFill()
                        .frame(width: geometry.size.width, height: geometry.size.height)
                        .cornerRadius(10.0)
                        .onAppear {
                            proxy = geometry
                        }
                }
            }
            Spacer()
        }
        .frame(height: 375)
        .padding(.horizontal)
        .overlay(
            TabView(selection: $selectedTabIndex) {
               //Omitted - these are views that are overlayed over the background or image and don't impact the size of the snapshot
               
            }
                .frame(height: 430)
                .padding(.horizontal)
                .tabViewStyle(.page(indexDisplayMode: isTakingSnapShot ? .never : .always))
                .overlay(
                    VStack {
                        switch shareType {
                        case .TodaySummary:
                            VStack {
                                HStack {
                                    HStack(alignment: .center) {
                                        Image("logo")
                                            .resizable()
                                            .aspectRatio(contentMode: .fit)
                                            .frame(height: 40)
                                            .padding(.leading)
                                        VStack(alignment: .leading, spacing: 3) {
                                            Text("AppName")
                                                .foregroundColor(.white)
                                                .font(.headline)
                                                .fontWeight(.bold)
                                           
                                            Text(Date(), style: .date)
                                                .foregroundColor(.white)
                                                .font(.footnote)
                                        }
                                    }
                                    Spacer()
                                }
                                .padding([.leading, .top])
                                Spacer()
                            }
                            .frame(height: 375)
                        case .Workout:
                            EmptyView() //Pass empty view here because we use a different header for workout share
                        }
                    }
                )
        )
        
    }
GarySabo
  • 5,806
  • 5
  • 49
  • 124
  • You really can't. Images are rectangles. – Yrb Apr 19 '22 at 14:22
  • @Yrb or at least make the black corner transparent? – GarySabo Apr 19 '22 at 14:35
  • [See this question](https://stackoverflow.com/q/4885713/7129318) – Yrb Apr 19 '22 at 14:45
  • @Yrb thanks but the solution at the end uses a `UIImageView` whereas I only am working with a `UIImage` – GarySabo Apr 19 '22 at 14:53
  • @GarySabo - can you edit your question and include your sample view that you're trying to capture as a `UIImage`? – DonMag Apr 19 '22 at 16:12
  • @DonMag sure, I added to my question. – GarySabo Apr 19 '22 at 17:44
  • @GarySabo - I have done very little with SwiftUI ... are you trying to get a `UIImage` of *only* the `shareCard` view? It's a little confusing since your `takeScreenshot(...)` func is using the bounds of the app window? – DonMag Apr 19 '22 at 19:58
  • @DonMag sorry should have included I set the GeometryProxy locally (the onAppear in the shareCard) so then I can pass in the bounds via `shareCard.takeScreenshot(origin: proxy.frame(in: .global).origin, size: proxy.size)` – GarySabo Apr 19 '22 at 22:45

1 Answers1

0

As I said in my comment, I've done very little with SwiftUI -- but, this may help get you on your way.

Using code that runs this:

enter image description here

Tapping the red button captures and saves this image:

enter image description here

I tried to make it obvious by giving the main view a yellow background so we can see the transparent rounded corners. Of course, you can't see it here on the page, but if you download the above image (or run the following code and try it for yourself) you'll see that it has transparency.

A few too many unknowns in the code you posted, so I declared my own testCard view. Everything should be pretty straight-forward.

import SwiftUI

struct CaptureView: View {
    
    var testCard: some View {
        VStack {
            Spacer()
            GeometryReader { geometry in
                Image("sampleBackground")
                    .resizable()
                    .frame(width: geometry.size.width, height: geometry.size.height)
                    .cornerRadius(20.0)
            }
            Spacer()
        }
        .frame(width: 320, height: 240)
        .padding(.horizontal)
        .overlay(
            VStack {
                VStack {
                    HStack {
                        HStack(alignment: .center) {
                            Image("logo")
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                .frame(height: 40)
                                .padding(.leading)
                            VStack(alignment: .leading, spacing: 3) {
                                Text("AppName")
                                    .foregroundColor(.white)
                                    .font(.headline)
                                    .fontWeight(.bold)
                                
                                Text(Date(), style: .date)
                                    .foregroundColor(.white)
                                    .font(.footnote)
                            }
                        }
                        Spacer()
                    }
                    .padding([.leading, .top])
                    Spacer()
                }
                .frame(height: 220)
            }
        )
    }
    
    var body: some View {
        VStack {
            Spacer()
            Text("Tap red Button to capture view")
            Spacer()
            testCard
            Spacer()
            Button("Capture testCard View") {
                let img = testCard.snapshot(atSize: CGSize(width: 320.0, height: 240.0))
                saveImage(img)
            }
            .padding()
            .foregroundColor(.white)
            .background(Color.red)
            .cornerRadius(40)
            Spacer()
        }
        .background(Color(red: 1.0, green: 1.0, blue: 0.0))
    }
    
    func saveImage(_ img: UIImage) {
        var s: String = "Results:\n\n"
        
        let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let fName: String = "testCard.png"
        let url = documents.appendingPathComponent(fName)
        if let data = img.pngData() {
            do {
                try data.write(to: url)
            } catch {
                s += "Unable to Write Image Data to Disk"
                print(s)
                return
            }
        } else {
            s += "Could not get png data"
            print(s)
            return
        }
        s += "Logical Size: \(img.size)\n"
        s += "Scale: \(img.scale)\n"
        s += "Actual Size: \(CGSize(width: img.size.width * img.scale, height: img.size.height * img.scale))\n"
        s += "File \"\(fName)\" saved to Documents folder\n"
        print(s)

        // print the path to documents in debug console
        //  so we can copy/paste into Finder to get to the files
        print(documents.path)

    }
}

extension View {
    // if view HAS a valid .intrinsicContentSize
    func snapshot() -> UIImage {
        let controller = UIHostingController(rootView: self)
        let view = controller.view
        
        let targetSize = controller.view.intrinsicContentSize
        view?.bounds = CGRect(origin: .zero, size: targetSize)
        view?.backgroundColor = .clear
        
        let renderer = UIGraphicsImageRenderer(size: targetSize)
        
        return renderer.image { _ in
            view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
        }
    }
    // specify size to save
    func snapshot(atSize targetSize: CGSize) -> UIImage {
        let controller = UIHostingController(rootView: self, ignoreSafeArea: true)
        let view = controller.view
        
        view?.bounds = CGRect(origin: .zero, size: targetSize)
        view?.backgroundColor = .clear
        
        let renderer = UIGraphicsImageRenderer(size: targetSize)
        
        return renderer.image { _ in
            view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
        }
    }
}
// extension to remove safe area from UIHostingController
//  source: https://stackoverflow.com/a/70339424/6257435
extension UIHostingController {
    convenience public init(rootView: Content, ignoreSafeArea: Bool) {
        self.init(rootView: rootView)
        
        if ignoreSafeArea {
            disableSafeArea()
        }
    }
    
    func disableSafeArea() {
        guard let viewClass = object_getClass(view) else { return }
        
        let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
        if let viewSubclass = NSClassFromString(viewSubclassName) {
            object_setClass(view, viewSubclass)
        }
        else {
            guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
            guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
            
            if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
                let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
                    return .zero
                }
                class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
            }
            
            objc_registerClassPair(viewSubclass)
            object_setClass(view, viewSubclass)
        }
    }
}

struct CaptureView_Previews: PreviewProvider {
    static var previews: some View {
        CaptureView()
    }
}
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Thanks so much DonMag but at least for use case (capturing the image and sharing to Instagram) your code looks the same as mine with the black corners extending under the background's rounded corners. Perhaps this is just not possible currently. – GarySabo Apr 20 '22 at 02:46
  • @GarySabo - hmmm... is it possible Instagram converts the image to jpg format? If you have an image with transparency in your camera roll, and try to share it it Instagram from the Photos app, do you get the same result? – DonMag Apr 20 '22 at 12:09
  • It could be, I don't think I have a transparent image to test. It's strange though if I share you IG Stories I get the black tips on the corners, if I share yo iMessage I get totally rounded corners, If I save to iPhone I get corners of the image but not the black and same with gmail. ‍♂️ It's not the end of the world, just would be a nice to have. – GarySabo Apr 20 '22 at 19:17
  • @GarySabo - curious... I added a Share func -- sharing to Instagram and to Instagram Stories appears to have kept the transparency? That is... no black corners. – DonMag Apr 20 '22 at 20:18
  • I'm not sure what the difference might be – GarySabo Apr 21 '22 at 03:11