【发布时间】:2022-12-17 14:25:58
【问题描述】:
我正在尝试获取 SwiftUI 视图并将其呈现为我的应用程序的图像。目前我正在使用以下代码 https://www.hackingwithswift.com/example-code/media/how-to-render-a-uiview-to-a-uiimage 和一个 UIHostingController,但是我得到了奇怪的结果,正确的视图看起来是正确的,但生成的图像看起来不正确。
这是我看到的图片,
【问题讨论】:
我正在尝试获取 SwiftUI 视图并将其呈现为我的应用程序的图像。目前我正在使用以下代码 https://www.hackingwithswift.com/example-code/media/how-to-render-a-uiview-to-a-uiimage 和一个 UIHostingController,但是我得到了奇怪的结果,正确的视图看起来是正确的,但生成的图像看起来不正确。
这是我看到的图片,
【问题讨论】:
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()
}
}
【讨论】: