1

My goal is to convert SwiftUI view to image. I am currently building it for iOS 15 here is the code that converts the view to image

extension View {
    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)
        }
    }
}

Expected result: https://share.cleanshot.com/5Pvzb7

Actual result: https://share.cleanshot.com/O4GKUF

Below is the body of the view that has a button and image view

var body: some View {
        VStack {
            
            imageView
            
            Spacer(minLength: 200)
            
            Button {
                UIImageWriteToSavedPhotosAlbum(imageView.snapshot(), nil, nil, nil)
                
            } label: {
                Label("Save to Photos", systemImage: "photo")
            }
        }
    }

Imageview is hardcoded with data and image is loaded from the internet

    var imageView: some View {
        VStack {
            ZStack(alignment: .top) {
                
                Color.red
                
                VStack {
                    VStack {
                        HStack(alignment: .top) {
                            Rectangle.init().frame(width: 56, height: 56).cornerRadius(28)
                            
                            VStack {
                                HStack {
                                    Text.init("Yura Filin").font(.system(size: 16))
                                    Spacer()
                                }.padding(.bottom, 1)
                                
                                HStack {
                                    Text.init("qweqwewqeqqweqeasd asd asd aosidhsa doaid adoaid adiad hiu uh i hiu ih uhuih iuhiu ih asdi ").lineLimit(90).multilineTextAlignment(.leading).font(.system(size: 16))
                                    Spacer()
                                }.padding(.bottom, 2)
                                
                                HStack {
                                    Image(uiImage: image)
                                        .resizable()
                                        .aspectRatio(contentMode: .fit)
                                        .frame(width: (286 - 64 - 12))
                                        .onReceive(imageLoader.didChange) { data in
                                            self.image = UIImage(data: data) ?? UIImage() }.cornerRadius(14)
                                    Spacer()
                                }
                                Spacer()
                            }.padding(.leading, 12).readSize { size in
                                print(size)
                            }
                        }
                    }.padding(16).background(.green).cornerRadius(12)
                }.padding(64)
            }
        }.ignoresSafeArea()
    }

Any help is appreciated. Thanks!

user4150758
  • 384
  • 4
  • 17

2 Answers2

1

Here is the code as shown below which solves your problem and in order to call UIImageWriteToSavedPhotosAlbum() you must add the NSPhotoLibraryAddUsageDescription key to your Info.plist

import SwiftUI

struct ContentView: View {
    var imageView: some View {
        VStack {
            ZStack(alignment: .top) {
                
                Color.red
                
                VStack {
                    VStack {
                        HStack(alignment: .top) {
                            Rectangle.init().frame(width: 56, height: 56).cornerRadius(28)
                            
                            VStack {
                                HStack {
                                    Text.init("Yura Filin").font(.system(size: 16))
                                    Spacer()
                                }.padding(.bottom, 1)
                                
                                HStack {
                                    Text.init("qweqwewqeqqweqeasd asd asd aosidhsa doaid adoaid adiad hiu uh i hiu ih uhuih iuhiu ih asdi ").lineLimit(90).multilineTextAlignment(.leading).font(.system(size: 16))
                                    Spacer()
                                }.padding(.bottom, 2)
                                
                                HStack {
                                    Image("image")
                                        .resizable()
                                        .aspectRatio(contentMode: .fit)
                                        .frame(width: (286 - 64 - 12))
                                        .cornerRadius(14)
                                    Spacer()
                                }
                                Spacer()
                            }.padding(.leading, 12)
                        }
                    }.padding(16).background(.green).cornerRadius(12)
                }.padding(64)
            }
        }.ignoresSafeArea()
    }
    
    var body: some View {
        VStack {
            Button("Save to Photos") {
                let image = imageView.snapshot()
                UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
            }
            imageView
            
            Spacer(minLength: 200)
            
            Button("Save to Photos") {
                let image = imageView.snapshot()
                UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
            }
        }
    }
    
}


extension View {
    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)
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Output :

enter image description here

Saurabh Pathak
  • 879
  • 7
  • 10
0

Unlike when a usual view is rendered and the sizing of that view is constrained by the device, that doesn't happen here. So you need to apply a frame with defined width (and possibly height) to the view you're capturing.

When I needed to do this, I created another view that isn't rendered to the user and used it solely for rendering the image. That way, any sizing you have to define in the frame doesn't mess up what the user sees.

It might take you a while to get it right, but try adding a frame modifier with desired width to the view you're rendering.

  • I see what you mean, do you have a code example where I can see how it works and where the modifier should be applied? Thank you. – user4150758 Sep 08 '22 at 13:41
  • Sure! So, you're capturing ImageView, try putting this frame modifier on the parent VStack (the one that contains all the content). `.frame(width: 400)` - You can play around with width and height till you get the proportions you want. I've gotten away with just setting the width in my app. Let me know how it goes! – James William Sep 09 '22 at 10:27
  • Did you have any luck with this @user4150758? – James William Sep 14 '22 at 14:06
  • unfortunately no :( – user4150758 Sep 16 '22 at 09:04