如何在 ViewController 中将 SwiftUI View body 转换为 UIImage
How to convert SwiftUI View body to UIImage in ViewController
我正在处理这个转换并尝试了很多解决方案(扩展和方法),因为有很多与此相关的问题和答案,但没有任何帮助,就像我尝试了以下解决方案但没有帮助
尝试过的解决方案
以下摘自
extension View {
func asImage() -> UIImage {
let controller = UIHostingController(rootView: self)
// locate far out of screen
controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.sizeToFit()
let image = controller.view.asImage()
controller.view.removeFromSuperview()
return image
}
}
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
// [!!] Uncomment to clip resulting image
// rendererContext.cgContext.addPath(
// UIBezierPath(roundedRect: bounds, cornerRadius: 20).cgPath)
// rendererContext.cgContext.clip()
// As commented by @MaxIsom below in some cases might be needed
// to make this asynchronously, so uncomment below DispatchQueue
// if you'd same met crash
// DispatchQueue.main.async {
layer.render(in: rendererContext.cgContext)
// }
}
}
}
这个解决方案有帮助,但我不想将图像添加为 superView 的子视图
func extractView(){
let hostView = UIHostingController(rootView: ContentView())
hostView.view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
hostView.view.topAnchor.constraint(equalTo: view.topAnchor),
hostView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hostView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostView.view.widthAnchor.constraint(equalTo: view.widthAnchor),
hostView.view.heightAnchor.constraint(equalTo: view.heightAnchor),
]
self.view.addSubview(hostView.view)
self.view.addConstraints(constraints)
}
我想做什么???
我有一个扩展 swiftUI View 的结构,我在其中有一个设计。现在我想将 swiftUI View 转换为故事板 ViewController 内的 UIImage,当我的屏幕加载和 viewDidLoad()
函数调用时,系统会更新故事板
中 UIImageView 的图像
这是我的 SwiftUI 代码
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack(alignment: .center){
Rectangle()
.frame(width: 200, height: 75)
.cornerRadius(10)
.foregroundColor(.white)
Circle()
.stroke(lineWidth:5)
.foregroundColor(.red)
.frame(width: 75, height: 75, alignment: .leading)
.background(
Image("tempimage")
.resizable()
)
}
}
}
您 可以 这样做...但不能在 viewDidLoad()
-- 您必须至少等到 viewDidLayoutSubviews()
.
而且,必须将视图添加到视图层次结构中——但可以在我们生成图像后立即将其删除,因此它永远不会出现“on-screen”。“
注意:此处所有“结果”图片均使用:
- 一个
240 x 200
图像视图
.contentMode = .center
- 绿色背景,以便我们可以看到框架
我们给 UIImage
从 SwiftUI ContentView
生成黄色背景,因为我们需要解决一些布局问题。
因此,要生成图像并将其设置为 UIImageView
,我们可以这样做:
// we will generate the image in viewDidLayoutSubview()
// but that can be (and usually is) called more than once
// so we'll use this to make sure we only generate the image once
var firstTime: Bool = true
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// we only want this to run once
if firstTime {
firstTime = false
if let img = imageFromContentView() {
imgView.image = img
}
}
}
使用这个 imageFromContentView()
函数:
func imageFromContentView() -> UIImage? {
let swiftUIView = UIHostingController(rootView: ContentView())
// add as chlld controller
addChild(swiftUIView)
// make sure we can get its view (safely unwrap its view)
guard let v = swiftUIView.view else {
swiftUIView.willMove(toParent: nil)
swiftUIView.removeFromParent()
return nil
}
view.addSubview(v)
swiftUIView.didMove(toParent: self)
// size the view to its content
v.sizeToFit()
// force it to layout its subviews
v.setNeedsLayout()
v.layoutIfNeeded()
// if we want to see the background
v.backgroundColor = .systemYellow
// get it as a UIImage
let img = v.asImage()
// we're done with it, so get rid of it
v.removeFromSuperview()
swiftUIView.willMove(toParent: nil)
swiftUIView.removeFromParent()
return img
}
结果 #1:
请注意顶部的 20 磅黄带,内容未垂直居中...这是因为 UIHostingController
应用了安全区域布局指南。
解决这个问题的几个选项...
如果我们添加这一行:
view.addSubview(v)
swiftUIView.didMove(toParent: self)
// add same bottom safe area inset as top
swiftUIView.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: v.safeAreaInsets.top, right: 0)
// size the view to its content
v.sizeToFit()
我们得到这个结果:
渲染图像现在有 20 磅的顶部和底部“安全区域”插图。
如果我们不想任何安全区域插入,我们可以使用这个扩展:
// extension to remove safe area from UIHostingController
// source:
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)
}
}
}
并将函数的第一行更改为:
let swiftUIView = UIHostingController(rootView: ContentView(), ignoreSafeArea: true)
我们得到这个结果:
因为 SwiftUI ContentView
布局使用 zStack
其内容(“环”)超出其垂直边界,所以环的顶部和底部被“截断”。
我们可以通过更改 ContentView
中的框架来解决此问题:
或者增加加载视图的框架高度,例如:
// size the view to its content
v.sizeToFit()
// for this explicit example, the "ring" extends vertically
// outside the bounds of the zStack
// so we'll add 10-pts height
v.frame.size.height += 10.0
这是一个完整的实现(使用您未修改的 ContentView
):
class ViewController: UIViewController {
let imgView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
imgView.contentMode = .center
imgView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imgView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// let's put the imageView 40-pts from Top
imgView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
// centered horizontally
imgView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// width: 240
imgView.widthAnchor.constraint(equalToConstant: 240.0),
// height: 200
imgView.heightAnchor.constraint(equalToConstant: 200.0),
])
// show the image view background so we
// can see its frame
imgView.backgroundColor = .systemGreen
}
// we will generate the image in viewDidLayoutSubview()
// but that can be (and usually is) called more than once
// so we'll use this to make sure we only generate the image once
var firstTime: Bool = true
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// we only want this to run once
if firstTime {
firstTime = false
if let img = imageFromContentView() {
imgView.image = img
}
}
}
func imageFromContentView() -> UIImage? {
let swiftUIView = UIHostingController(rootView: ContentView(), ignoreSafeArea: true)
// add as chlld controller
addChild(swiftUIView)
// make sure we can get its view (safely unwrap its view)
guard let v = swiftUIView.view else {
swiftUIView.willMove(toParent: nil)
swiftUIView.removeFromParent()
return nil
}
view.addSubview(v)
swiftUIView.didMove(toParent: self)
// size the view to its content
v.sizeToFit()
// for this explicit example, the "ring" extends vertically
// outside the bounds of the zStack
// so we'll add 10-pts height
v.frame.size.height += 10.0
// force it to layout its subviews
v.setNeedsLayout()
v.layoutIfNeeded()
// if we want to see the background
v.backgroundColor = .systemYellow
// get it as a UIImage
let img = v.asImage()
// we're done with it, so get rid of it
v.removeFromSuperview()
swiftUIView.willMove(toParent: nil)
swiftUIView.removeFromParent()
return img
}
}
// extension to remove safe area from UIHostingController
// source:
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)
}
}
}
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: frame.size)
return renderer.image { context in
layer.render(in: context.cgContext)
}
}
}
我正在处理这个转换并尝试了很多解决方案(扩展和方法),因为有很多与此相关的问题和答案,但没有任何帮助,就像我尝试了以下解决方案但没有帮助
尝试过的解决方案
以下摘自
extension View {
func asImage() -> UIImage {
let controller = UIHostingController(rootView: self)
// locate far out of screen
controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.sizeToFit()
let image = controller.view.asImage()
controller.view.removeFromSuperview()
return image
}
}
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
// [!!] Uncomment to clip resulting image
// rendererContext.cgContext.addPath(
// UIBezierPath(roundedRect: bounds, cornerRadius: 20).cgPath)
// rendererContext.cgContext.clip()
// As commented by @MaxIsom below in some cases might be needed
// to make this asynchronously, so uncomment below DispatchQueue
// if you'd same met crash
// DispatchQueue.main.async {
layer.render(in: rendererContext.cgContext)
// }
}
}
}
这个解决方案有帮助,但我不想将图像添加为 superView 的子视图
func extractView(){
let hostView = UIHostingController(rootView: ContentView())
hostView.view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
hostView.view.topAnchor.constraint(equalTo: view.topAnchor),
hostView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hostView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostView.view.widthAnchor.constraint(equalTo: view.widthAnchor),
hostView.view.heightAnchor.constraint(equalTo: view.heightAnchor),
]
self.view.addSubview(hostView.view)
self.view.addConstraints(constraints)
}
我想做什么???
我有一个扩展 swiftUI View 的结构,我在其中有一个设计。现在我想将 swiftUI View 转换为故事板 ViewController 内的 UIImage,当我的屏幕加载和 viewDidLoad()
函数调用时,系统会更新故事板
这是我的 SwiftUI 代码
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack(alignment: .center){
Rectangle()
.frame(width: 200, height: 75)
.cornerRadius(10)
.foregroundColor(.white)
Circle()
.stroke(lineWidth:5)
.foregroundColor(.red)
.frame(width: 75, height: 75, alignment: .leading)
.background(
Image("tempimage")
.resizable()
)
}
}
}
您 可以 这样做...但不能在 viewDidLoad()
-- 您必须至少等到 viewDidLayoutSubviews()
.
而且,必须将视图添加到视图层次结构中——但可以在我们生成图像后立即将其删除,因此它永远不会出现“on-screen”。“
注意:此处所有“结果”图片均使用:
- 一个
240 x 200
图像视图 .contentMode = .center
- 绿色背景,以便我们可以看到框架
我们给 UIImage
从 SwiftUI ContentView
生成黄色背景,因为我们需要解决一些布局问题。
因此,要生成图像并将其设置为 UIImageView
,我们可以这样做:
// we will generate the image in viewDidLayoutSubview()
// but that can be (and usually is) called more than once
// so we'll use this to make sure we only generate the image once
var firstTime: Bool = true
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// we only want this to run once
if firstTime {
firstTime = false
if let img = imageFromContentView() {
imgView.image = img
}
}
}
使用这个 imageFromContentView()
函数:
func imageFromContentView() -> UIImage? {
let swiftUIView = UIHostingController(rootView: ContentView())
// add as chlld controller
addChild(swiftUIView)
// make sure we can get its view (safely unwrap its view)
guard let v = swiftUIView.view else {
swiftUIView.willMove(toParent: nil)
swiftUIView.removeFromParent()
return nil
}
view.addSubview(v)
swiftUIView.didMove(toParent: self)
// size the view to its content
v.sizeToFit()
// force it to layout its subviews
v.setNeedsLayout()
v.layoutIfNeeded()
// if we want to see the background
v.backgroundColor = .systemYellow
// get it as a UIImage
let img = v.asImage()
// we're done with it, so get rid of it
v.removeFromSuperview()
swiftUIView.willMove(toParent: nil)
swiftUIView.removeFromParent()
return img
}
结果 #1:
请注意顶部的 20 磅黄带,内容未垂直居中...这是因为 UIHostingController
应用了安全区域布局指南。
解决这个问题的几个选项...
如果我们添加这一行:
view.addSubview(v)
swiftUIView.didMove(toParent: self)
// add same bottom safe area inset as top
swiftUIView.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: v.safeAreaInsets.top, right: 0)
// size the view to its content
v.sizeToFit()
我们得到这个结果:
渲染图像现在有 20 磅的顶部和底部“安全区域”插图。
如果我们不想任何安全区域插入,我们可以使用这个扩展:
// extension to remove safe area from UIHostingController
// source:
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)
}
}
}
并将函数的第一行更改为:
let swiftUIView = UIHostingController(rootView: ContentView(), ignoreSafeArea: true)
我们得到这个结果:
因为 SwiftUI ContentView
布局使用 zStack
其内容(“环”)超出其垂直边界,所以环的顶部和底部被“截断”。
我们可以通过更改 ContentView
中的框架来解决此问题:
或者增加加载视图的框架高度,例如:
// size the view to its content
v.sizeToFit()
// for this explicit example, the "ring" extends vertically
// outside the bounds of the zStack
// so we'll add 10-pts height
v.frame.size.height += 10.0
这是一个完整的实现(使用您未修改的 ContentView
):
class ViewController: UIViewController {
let imgView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
imgView.contentMode = .center
imgView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imgView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// let's put the imageView 40-pts from Top
imgView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
// centered horizontally
imgView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// width: 240
imgView.widthAnchor.constraint(equalToConstant: 240.0),
// height: 200
imgView.heightAnchor.constraint(equalToConstant: 200.0),
])
// show the image view background so we
// can see its frame
imgView.backgroundColor = .systemGreen
}
// we will generate the image in viewDidLayoutSubview()
// but that can be (and usually is) called more than once
// so we'll use this to make sure we only generate the image once
var firstTime: Bool = true
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// we only want this to run once
if firstTime {
firstTime = false
if let img = imageFromContentView() {
imgView.image = img
}
}
}
func imageFromContentView() -> UIImage? {
let swiftUIView = UIHostingController(rootView: ContentView(), ignoreSafeArea: true)
// add as chlld controller
addChild(swiftUIView)
// make sure we can get its view (safely unwrap its view)
guard let v = swiftUIView.view else {
swiftUIView.willMove(toParent: nil)
swiftUIView.removeFromParent()
return nil
}
view.addSubview(v)
swiftUIView.didMove(toParent: self)
// size the view to its content
v.sizeToFit()
// for this explicit example, the "ring" extends vertically
// outside the bounds of the zStack
// so we'll add 10-pts height
v.frame.size.height += 10.0
// force it to layout its subviews
v.setNeedsLayout()
v.layoutIfNeeded()
// if we want to see the background
v.backgroundColor = .systemYellow
// get it as a UIImage
let img = v.asImage()
// we're done with it, so get rid of it
v.removeFromSuperview()
swiftUIView.willMove(toParent: nil)
swiftUIView.removeFromParent()
return img
}
}
// extension to remove safe area from UIHostingController
// source:
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)
}
}
}
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: frame.size)
return renderer.image { context in
layer.render(in: context.cgContext)
}
}
}