공유 기능 구현 시 발생한 문제 정리
- SwiftUI
- UIKit
- Swift
- Metal
1. 배경 및 목적
요구사항 정의하기
사용자가 촬영한 사진에 프레임을 합성한 최종 결과물을 다른 기기로 공유(Airdrop, 카카오톡 등)하는 기능을 추가하고자 했다.
📌 기존 구조
PhotoComposer가 SwiftUI View를 렌더링하여 최종 이미지를 생성한다.ResultView에서 사용자 저장 및 공유 액션을 제공한다. (하지만 현재는 숨겨있는 상태이다.)
2. 기술 선택: ShareLink vs UIActivityViewController
이와 관련된 학습 정리는 아래 링크를 참고해주세요.
공유 기능 구현을 위해 Apple에서 제공하는 두 가지 주요 방법을 비교했다.
ShareLink(SwiftUI 네이티브 API)UIActivityViewController(UIKit 기반 공유 시트)
비교 분석
| 항목 | UIActivityViewController | ShareLink |
|---|---|---|
| 프레임워크 | UIKit | SwiftUI |
| 코드 복잡도 | 래퍼 필요 | 선언적 |
| 최소 OS | iOS 6+ | iOS 16+ |
| 타입 안정성 | Any | Transferable |
| SwiftUI 통합 | 상당히 코드가 늘어난다. | 오직 네이티브 |
2-1. 1차 선택: ShareLink 기반 구현 시도
초기 구현 단계에서는 SwiftUI 환경에 자연스럽게 녹아드는 ShareLink를 우선적으로 선택했다.
ShareLink(item: photoItem) {
Label("공유하기", systemImage: "paperplane")
}
선택 이유
- 위 예시 코드와 같이 SwiftUI 네이티브 API 스타일이다.
- Transferable 기반이기에 타입 안정성도 잡을 수 있다.
- UIActivityViewController 브릿징이 필요 없다! ← 이게 제일 큰 이유이다.
2-2. 실제 구현 이후 문제 발생
하지만 실제로 ImageRenderer 기반으로 동적 생성된 이미지를 ShareLink로 전달하는 과정에서
Metal 관련 크래시가 지속적으로 발생했다.
(자세한 내용은 하단 5. 해결시도 항목을 확인해주세요.)
결과적으로 문제는 특정 이미지 포맷이나 렌더링 방식이 아니라
ShareLink + Metal 기반 동적 이미지 생성 조합에서 발생하는 디버그 레이어에서 일어나는 충돌이었다..
2-3. 최종 선택: UIActivityViewController 채택
Metal API Validation 이슈를 해결하고 나서 기능 자체는 정상 동작하는 것을 확인했지만..
ShareLink는 내부 처리 흐름을 제어할 수 없으며 Preview 커스터마이징 또한 제한적임과 동시에 Activity 필터링에도 한계가 존재한다.
또한, 갤러리 저장 기능이 별도로 존재하고 프리뷰 등을 고려했을 때 보다 세밀한 제어가 가능한 UIActivityViewController 구조가 더 적합하다고 판단했다.
3. Transferable 래퍼 설계
해당 섹션은 _
ShareLink_에 대한 설명입니다.
ShareLink 에 원하는 아이템을 추가하기 위해선 선행작업이 필요하다.
UIImage는 기본적으로 Transferable을 준수하지 않는다.
따라서 공유 전용 래퍼를 아래와 같이 설계했다.
import SwiftUI
import UniformTypeIdentifiers
struct PhotoShareItem: Transferable {
let image: UIImage
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(exportedContentType: .jpeg) { item in
guard let data = item.image.jpegData(compressionQuality: 0.9) else {
throw TransferError.exportFailed
}
return data
}
.suggestedFileName("mirroringbooth_\\(Date().timeIntervalSince1970).jpg")
}
enum TransferError: Error {
case exportFailed
}
}
4. 발생한 문제 - Metal 텍스처 크래시
writeImageAtIndex:1094: ERROR: trying to save opaque image with AlphaPremulLast
-[MTLDebugBuffer newTextureWithDescriptor:offset:bytesPerRow:]:321:
failed assertion `resourceOptions must match backing buffer resource options`
error fetching item for URL:file:///.../mirroringbooth_xxx.jpg
위 로그에 출력되는 메세지를 해석해보면 아래와 같다.
- Metal backing buffer → resourceOptions =
0x10 - ShareLink 내부 texture descriptor → resourceOptions =
0x0
즉, resourceOptions 메모리의 불일치로 assertion 에러가 발생하고 있었다.
또한 로그 메세지엔 AlphaPremulLast 문구도 출력되고 있는데.. 예상하는 문제는 아래와 같았다.
- 불투명 이미지인데 AlphaPremulLast 유지해서 발생한 것인가?
- Metal에서 JPEG나 PNG 변환 중 충돌이 발생한 것인가?
그것도 아니라면..
ShareLink 내부 임시 파일 생성 파이프라인에서 크래시가 터진건가? 원인을 도저히 알 수 없었다.
따라서 아래와 같이 다양한 시도를 해보았다.
5. 해결 시도
5-1. ShareLink + ImageRenderer가 아닌 UIGraphicsImageRenderer로 변경 ❌
let format = UIGraphicsImageRendererFormat()
format.opaque = true
format.preferredRange = .standard
let renderer = UIGraphicsImageRenderer(size: item.image.size, format: format)
let cleanImage = renderer.image { _ in
item.image.draw(at: .zero)
}
기존 PhotoComposer는 ImageRenderer 기반으로 처리를 진행하고 있다.
현재 크래시 문구는 Metal 오류라고 판단되어 가장 먼저 UIGraphicsImageRenderer 방식으로 변경해보았다.
다만.. 위 방식은 아래와 같은 문제점을 내포하고 있다.
- 이미 완성된 UIImage를 다시 렌더링하는 효율적이지 못한 구조이다.
- 메모리 사용량이 2배 정도 증가한다.
- 제일 큰 문제가.. 공유 시마다 CPU 렌더링이 발생된다.
그럼에도 불구하고 위 방식을 사용했지만.. 근본적으로 해결은 못했다 ㅠ
5-2. ShareLink + FileRepresentation 기반 임시 파일 공유 ❌
FileRepresentation(contentType: .jpeg) { item in
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("jpg")
try data.write(to: url)
return SentTransferredFile(url)
}
Metal에서 발생한 크래시라면 Metal 파이프라인을 타지 않도록 임시 파일을 공유하는 방법으로 우회하는 방법을 사용해보았지만.. 이 또한 크래시가 터지면서 실패했다.
5-3. ShareLink + UIHostingController + CoreGraphics 직접 렌더링 ❌
let hostingController = UIHostingController(rootView: preview)
hostingController.view.frame = CGRect(origin: .zero, size: targetSize)
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { context in
hostingController.view.layer.render(in: context.cgContext)
}
또 다른 방법으로 UIHostingController 를 사용해서 CoreGraphics를 직접 렌더링 하는 방법을 사용해보았다.
위 방식을 사용한다면 Metal 파이프라인을 우회하기 때문에 크래시가 터지지 않을 것으로 기대했지만..
여전히 동일한 크래시는 터지고 있었다.
5-4. ShareLink + PNG ❌
guard let pngData = image.pngData(),
let cleanImage = UIImage(data: pngData) else { return nil }
현재 크래시 로그에 AlphaPremulLast 이 출력된 것으로 보아 알파값 세팅 문제라고 판단하여
열심히 구글링을 진행한 결과 ShareLink의 item 에 들어가는 UIImage를 png으로 무손실 압축을 진행하여 넘기면 해결된다는 것을 확인하여 진행해보았다.
하지만.. 위 방식을 진행했을 때 png로 인코딩은 되지만 ShareLink 단계에서 또 다시 동일한 크래시가 터지고 있는 것을 보아.. ShareLink 자체의 문제라고 판단이 들었다.
5-5. ShareLink + normalizedForSharing로 재비트맵화 ❌
extension UIImage {
func normalizedForSharing() -> UIImage {
UIGraphicsImageRenderer(size: size).image { _ in
draw(in: CGRect(origin: .zero, size: size))
}
}
}
현재까지 단계를 거쳤지만 도저히 해결이 되질않아 AI의 도움을 받아 계속 디버깅을 수행했다.
그러다가 위 코드와 같이 normalizedForSharing 방식을 추천해주었는데,
이는 기존 UIImage에서 기타 설정한 렌더링 옵션 또는 상태를 깡그리 무시하고 새로운 이미지 컨텍스트 에서 이미지를 원점부터 원본 크기만큼 다시 그리는 방식이다.
위 방식을 통해 다양한 소스에서 생성된 UIImage의 일관성을 확보할 수 있고 공유 호환성 또한 충분히 잡을 수 있을 것이라고 생각해서 적용해보았다.
그 결과 Metal 옵션 불일치 경고는 사라졌고, UIImage 또한 성공적으로 생성되며 PNG 나 JPEG로 정상적으로 변환된 것을 확인했지만……
여전히 ShareLink 내부에서 똑같은 Metal 크래시가 터지고 있었다. (도대체 뭐가 문제지?)
5-6. 중간 결론 - ShareLink 포기 👋
지금까지 다양한 방법을 시도해보며 디버깅을 진행한 결과 얻은 결론은 아래와 같다.
- 이미지 생성 파이프라인 자체는 정상이다.
→ 실제로 Metal 경고를 제거했으며 PNG이나 JPEG 변환은 모두 성공했다.
→ Transferable 기반 이미지 export도 성공했다.
결국은 현재 프로젝트에서 사용중인 PhotoComposer와 Canvas기반의 PhotoFramePreview 의 View 조합 및 렌더링 처리로 인한 동적 이미지 생성 방식이 ShareLink와 맞지 않다고 판단했다.
따라서 사용하기 편하지만 구체적인 내부 처리를 제어할 수 없는 ShareLink 사용을 포기하고
코드가 좀 늘어나는 것을 감수하여 UIActivityViewController 기반으로 공유 기능을 구현하는 방향으로 전환했다.
5-7. Representable 래핑 객체와 .sheet를 사용하기 ❌
아래 내용부턴 ShareLink가 아닌 UIActivityViewController와
.sheet를 기반으로 한 커스텀 공유 시트를 구현한 흐름입니다.
struct ActivityViewController: UIViewControllerRepresentable {
let activityItems: [Any]
var applicationActivities: [UIActivity]? = nil
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities
)
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { }
}
아래는 당시 시도했던 흐름을 그대로 옮긴 의사코드입니다.
.sheet의 ViewBuilder 안에서guard ... return을 쓰고 있어 실제로는 컴파일되지 않습니다.
// ResultView 내부
.sheet(isPresented: $showShareSheet) {
if let image = shareImage {
guard let image = PhotoComposer.render(with: resultPhoto) else { return }
shareImage = image
showShareSheet = true
}
}
위 방식대로 .sheet 를 통해 ActivityViewController를 띄우는 방법을 사용했지만 여전히 아래와 같은
Metal 크래시가 발생했다..
-[MTLDebugBuffer newTextureWithDescriptor:offset:bytesPerRow:]:321: failed assertion `New Texture With Descriptor Validation
resourceOptions (0x0) must match backing buffer resource options (0x10).'
ShareLink에서 ActivityVC로 넘어왔는데 똑같은 크래시가 터졌다는 건 이미지를 렌더링하거나 처리할 때의 문제라고 판단했다.
따라서 ShareLink를 사용하여 시도한 흐름과 동일하게 기존 PhotoComposer에서 사용중인 ImageRenderer 방식에서 UIGraphicsImageRenderer 기반으로 변경한 후 다시 시도해보았다.
5-8. ActivityVC 기반, PhotoComposer를 UIGraphicsImageRenderer 기반으로 변경 ❌
static func render(with information: PhotoInformation) -> UIImage? {
// 출력 이미지 크기 결정 (예: 1080 x 1440 또는 레이아웃 비율에 맞춤)
let targetWidth: CGFloat = 1080
let targetHeight: CGFloat = targetWidth / information.layout.previewAspect
let targetSize = CGSize(width: targetWidth, height: targetHeight)
let preview = PhotoFramePreview(information: information)
.frame(width: targetSize.width, height: targetSize.height)
// UIHostingController를 사용한 스냅샷 방식
let controller = UIHostingController(rootView: preview)
controller.view.bounds = CGRect(origin: .zero, size: targetSize)
controller.view.backgroundColor = .clear // 투명 배경
// 렌더링 포맷 설정
let format = UIGraphicsImageRendererFormat()
format.scale = 1.0
format.opaque = true
let renderer = UIGraphicsImageRenderer(size: targetSize, format: format)
return renderer.image { _ in
controller.view.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
위와 같이 UIHostingController를 사용해서 직접적인 Metal 이슈를 회피해보자.

다만, 위 방식을 사용할 경우 아래와 같이 AttributeGraph Cycle 로그가 출력된다..
=== AttributeGraph: cycle detected through attribute 1206056 ===
=== AttributeGraph: cycle detected through attribute 1244376 ===
View (0x93fd91c00, _TtGC7SwiftUI14_UIHostingViewGVS_15ModifiedContentV14mirroringBooth17PhotoFramePreviewVS_12_FrameLayout__) drawing with afterScreenUpdates:YES inside CoreAnimation commit is not supported.
=== AttributeGraph: cycle detected through attribute 1244376 ===
=== AttributeGraph: cycle detected through attribute 1206880 ===
=== AttributeGraph: cycle detected through attribute 1206880 ===
위 내용은 drawHierarchy가 SwiftUI의 뷰 업데이트 사이클과 충돌하고 있다는 뜻으로 보인다.
따라서 afterScreenUpdates가 필요없는 CALayer 기반 렌더링으로 변경해보자.
혹시 모르니 레이아웃을 강제로 렌더링하는 코드도 추가해보자.
// PhotoComposer.swift 하단부
let preview = PhotoFramePreview(information: information)
.frame(width: targetSize.width, height: targetSize.height)
// UIHostingController를 사용한 스냅샷 방식
let controller = UIHostingController(rootView: preview)
controller.view.bounds = CGRect(origin: .zero, size: targetSize)
controller.view.setNeedsLayout()
controller.view.layoutIfNeeded()
// 렌더링 포맷 설정
let format = UIGraphicsImageRendererFormat()
format.scale = 1.0
format.opaque = true
let renderer = UIGraphicsImageRenderer(size: targetSize, format: format)
return renderer.image { context in
controller.view.layer.render(in: context.cgContext)
}
…그래도 여전히 크래시가 터진다.
-[MTLDebugBuffer newTextureWithDescriptor:offset:bytesPerRow:]:321: failed assertion `New Texture With Descriptor Validation
resourceOptions (0x0) must match backing buffer resource options (0x10).
'
5-9. Metal API Validation 옵션 해제 ✅
거의 모든 방법을 사용해봤는데 도대체 뭐가 문제일까.. 고심한 결과
크래시 로그의 [MTLDebugBuffer newTextureWithDescriptor:offset:bytesPerRow:] 이 부분이 Objective-C에서 호출되고 있는 메서드로 보여 뭐하는 녀석인지 Perplexity 에게 물어보았다.

