개발/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 스냅샷으로 호환.
반응형