iOS 공유하기 LPLinkMetadata (UIKit, SwiftUI) 디자인패턴 적용
iOS 시스템 공유하기 기능을 개발
SwiftUI에서 지원하는 ShareLink도 있지만, 더 범용성있게 사용하기 위해 해당 형태로 구현
히스토리
- 2021.06.17.: 초기버전
- Swift 5, Xcode 12, iOS minimum Target 9.0
- 2024.12.11: 개선버전
- Swift 5.10, Xcode 16.1, minimum Target 13.0
- 빌더 패턴 적용
- Mixin and Trait 개념 적용
- 모듈화
- LinkMetaData 적용
- 2026.07.24: 네이밍 개선버전
- Swift 6
- 역할이 이름에 드러나도록 리네임: SystemSharingFeature는 ShareSheetPresentable로, SharingFeatureBuilder는 ShareSheetBuilder로 변경
- strict concurrency 대응으로 UI를 다루는 타입에 @MainActor 명시
- 2026.07.26: 모듈 분리 버전
- ShareSheetUI 별도 모듈(Swift Package)로 분리
- 프로토콜 채택(Mixin) 제거. import ShareSheetUI 후 ShareSheet.show를 직접 호출하는 구조로 변경
- 채택을 강제하지 않고, 능력의 출처가 호출부(import)에 드러나는 것이 장점
- keyWindow, topMostViewController 헬퍼는 모듈 내부로 은닉
코드 구현에 사용된 개념
목차
- 결과 스크린샷
- 구현부 전체 코드
- Usage
- SwiftUI
- UIKit + MVC
- UIKit + MVVM
결과 스크린샷
액티비티 아이템에는 빌더 패턴을 통해 여러 값들을 넣어줄 수 있음. 시뮬레이터 예제로 실기기에서 테스트하면 더 다양한 옵션이 존재.
- 왼쪽이미지
- URL만 넣어준 경우 웹에서 세팅된 값을 자동으로 설정
- 오른쪽 이미지
- URL 뿐만아니라 썸네일, 타이틀 등 내가 필요한 것들을 명시적으로 세팅


구현부 전체 코드
구현에 대한 전체 코드. ShareSheetUI라는 별도 모듈(Swift Package)로 분리. 사용하는 쪽은 프로토콜 채택 없이 import ShareSheetUI 후 ShareSheet.show를 호출.
// MARK: - ShareSheet
/// 시스템 공유하기 진입점
///
/// 윈도우 최상단에 띄우는 방식이라 View가 없는 객체(예: RIBs의 Viewless RIB)에서도 사용 가능
public enum ShareSheet {
@MainActor
public static func show(_ activityViewController: UIActivityViewController) {
if let keyWindow = UIWindow.keyWindow {
let topViewController = keyWindow.rootViewController?.topMostViewController()
topViewController?.present(activityViewController, animated: true, completion: nil)
}
}
}
// MARK: - UIWindow + keyWindow (모듈 내부 전용)
extension UIWindow {
@MainActor
static var keyWindow: UIWindow? {
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first { $0.isKeyWindow }
}
}
// MARK: - UIViewController + topMostViewController (모듈 내부 전용)
extension UIViewController {
func topMostViewController() -> UIViewController {
if let presented = presentedViewController {
return presented.topMostViewController()
}
if let navigationController = self as? UINavigationController {
return navigationController.visibleViewController?.topMostViewController() ?? navigationController
}
if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.topMostViewController() ?? tabBarController
}
return self
}
}
// MARK: - ShareSheetBuildable
@MainActor
public protocol ShareSheetBuildable {
func excludedActivityTypes(_ excludedActivityTypes: [UIActivity.ActivityType]) -> Self
func activityItemSource(_ activityItemSource: ActivityItemSource) -> Self
func activityItem(_ activityItems: [Any]) -> Self
func build() -> UIActivityViewController
}
// MARK: - ShareSheetBuilder (Builder Pattern)
@MainActor
public final class ShareSheetBuilder: ShareSheetBuildable {
var excludedActivityTypes: [UIActivity.ActivityType]?
var activityItemSource: ActivityItemSource?
var activityItems: [Any]?
public init() {}
public func excludedActivityTypes(_ excludedActivityTypes: [UIActivity.ActivityType]) -> Self {
self.excludedActivityTypes = excludedActivityTypes
return self
}
public func activityItemSource(_ activityItemSource: ActivityItemSource) -> Self {
self.activityItemSource = activityItemSource
return self
}
public func activityItem(_ activityItems: [Any]) -> Self {
self.activityItems = activityItems
return self
}
public func build() -> UIActivityViewController {
let activityViewController = UIActivityViewController(
activityItems: activityItems ?? [activityItemSource as Any],
applicationActivities: nil
)
activityViewController.excludedActivityTypes = excludedActivityTypes
return activityViewController
}
}
// MARK: - ActivityItemSource (LPLinkMetadata)
/// 공유 시트에 노출될 미리보기(타이틀, 썸네일, URL)를 명시적으로 세팅하는 아이템 소스
public final class ActivityItemSource: NSObject, UIActivityItemSource {
private let title: String
private let url: URL?
private let image: UIImage
public init(title: String, urlString: String, image: UIImage) {
self.title = title
self.url = URL(string: urlString)
self.image = image
}
public func activityViewControllerPlaceholderItem(
_ activityViewController: UIActivityViewController
) -> Any {
title
}
public func activityViewController(
_ activityViewController: UIActivityViewController,
itemForActivityType activityType: UIActivity.ActivityType?
) -> Any? {
url ?? title
}
public func activityViewController(
_ activityViewController: UIActivityViewController,
subjectForActivityType activityType: UIActivity.ActivityType?
) -> String {
title
}
public func activityViewControllerLinkMetadata(
_ activityViewController: UIActivityViewController
) -> LPLinkMetadata? {
let metadata = LPLinkMetadata()
metadata.title = title
metadata.originalURL = url
metadata.url = url
metadata.imageProvider = NSItemProvider(object: image)
metadata.iconProvider = NSItemProvider(object: image)
return metadata
}
}
Usage
사용법 정리
- SwiftUI
- UIKit + MVC
- UIKit + MVVM
빌더 패턴을 적용하여 필요한 것들을 붙여서 사용할 수 있음
- 윈도우 위에 띄우는 구현 방식으로 View가 없는 객체에서도 사용할 수 있음
- 즉, RIBs에서 View가 없는 RIB에서 해당 행위 호출 가능하다는 의미