즉, 지금까지 출력된 크래시는 버퍼 메모리 에서 텍스처를 동적으로 실행할 때 Metal 검증이 실패하면 나타내는 assertion 메세지라고 한다.
실제로 위 Object-C 메서드를 통해 다음과 같이 매우 흔한 assertion이 출력된다고 한다.
-[MTLDebugBuffer newTextureWithDescriptor:offset:bytesPerRow:]:326:
failed assertion `resourceOptions (0x10) must match backing buffer resource options (0x20)`
.. 지금 내 상황과 완전히 일치하는 크래시 내용이었다.
조금 더 구글링을 진행한 결과 다음과 같은 내용을 찾을 수 있었다.
해당 크래시는 Metal의 Validation 에러 로그이다.
즉, Xcode 세팅에서 Metal API Validation을 끄면 자연스레 사라진다고 한다.. (?)
따라서 아래와 같이 Metal 의 API Validation을 해제하였다.

Metal API Validation 옵션은 Metal 시스템의 Validation 레이어를 활성화시키는 옵션이다.
이는 Xcode의 런타임 검증 도구로써 Metal API 호출 을 실시간으로 검사하여 리소스 생성 중 오류가 발생했는지, 인코딩 실수가 있는지, 메모리 접근을 잘못했는지 등을 검증해준다.
Validating your app’s Metal API usage | Apple Developer Documentation
stackoverflow.com/questions/60645401/xcode-logging-metal-api-validation-enabled
실제 Metal API Validation을 활성화함으로써 사용되는 MTLDebugBuffer는 실제 Metal 엔진엔 존재하지 않고 오직 디버그 전용 검증을 위해 래핑된 객체라고 한다.
그러다보니 해당 Validation 옵션을 해제하면 MTLDebugBuffer를 우회하고 바로 Metal 드라이버를 호출하기 때문에 CPU 오버헤드가 감소하는 효과를 얻을 수 있다.
실제로 위 옵션을 제거해보니
정상적으로 에어드랍 공유가 가능해졌다!
해당 시점에서 사용한 코드는 아래와 같다.
// ResultView 일부
ShareLink(
item: PhotoShareItem(image: result),
preview: SharePreview("미러링부스")
) {
sharingButtonLabel(
icon: "paperplane",
title: "Airdrop",
isContrast: true
)
}
// PhotoShareItem.swift
struct PhotoShareItem: Transferable {
let image: UIImage
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(exportedContentType: .jpeg) { item in
guard let data = item.image.jpegData(compressionQuality: 1.0) else {
throw TransferError.exportFailed
}
return data
}
.suggestedFileName("mirroringbooth_\(Date().timeIntervalSince1970).jpg") // 파일 이름은 임시 테스트용으로 넣어놨습니다. (수정예정)
}
enum TransferError: Error {
case exportFailed
}
}
그렇다면 Metal API Validation을 해제하는 것이 정말 근본적인 해결 방법일까? 라는 생각이 들었다.
현재 시도한 방법은 아래와 같다.
- ShareLink 사용 → 크래시 발생
- UIActivityViewController 사용 → 크래시 발생
- ImageRenderer가 아닌 UIGraphicsImageRenderer 사용 → 크래시 발생
- PNG로 노멀라이징 진행 → 크래시 발생
- CGImage로 다시 생성하기 → 크래시 발생
- CoreGraphics로 재렌더링 진행 → 크래시 발생
거의 모든 경우에서 크래시가 발생했으며..
이와 똑같은 증상을 겪은 사람들을 찾아본 결과( StackOverFlow 나 레딧같은 다른 커뮤니티) 공통적으로 해당 옵션을 제거함으로써 해결한 케이스가 대다수였다.
찾아본 커뮤니티 글
zhuanlan.zhihu.com/p/437328561
https://github.com/livekit/client-sdk-swift/issues/386
Apple Developer Forums — Metal API Validation 관련 스레드
stackoverflow.com/questions/60645401/xcode-logging-metal-api-validation-enabled
옵션을 끈 뒤 테스트를 진행하니 아래와 같이 정상적으로 기능이 동작하는 것을 확인했다.


