개발/iOS

화면을 이미지로 전환 (UIView -> UIImage, View -> Image)

까칠코더 2025. 11. 6. 15:19
반응형

화면을 이미지로 전환 (UIView -> UIImage, View -> Image)

 

1) UIKit: UIView를 UIImage로 변환

 

A. 빠르고 간단한 스냅샷 – UIGraphicsImageRenderer + layer.render

  • 장점: 빠름, 메모리 효율 좋음
  • 단점: 일부 효과(반투명·블러·스크롤 뷰 오버레이 등) 누락 가능
import UIKit

extension UIView {
    /// 현재 bounds 크기 그대로 레이어 렌더
    func snapshot() -> UIImage {
        let format = UIGraphicsImageRendererFormat.default()
        format.scale = UIScreen.main.scale   // 0(자동) 도 가능
        format.opaque = isOpaque             // 투명 필요시 false
        return UIGraphicsImageRenderer(bounds: bounds, format: format)
            .image { ctx in
                layer.render(in: ctx.cgContext)
            }
    }
}

 

B. 실제 화면과 최대한 동일한 렌더 – drawHierarchy 기반

  • 장점: UIVisualEffectView(블러), 뷰 계층 효과 반영
  • 단점: 상대적으로 느림
extension UIView {
    func snapshotHierarchy(afterScreenUpdates: Bool = true) -> UIImage {
        let size = bounds.size
        UIGraphicsBeginImageContextWithOptions(size, isOpaque, 0) // scale 0 = device scale
        defer { UIGraphicsEndImageContext() }
        drawHierarchy(in: bounds, afterScreenUpdates: afterScreenUpdates)
        return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
    }
}

Tip: 새로 그려진 컨텐츠를 반드시 포함해야 하면 afterScreenUpdates: true,

애니메이션 중 프레임 성능이 중요하면 false를 고려하세요.

 

C. 스크롤 전체 캡처(콘텐츠 크기 기준)

스크롤뷰(또는 컬렉션/테이블) 전체 높이를 이미지로 만들 때:

extension UIScrollView {
    func snapshotFullContent() -> UIImage {
        let prevOffset = contentOffset
        let prevFrame  = frame

        let contentSize = self.contentSize
        frame = CGRect(origin: .zero, size: contentSize)
        contentOffset = .zero

        let format = UIGraphicsImageRendererFormat.default()
        format.scale = UIScreen.main.scale
        let img = UIGraphicsImageRenderer(size: contentSize, format: format).image { ctx in
            layer.render(in: ctx.cgContext)
        }

        // 상태 복구
        frame = prevFrame
        contentOffset = prevOffset
        return img
    }
}

주의: 매우 긴 콘텐츠는 메모리 사용량이 큽니다. 섹션 단위로 쪼개어 캡처 후 이어붙이는 전략도 고려하세요.

 

D. 비동기/메인 스레드 보장 헬퍼

렌더링은 반드시 메인 스레드에서 실행하세요.

extension UIView {
    func snapshotAsync(usingHierarchy: Bool = false, completion: @escaping (UIImage) -> Void) {
        DispatchQueue.main.async {
            let image = usingHierarchy ? self.snapshotHierarchy() : self.snapshot()
            completion(image)
        }
    }
}

 

2) SwiftUI: View를 UIImage / Image로 변환

 

A. iOS 16+ 권장: ImageRenderer

가장 간단하고 정확한 최신 방식.

import SwiftUI

@available(iOS 16.0, *)
func renderUIImage<V: View>(_ view: V, size: CGSize? = nil, scale: CGFloat = UIScreen.main.scale) -> UIImage? {
    let renderer = ImageRenderer(content: view)
    renderer.scale = scale
    if let size {
        renderer.proposedSize = .init(size) // 고정 크기 필요 시
    }
    return renderer.uiImage
}

// SwiftUI Image로 쓰기
@available(iOS 16.0, *)
func renderSwiftUIImage<V: View>(_ view: V) -> Image? {
    let renderer = ImageRenderer(content: view)
    return renderer.swiftUIImage
}

