如何将图像转换为UIImage?

37

我该如何将SwiftUI中的Image转换为UIImage

let image = Image(systemName: "circle.fill")
let UIImage = image as UIImage

1
嗨 @nOK,你为什么需要那个?你想要实现什么? - Giuseppe Sapienza
UIImageUIKit 的一部分,而不是 SwiftUI。你到底想做什么?没有 SwiftUI,你可以很容易地通过非常详细的文档在UIImageCGImageCIImage之间进行转换。如果你认为 Image 更像是一个 UIImageView,那么你真正寻找的是什么呢? - user7014451
1
我试图在CoreData中保存一张图片,使用UIImage时,我可以轻松地使用image.pngData(),但是在SwiftUI中还没有找到这样的可能性。 - nOk
同时,类型为“Image”的变量似乎不符合“Hashable”原型。 - nOk
6个回答

36

没有直接将Image转换为UIImage的方法。相反,您应该将Image视为View,并尝试将该View转换为UIImage。 Image符合View,因此我们已经拥有所需的View。现在我们只需要将该View转换为UIImage。

我们需要两个组件来实现这一点。首先,一个将我们的Image/View更改为UIView的函数,其次是将我们创建的UIView更改为UIImage的函数。

为了更方便,这两个函数都声明为它们相应类型的扩展。

extension View {
// This function changes our View to UIView, then calls another function
// to convert the newly-made UIView to a UIImage.
    public func asUIImage() -> UIImage {
        let controller = UIHostingController(rootView: self)
        
 // Set the background to be transparent incase the image is a PNG, WebP or (Static) GIF
        controller.view.backgroundColor = .clear 
        
        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()
        
// here is the call to the function that converts UIView to UIImage: `.asUIImage()`
        let image = controller.view.asUIImage()
        controller.view.removeFromSuperview()
        return image
    }
}

extension UIView {
// This is the function to convert UIView to UIImage
    public func asUIImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: bounds)
        return renderer.image { rendererContext in
            layer.render(in: rendererContext.cgContext)
        }
    }
}

如何使用?

let image: Image = Image("MyImageName") // Create an Image anyhow you want
let uiImage: UIImage = image.asUIImage() // Works Perfectly

奖励

正如我所说,我们将图像视为视图。在此过程中,我们不使用任何特定的图像功能,唯一重要的是我们的图像是一个视图(符合视图协议)。 这意味着使用此方法,您不仅可以将图像转换为UIImage,还可以将任何视图转换为UIImage。

var myView: some View {
// create the view here
}
let uiImage = myView.asUIImage() // Works Perfectly

1
我该如何在iOS 15中更改行UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)?我收到了弃用警告:“'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead" - Rillieux
@Rillieux - 就像这样。当然,你也可以加上保护等等。但是简单来说:let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene scene?.windows.first?.rootViewController?.view.addSubview(controller.view) - C6Silver

27

这种事在SwiftUI中是不可能的,我敢打赌它将永远不会实现。这与整个框架概念相违背。然而,你可以朝相反的方向发展:

let uiImage = UIImage(systemName: "circle.fill")

let image = Image(uiImage: uiImage)

类型为“Image”的变量似乎不符合“Hashable”原型,所以我希望能够进行转换 :/ - nOk
7
如果你想从一个“Image”视图获取数据,你不能直接从“Image”本身获取,而是要从“Image”获取数据的相同位置获取。例如,如果你使用uiImage对象加载图像,你应该去找这个uiImage对象。如果视图从文件名获取了图像,你应该去从文件中获取它。你永远无法查询“Image”的二进制数据,因为这不是框架设计的方式。 - kontiki
谢谢您的帮助。我本来想避免这种情况,但是还是觉得可能要重写代码:D那我们就开始重写吧:D - nOk
3
@kontiki 为什么这与整个框架概念相违背? - villb

6
尝试在视图中添加扩展,像这样:

extension Image {
    @MainActor
    func getUIImage(newSize: CGSize) -> UIImage? {
        let image = resizable()
            .scaledToFill()
            .frame(width: newSize.width, height: newSize.height)
            .clipped()
        return ImageRenderer(content: image).uiImage
    }
}

你的使用案例将是什么?
    let image = Image("someFromAssets")
    let size = CGSize(width: 100, height: 100)
    let uiImage = image.getUIImage(newSize: size)


1
这应该是被接受的答案。 - undefined

4
截至iOS 16和MacOS 13,上面大部分的答案已经过时了。现在的答案是使用ImageRenderer,它被设计用于将视图(包括图像)转换为UIImage和CGImage。
主要的注意事项是ImageRenderer希望在主线程中运行,所以你需要使用GCD来进行图像转换。
public func convert(image: Image, callback: @escaping ((UIImage?) -> Void)) {
    DispatchQueue.main.async {
        let renderer = ImageRenderer(content: image)

        // to adjust the size, you can use this (or set a frame to get precise output size)
        // renderer.scale = 0.25
        
        // for CGImage use renderer.cgImage
        callback(renderer.uiImage)
    }
}

2
iOS 16
extension View {
    /// Usually you would pass  `@Environment(\.displayScale) var displayScale`
    @MainActor func render(scale displayScale: CGFloat = 1.0) -> UIImage? {
        let renderer = ImageRenderer(content: self)

        renderer.scale = displayScale
        
        return renderer.uiImage
    }
    
}

注意事项:
1. 使用@MainActor操作ImageRenderer。 2. 传递正确的.displayScale。 3. 如果尝试渲染当前屏幕上的视图(请参阅注释中的注意事项),将会导致错误。
iOS 15及以下版本。
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 RenderView: View {
    let text: String
    
    var body: some View {
        Text(text)
            .font(.largeTitle)
            .foregroundStyle(.white)
            .padding()
            .background(.blue)
            .clipShape(Capsule())
    }
}

struct ContentView: View {
    @State private var text = "Your text here"
    @State private var uiImage = UIImage(systemName: "photo")
    @Environment(\.displayScale) private var displayScale
    
    var body: some View {
        let renderedImage = Image(uiImage: uiImage!)
        let textField = TextField("Enter some text", text: $text)
            .textFieldStyle(.roundedBorder)
            .padding()
            .background(.blue)
            .clipShape(Capsule())
        
        VStack {
            renderedImage
            
            ShareLink("Export", item: renderedImage, preview: SharePreview(Text("Shared image"), image: renderedImage))

            textField

        }
        .onChange(of: text) { _ in
            uiImage = RenderView(text: text).render(scale: displayScale)!
            // Note that the following will not work for some reason
            // uiImage = textField.render(scale: displayScale)!
            
            // This kind of works, but rendered size differs from original
            // uiImage = textField.snapshot()
        }
    }

}

感谢Paul Hudson提供的贡献。

0

我一直在使用一个包装器来实现这个解决方法

struct AppImage: View {
    private let name: String

    var body: some View {
        Image(name)
    }

    var asUIImage: UIImage? {
        UIImage(named: name)
    }
}

如果你想从互联网上下载一张图片,你可以使用Kingfisher来实现相同的输出。
import UIKit
import Kingfisher

struct AppImage: View {
    private let urlString: String

    var body: some View {
        KFImage(URL(string: urlString))
    }

    var asUIImage: UIImage {
        let imageView = UIImageView()
        imageView.kf.setImage(with: URL(string: urlString))
        return imageView.image ?? UIImage(named: "placeholder-image")!
    }
}

稍后,如果您在初始化图像时坚持使用AppImage类型,则可以在应用程序中使用任何图像作为UIImage

AppImage(name: "image-name").asUIImage

或者 AppImage(urlString: "https://url-to-your-image.com").asUIImage


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接