위 처럼 진행했을 때 경고 몇개가 출력되는 것을 확인할 수 있다.
writeImageAtIndex:1094: ⭕️ ERROR: 'mirroringBooth' is trying to save an opaque image (2160x3015) with 'AlphaPremulLast'.
This would unnecessarily increase the file size and will double (!!!) the required memory when decoding the image --> ignoring alpha.
Unable to create bundle at URL ((null)): normalized URL null
Failed to send a DTLS packet with 78 bytes; sendmsg error: No route to host (65).
SSLWrite failed, packet was not sent for participant [0CC07A5B] channelID [4] DTLS context [0x8e4bf7480] pCList [0x8e45e0700]; SSLError = Would Block (-9803): errno = Undefined error: 0 (0).
Connection invalidated
Unable to create bundle at URL ((null))Failed to send a DTLS packetSSLWrite failed, Would Block
위 3개의 경고는 공유 시트가 실제 기기간 통신을 시도할 때 출력되는 내부 프레임워크 디버깅용 로그이며
이때, ERROR: 'mirroringBooth' is trying to save an opaque image (2160x3015) with 'AlphaPremulLast' 이런 로그가 찍히고 있는 것을 볼 수 있다.
이는 불투명 이미지 인데 알파 채널이 포함된 포맷으로 저장을 시도하려고 해서 발생되는 로그이다.
즉, PhotoComposer에서 반환되는 ImageRenderer.uiImage 는 실제로 불투명하지만, 포맷은 AlphaPremulLast로 알파채널이 포함되어있어서 발생되는 로그였다.
따라서 알파 채널을 제거하면 로그가 사라진다.
알파 채널을 제거하는 방법으론
- PhotoComposer에 JPEG로 압축하는 코드를 추가하거나,
UIGraphicsImageRendererFormat에 존재하는.opaque옵션을true로 설정하여 압축 없이 알파 채널만 제거하거나,- ImageRenderer 방식에 결과물을 PNG로 반환하거나,
- ImageRenderer 방식이 아닌 완전한
UIGraphicsImageRenderer방식으로 치환하는 방식이 존재한다.
물론.. 단순한 로그에 불과하니 넘어가도 괜찮지만 그래도 해결하기 위해 현재 상황에 그나마 적합한
2번 방식을 적용하기로 했다.
다만, ImageRenderer와 UIGraphicsImageRenderer 방식을 하이브리드로 사용한다는 단점이 존재하지만
기존 ImageRenderer 방식에서 온전히 UIGraphicsImageRenderer 방식을 적용하기 위해 UIHostingController 로 전환할 때 발생되는 비용이 더 클 것이라고 판단했다.
- PhotoFramePreview의 Canvas 렌더링이 제대로 동작되는지
- 날짜, 프레임, 사진 합성 기능이 동일하게 동작하는지,
- 예상하지 못한 레이아웃 차이가 발생하는지,
- 새로운 버그가 발생하거나 QA 시간에 더 투자해야한다는 리스크
따라서 PhotoComposer 코드에 아래와 같이 UIGraphicsImageRendererFormat 를 추가하여 알파 채널을 제거하고,
로그가 출력되지 않음을 확인했다.
@MainActor
static func render(with information: PhotoInformation) -> UIImage? {
// 출력 이미지 크기 결정 (예: 1080 x 1440 또는 레이아웃 비율에 맞춤)
let targetWidth: CGFloat = 1080
let targetHeight: CGFloat = targetWidth / information.layout.previewAspect
let targetSize = CGSize(width: targetWidth, height: targetHeight)
let preview = PhotoFramePreview(information: information)
.frame(width: targetSize.width, height: targetSize.height)
let renderer = ImageRenderer(content: preview)
// GPU 텍스처 한계(약 8192px)를 초과하지 않도록 scale 조정
let maxDimension: CGFloat = 8192
let maxScale = min(maxDimension / targetWidth, maxDimension / targetHeight)
renderer.scale = min(UIScreen.main.scale, maxScale)
guard let metalImage = renderer.uiImage else { return nil }
// 알파 채널 제거 <- 새로 추가된 코드입니다.
let format = UIGraphicsImageRendererFormat()
format.opaque = true
format.scale = metalImage.scale
let imageRenderer = UIGraphicsImageRenderer(size: metalImage.size, format: format)
return imageRenderer.image { _ in
metalImage.draw(at: .zero)
}
}
6. 결론
앞서 5-9. 에서 Metal API Validation 옵션을 해제함으로써 문제를 해결했다.
그러다보니 ShareLink 기반으로 간단하게 공유 시트를 구현하려 했지만
갤러리 저장 버튼과 용도와 기능이 일치하는 Activity가 존재했다.
물론, 그대로 유지하는 방향도 있지만 공유 시트의 경우
아래 이미지에 있는 활성화할 수 있는 무료 계정이 최대 개수... 이 부분이 Preview로 사용자에게 노출된다.