// 사용 예
struct Badge: View {
    var body: some View {
        ZStack {
            Circle().fill(.blue)
            Text("PRO").font(.headline).foregroundColor(.white)
        }
        .frame(width: 120, height: 120)
        .padding(8)
    }
}

@available(iOS 16.0, *)
func example() {
    if let ui = renderUIImage(Badge(), size: CGSize(width: 136, height: 136)) {
        // ui 사용
    }
}

renderer.proposedSize 를 지정하지 않으면 View의 고유 사이즈에 따라 렌더됩니다.

투명 배경이 필요하면 SwiftUI 뷰에 .background(Color.clear) 를 사용하세요.

 

B. iOS 15 이하 호환: UIHostingController 스냅샷

SwiftUI 뷰를 UIHostingController에 올리고 UIView 스냅샷 기법을 재사용합니다.

import SwiftUI

func snapshotSwiftUIView<V: View>(_ rootView: V, size: CGSize) -> UIImage {
    let hosting = UIHostingController(rootView: rootView)
    hosting.view.bounds = CGRect(origin: .zero, size: size)
    hosting.view.backgroundColor = .clear   // 투명 필요 시

    // 레이아웃 강제
    hosting.view.setNeedsLayout()
    hosting.view.layoutIfNeeded()

    return hosting.view.snapshotHierarchy() // ① 위에서 만든 확장 재사용
    // 혹은 hosting.view.snapshot()      // ② 성능 우선
}

 

C. SwiftUI Image 생성

스냅샷한 UIImage를 SwiftUI에서 쓰려면:

import SwiftUI

let ui: UIImage = /* snapshot 결과 */
let swiftUIImage = Image(uiImage: ui)

 

3) 어떤 방식을 써야 하나? (의사결정 가이드)

 

상황 권장 방식 이유
정확히 현재 화면과 동일한 결과 drawHierarchy / SwiftUI ImageRenderer 효과·블러까지 반영
성능·속도가 더 중요 layer.render 빠르고 메모리 덜 씀
스크롤 전체 캡처 커스텀 렌더(콘텐츠 사이즈) 길이 제한 해소
SwiftUI 전용(iOS 16+) ImageRenderer 간결/정확/공식 권장
iOS 15 이하 호환 UIHostingController + UIView 스냅샷 범용 호환성

 

4) 주의사항 & 팁

  • 메인 스레드에서 렌더링: UIKit/SwiftUI UI 작업은 메인 스레드 필수
  • 투명 배경 필요 시: isOpaque = false, 배경 .clear 적용
  • Retina 스케일: scale = 0(자동) 또는 UIScreen.main.scale 지정
  • Metal/동영상 레이어: AVPlayerLayer, MTKView 등은 캡처가 제한적 → 프레임 이미지는 AVAssetImageGenerator, CIContext 등 다른 API 사용
  • 메모리: 초대형 뷰 캡처는 OOM 위험 → 분할 렌더링/타iled 방식 고려
  • 보안 컨텐츠: 일부 웹/보안 뷰는 캡처가 차단될 수 있음

 

5) 유용한 코드

import SwiftUI

enum Snapshot {
    static func uiImage(from view: UIView, useHierarchy: Bool = false) -> UIImage {
        useHierarchy ? view.snapshotHierarchy() : view.snapshot()
    }

    @available(iOS 16.0, *)
    static func uiImage<V: View>(from view: V, size: CGSize? = nil) -> UIImage? {
        renderUIImage(view, size: size)
    }

    static func uiImageLegacy<V: View>(from view: V, size: CGSize) -> UIImage {
        snapshotSwiftUIView(view, size: size)
    }

    static func swiftUIImage(from uiImage: UIImage) -> Image {
        Image(uiImage: uiImage)
    }
}

 

결론

  • UIKit: layer.render(빠름) vs drawHierarchy(정확) 를 상황에 맞게 선택.
  • SwiftUI: iOS 16+는 ImageRenderer가 최선, 그 이하 버전은 HostingController + UIView 스냅샷으로 호환.
반응형