【问题标题】:Swift UI View does not render the same in a Image Graphics Context as in the actual viewSwift UI View 在 Image Graphics Context 中的渲染与在实际视图中的渲染不同
【发布时间】:2022-12-17 14:25:58
【问题描述】:

我正在尝试获取 SwiftUI 视图并将其呈现为我的应用程序的图像。目前我正在使用以下代码 https://www.hackingwithswift.com/example-code/media/how-to-render-a-uiview-to-a-uiimage 和一个 UIHostingController,但是我得到了奇怪的结果,正确的视图看起来是正确的,但生成的图像看起来不正确。

这是我看到的图片,

【问题讨论】:

    标签: ios swiftui uikit


    【解决方案1】:

    iOS 16 更新

    注意:iOS 16 现在有 ImageRenderer 见 https://www.hackingwithswift.com/quick-start/swiftui/how-to-convert-a-swiftui-view-to-an-image 。我不知道它是否有相同的问题,但下面的代码应该适用于 16 之前的 iOS 版本。

    原帖

    第一次失败(视图根本不呈现)是因为 drawHierarchy 失败,似乎是因为它没有在 onAppear 之后运行(我认为)。

    第二个是因为 UIHostingController 引入了安全区域插入填充(参见Cannot place SwiftUI view outside the SafeArea when embedded in UIHostingController

    这是带有工作示例的示例图像(请参见下面的代码):

    这是最后一个工作的示例代码:

    import SwiftUI
    
    struct ContentView: View {
        
        // normal view version using Text
        @State
        var textView:  AnyView
        
        // uiImage created before onAppear
        @State
        var imageBeforeOnAppear:     UIImage?
        
        // uiImage displayed using image without safe area insets
        @State
        var imageWithoutSafeArea:     UIImage?
        
        // uiImage displayed using image with safe area insets
        @State
        var imageWithSafeArea: UIImage?
        
        @State
        var show = false
        
        var body: some View{
            VStack{
                if (!show){
                    Button(action: {
                        self.render()
                        self.show.toggle()
                    }, label: {Text("Show")})
                }
                if (show){
                    Text("Normal View")
                    self.textView.border(Color.red, width: 5)
                    Text("Image created in init (drawHierarchy fails)")
                    Image(uiImage: self.imageBeforeOnAppear!).border(Color.red, width: 5)
                    Text("Image created without SafeArea being disabled")
                    Image(uiImage: self.imageWithoutSafeArea!).border(Color.red, width: 5)
                    Text("Image created with SafeArea being disabled")
                    Image(uiImage: self.imageWithSafeArea!).border(Color.red, width: 5)
                }
            }
        }
        
        init(){
            print("Creating content view...")
            
            // generate a nice looking date for our text
            let localDateFormatter = DateFormatter();
            localDateFormatter.dateStyle = DateFormatter.Style.short
            localDateFormatter.timeStyle = DateFormatter.Style.medium
            localDateFormatter.string(from: Date())
            
            let textString = "Generated at (localDateFormatter.string(from: Date()))"
            print(textString)
    
            self.textView = AnyView(Text(textString)
                .foregroundColor(Color.white)
                .padding()
                .background(Color.purple))
            
            let iboa = ContentView.toImage(view: textView, disableSafeArea: true)
            print("Image size (iboa.size)")
            
            // see https://stackoverflow.com/questions/56691630/swiftui-state-var-initialization-issue
            // force this to be initialized in init
            // this will NOT work:
            //     self.imageBeforeOnAppear = iboa
            // it will still be null
            self._imageBeforeOnAppear = State(initialValue: iboa)
        }
        
        // Need to call this after the ContentView is showing!
        // That's why we have a button to delay this (and not
        // run it on init for example) otherwise drawHierarchy
        // will fail if you set afterScreenUpdates to true!
        // (and it won't render anything if you set afterScreenUpdates
        //  to false)
        func render(){
            
            // generate image version with safe area disabled
            self.imageWithSafeArea = ContentView.toImage(view: textView)
            print("Image size (self.imageWithSafeArea!.size)")
            
            
            // generate image version without safe area disabled
            self.imageWithoutSafeArea = ContentView.toImage(view: textView, disableSafeArea: false)
            print("Image size (self.imageWithoutSafeArea!.size)")
            
            
        }
        
        static func toImage(view: AnyView, disableSafeArea: Bool = true) -> UIImage{
            print("Thread info  (Thread.current)")
            print("Thread main? (Thread.current.isMainThread)")
    
            let controller = UIHostingController(rootView:view)
            
            if (disableSafeArea){
                // otherwise there is a buffer at the top of the frame
                controller.disableSafeArea()
            }
            
            controller.view.setNeedsLayout()
            controller.view.layoutIfNeeded()
            
            let targetSize = controller.view.intrinsicContentSize
            print("Image Target Size (targetSize)")
            let rect = CGRect(x: 0,
                              y: 0,
                              width: targetSize.width,
                              height:targetSize.height)
            
            controller.view.bounds = rect
            controller.view.frame  = rect
            // so we at least see something if the SwiftUI view
            // fails to render
            controller.view.backgroundColor = UIColor.green
    
            let renderer = UIGraphicsImageRenderer(size: targetSize)
            
            let image = renderer.image(actions: { rendererContext in
                controller.view.layer.render(in: rendererContext.cgContext)
                let result = controller.view.drawHierarchy(in: controller.view.bounds,
                                                           afterScreenUpdates: true)
    
                print("drawHierarchy successful? (result)")
            })
            
            return image
            
        }
        
       
    }
    
    // see https://stackoverflow.com/questions/70156299/cannot-place-swiftui-view-outside-the-safearea-when-embedded-in-uihostingcontrol
    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 ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2015-06-15
      • 2021-03-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-09-19
      • 1970-01-01
      • 2021-01-01
      相关资源
      最近更新 更多