해당 Preview를 커스텀하거나..
혹은 excludedActivityTypes 을 사용해서 현재 앱 방향성에 맞지 않는 Activity는 제거하는 것이 좋다고 HIG에 명시되어있다.
💁 Make sure activities are appropriate for the current context. Although you can’t reorder system-provided tasks in an activity view, you can exclude tasks that aren’t applicable to your app. For example, if it doesn’t make sense to print from within your app, you can exclude the Print activity. You can also identify which custom tasks to show at any given time.
Activity views | Apple Developer Documentation
Activity views | Apple Developer Documentation
전체적으로 커스텀을 진행하기 위해선 ShareLink가 아니라
UIActivityViewController + UIActivityItemSource 을 조합해 뷰를 만드는 것이 좋겠다고 판단하여
최종적으로 UIActivityViewController 기반으로 작성했다.
import LinkPresentation
import SwiftUI
// MARK: - 공유 시트 담당 ActivityViewController
struct PhotoActivityViewController: UIViewControllerRepresentable {
let image: UIImage
let applicationActivities: [UIActivity]?
let excludedActivityTypes: [UIActivity.ActivityType]?
func makeUIViewController(context: Context) -> UIActivityViewController {
let appIcon = getAppIcon()
// ActivityItemSource 연결
let itemSource = PhotoActivityItemSource(image: image, appIcon: appIcon)
let controller = UIActivityViewController(
activityItems: [itemSource],
applicationActivities: applicationActivities
)
// Activity 제거 (파일에 저장)
controller.excludedActivityTypes = excludedActivityTypes
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { }
private func getAppIcon() -> UIImage? {
if let icons = Bundle.main.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any],
let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
let lastIcon = iconFiles.last,
let icon = UIImage(named: lastIcon) {
return icon
}
return UIImage(systemName: "camera.fill")
}
}
// MARK: - ActivityItemSource (ActivityViewController에 연결되는 프리뷰 인스턴스)
class PhotoActivityItemSource: NSObject, UIActivityItemSource {
let image: UIImage
let appIcon: UIImage?
init(image: UIImage, appIcon: UIImage?) {
self.image = image
self.appIcon = appIcon
super.init()
}
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return image
}
func activityViewController(
_ activityViewController: UIActivityViewController,
itemForActivityType activityType: UIActivity.ActivityType?
) -> Any? {
return image
}
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
let metadata = LPLinkMetadata()
metadata.title = "Mirroring Booth"
// 앱 아이콘 설정
if let appIcon = appIcon {
metadata.iconProvider = NSItemProvider(object: appIcon)
}
return metadata
}
}