import ShareSheetUI
// MARK: - Usage (SwiftUI)
struct SwiftUIView: View {
var body: some View {
Button("공유하기 버튼 실행") {
let builder = ShareSheetBuilder()
.activityItemSource(.init(title: "당근", urlString: "https://rldd.tistory.com/720", image: .init()))
.build()
ShareSheet.show(builder)
}
}
}
// MARK: - Usage (UIKit - MVC)
final class ViewController_MVC: UIViewController {
init() {
super.init(nibName: nil, bundle: nil)
configureUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private lazy var shareButton: UIBarButtonItem = {
let btn = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(didTapShareButton))
return btn
}()
@objc private func didTapShareButton() {
let builder = ShareSheetBuilder()
.activityItemSource(.init(title: "당근", urlString: "https://rldd.tistory.com/720", image: .init()))
.build()
ShareSheet.show(builder)
}
private func configureUI() {
navigationItem.rightBarButtonItem = shareButton
}
}
// MARK: - Usage (UIKit - MVVM)
final class ViewModel {
enum Action {
case shareButtonTapped
}
func send(action: Action) {
switch action {
case .shareButtonTapped:
let builder = ShareSheetBuilder()
.activityItemSource(.init(title: "당근", urlString: "https://rldd.tistory.com/720", image: .init()))
.build()
ShareSheet.show(builder)
}
}
}
final class ViewController_MVVM: UIViewController {
private let viewModel: ViewModel
init() {
self.viewModel = .init()
super.init(nibName: nil, bundle: nil)
configureUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private lazy var shareButton: UIBarButtonItem = {
let btn = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(didTapShareButton))
return btn
}()
@objc private func didTapShareButton() {
viewModel.send(action: .shareButtonTapped)
}
private func configureUI() {
navigationItem.rightBarButtonItem = shareButton
}
}
'Project > 개발 업무' 카테고리의 다른 글
| (Xcode 15.0 beta) Preview Macro Bug (0) | 2023.06.08 |
|---|---|
| Lottie 리소스 문제로 앱이 초기화되는 현상 (0) | 2022.07.12 |
| iOS RxDelegateProxy 만들어보기 #2 (StarScream + RxProxy) (0) | 2022.01.12 |
| iOS 앱 이름 변경하기 (1) | 2021.11.09 |
| [iOS] WebView javaScript 함수 호출 (1) | 2021.10.25 |