Ssul's Blog
ios(swift)개발, 다른 앱에서 내앱 호출하기(딥링크) 본문
ios앱을 개발중이다. 이번 목표는 아래와 같다.
유튜브 앱에서 공유하기 버튼을 누른다. 공유 가능한 앱에 내 앱아이콘이 등장한다.
해당 앱 아이콘을 클릭하면, 내 앱으로 이동한다.
내앱에 들어가서, 내가 원하는 메뉴를 띄우고, 공유하는 유튜브 링크를 붙여넣는다.
위 3가지 포인트가 완성되어야 한다. 어떻게 해야 할까?
#1. ShareExtension(내 앱 아이콘이 등장 > 내 앱으로 이동 전까지)
ShareExtension은 다른 앱에서 공유하기 눌렀을때, 내 앱아이콘이 뜨고, 그걸 클릭했을때 연결해주는 역할을 하는 녀석임.
1-1. Xcode → File ▸ New ▸ Target ▸ iOS ▸ Share Extension클릭
- 적절한 이름을 설정하고 추가
1-2. bundle Identifier설정
- 새로생긴 ShareExtension클릭 > Singing & Capabilities > bundle identifier설정
- com.mycompanay.myapp.share
1-3. Targets > myappShareExtension > Signing & Capabilities ▸ + Capability ▸ App Groups
- 호스트-앱과 같은 ID (group.com.mycompany.myapp) 추가
1-4. Targets > myapp > Signing & Capabilities ▸ + Capability ▸ App Groups
- ShareExtension과 같은 ID (group.com.mycompany.myapp) 추가
1-5. ShareExtension 코드구성(myappShareExtension/info.plist)
- 다른앱에서 나의 앱으로 진입하는 지점이다
- 해당 진입지적을 $(PRODUCT_MODULE_NAME).ShareExtensionHandler로 설정
(*storyboard와 ShareViewController를 사용한다면 $(PRODUCT_MODULE_NAME).ShareViewController로 설정)
- myappShareExtension/info.plist를 설정. app groups와 principalClass잘 설정해주기
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<!-- 여기 여러분의 그룹으로 -->
<string>group.com.mycompany.myapp</string>
</array>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<!-- "URL 1개" 받을 때만 Share Sheet에 노출 -->
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
<!-- 유튜브 URL 필터 추가 -->
<key>NSExtensionActivationWebURLPredicate</key>
<string>SUBQUERY(absoluteURL, $url, $url CONTAINS 'youtube.com' OR $url CONTAINS 'youtu.be').@count > 0</string>
</dict>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ShareExtensionHandler</string>
</dict>
</dict>
</plist>
[다른앱에서 공유하기를 누르면 나의 앱이 보인다 + 해당 앱 아이콘을 클릭하면 ShareExtensionHandler를 실행한다
1-6. ShareExtensionHandler.swift구성(앱 아이콘 클릭시 실행되어야 할 코드)
- 추가된 폴더 안에 ShareExtensionHandler.swift를 생성합니다
(*이 파일은 유튜브앱에서 내 앱 아이콘을 클릭시 실행되는 코드가 들어갑니다.)
(**storyboard+ShareViewController로 구현이 가능하지만, 저는 특별히 화면이 필요없고, 클릭시 바로 내 앱으로 넘어가면 되기 때문에 ShareExtensionHandler사용)
//
// ShareExtensionHandler.swift
// InboxShareExtension
//
//
// InboxShareExtension/ShareExtensionHandler.swift
import UIKit
import MobileCoreServices
import UniformTypeIdentifiers
final class ShareExtensionHandler: UIViewController {
private let appGroupID = "group.com.mycompany.myapp"
private let mainAppURLScheme = "deeplinkmyapp://"
override func viewDidLoad() {
super.viewDidLoad()
// 번들 ID 확인
let bundleID = Bundle.main.bundleIdentifier ?? "없음"
print("🔍 ShareExtension 번들 ID: \(bundleID)")
// App Group ID 확인
let defaults = UserDefaults(suiteName: appGroupID)
let canAccessAppGroup = defaults != nil
print("🔍 App Group 접근 가능: \(canAccessAppGroup)")
extractURLAndShareToMainApp()
}
private func extractURLAndShareToMainApp() {
print("🔄 ShareExtension: extractURLAndShareToMainApp 시작")
// 첫 번째 extensionItem 가져오기
guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else {
print("❌ ShareExtension: extensionItem 없음")
complete(shouldOpenMainApp: false)
return
}
// 디버깅 정보
print("🔍 ShareExtension: extensionItem = \(extensionItem)")
if let attachments = extensionItem.attachments {
print("🔍 ShareExtension: attachments = \(attachments.count)개")
// 각 첨부파일 타입 상세 출력
for (i, attachment) in attachments.enumerated() {
print("🔍 첨부파일 #\(i+1) 지원 타입:")
attachment.registeredTypeIdentifiers.forEach { type in
print(" - \(type)")
}
}
}
// 지원하는 URL 타입 목록
let urlTypeIdentifiers = [
UTType.url.identifier, // "public.url"
kUTTypeURL as String, // "public.url"
"public.url",
UTType.plainText.identifier, // "public.plain-text" (텍스트로 URL이 공유될 수 있음)
"public.plain-text",
"public.text"
]
// 사용할 첨부파일과 타입 식별자 찾기
var selectedProvider: NSItemProvider? = nil
var selectedTypeIdentifier: String? = nil
// 모든 첨부파일 확인
if let attachments = extensionItem.attachments {
for provider in attachments {
for typeID in urlTypeIdentifiers {
if provider.hasItemConformingToTypeIdentifier(typeID) {
selectedProvider = provider
selectedTypeIdentifier = typeID
print("✅ ShareExtension: 타입 \(typeID)를 지원하는 첨부파일 발견")
break
}
}
if selectedProvider != nil { break }
}
}
guard let provider = selectedProvider, let typeID = selectedTypeIdentifier else {
print("❌ ShareExtension: URL 또는 텍스트 형식의 첨부파일 없음")
complete(shouldOpenMainApp: false)
return
}
// 첨부파일에서 데이터 로드
provider.loadItem(forTypeIdentifier: typeID, options: nil) { [weak self] data, error in
if let error = error {
print("❌ ShareExtension: URL 로드 에러: \(error.localizedDescription)")
self?.complete(shouldOpenMainApp: false)
return
}
print("🔍 ShareExtension: 로드된 데이터 타입: \(type(of: data))")
// 다양한 형식의 데이터 처리
if let url = data as? URL {
print("✅ ShareExtension: URL 직접 추출 성공: \(url.absoluteString)")
self?.handleURL(url)
} else if let text = data as? String {
print("🔍 ShareExtension: 텍스트로 받음: \(text)")
// 텍스트에서 URL 추출 시도
if let url = URL(string: text.trimmingCharacters(in: .whitespacesAndNewlines)) {
print("✅ ShareExtension: 텍스트에서 URL 추출 성공: \(url.absoluteString)")
self?.handleURL(url)
} else {
print("❌ ShareExtension: 텍스트에서 URL 추출 실패")
self?.complete(shouldOpenMainApp: false)
}
} else {
print("❌ ShareExtension: 지원하지 않는 데이터 형식: \(String(describing: data))")
self?.complete(shouldOpenMainApp: false)
}
}
}
private func handleURL(_ url: URL) {
print("✅ ShareExtension: URL 추출 성공: \(url.absoluteString)")
// 유튜브 URL인지 확인 - 다양한 패턴 지원
let urlString = url.absoluteString.lowercased()
let isYoutubeURL =
url.host?.contains("youtube.com") == true ||
url.host?.contains("youtu.be") == true ||
urlString.contains("youtube.com") ||
urlString.contains("youtu.be")
guard isYoutubeURL else {
print("ℹ️ ShareExtension: 유튜브 URL이 아님: \(url.absoluteString)")
complete(shouldOpenMainApp: false)
return
}
print("✅ ShareExtension: 유튜브 URL 확인됨")
// App-Group에 URL 저장
let defaults = UserDefaults(suiteName: self.appGroupID)
defaults?.set(url.absoluteString, forKey: "sharedURL")
defaults?.synchronize()
print("✅ ShareExtension: UserDefaults에 URL 저장 완료")
// 메인 앱으로 돌아가기 위한 플래그 설정
defaults?.set(true, forKey: "pendingYoutubeShare")
defaults?.synchronize()
// 메인 앱으로 전환 (0.5초 지연으로 저장 완료 보장)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.complete(shouldOpenMainApp: true)
}
}
private func complete(shouldOpenMainApp: Bool) {
print("🔄 ShareExtension: complete(shouldOpenMainApp: \(shouldOpenMainApp))")
if shouldOpenMainApp {
// 메인 앱으로 돌아가기 위한 URL
if let url = URL(string: "\(mainAppURLScheme)open-from-share?t=\(Date().timeIntervalSince1970)") {
// 캐시 방지를 위해 타임스탬프 추가
var responder: UIResponder? = self
var targetApplication: UIApplication?
while responder != nil && targetApplication == nil {
if let app = responder as? UIApplication {
targetApplication = app
}
responder = responder?.next
}
// URL 열기 시도
self.extensionContext?.completeRequest(returningItems: [], completionHandler: { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
_ = targetApplication?.open(url)
print("✅ ShareExtension: 메인 앱 열기 시도 완료")
}
})
} else {
print("❌ ShareExtension: URL 생성 실패")
self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
} else {
// 그냥 Extension 종료
self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
print("ℹ️ ShareExtension: 메인 앱으로 전환하지 않고 종료")
}
}
}
- Info.plist에 NSExtensionPrincipalClass가 ShareExtensionHandler로 설정되어 있으므로 iOS는 이 클래스의 인스턴스를 생성
- viewDidLoad()가 호출되고,
- 유튜브 URL을 추출합니다.
- 추출한 URL을 App Group의 UserDefaults에 저장합니다(*이 Group UserDefault가 내 앱과 정보를 공유하는 개념)
- 나의 앱과 ShareExtension은 따로 있다고 생각하는게 편함. ShareExtension은 다른 앱에서 노출되는 나의 앱 아이콘, App Group으로 데이터 공유.. 이정도의 개념으로
- shouldOpenMainApp이 호출되면, 설정한 메인앱 딥링크로 호스트(나의 앱)앱 호출
(*해당 코드에서는 deeplinkmyapp://)
#2. 메인앱이 응답하게 하기(내 앱으로 이동)
2-1. AppDelegate 설정
- 동일한 딥링크를 사용하고, app Group사용
//
// AppDelegate.swift
//
import SwiftUI
import GoogleSignIn
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
// 번들 ID 확인
let bundleID = Bundle.main.bundleIdentifier ?? "없음"
print("🔍 메인 앱 번들 ID: \(bundleID)")
// App Group ID 확인
let defaults = UserDefaults(suiteName: SharedURLRouter.appGroupID)
let canAccessAppGroup = defaults != nil
print("🔍 메인 앱 App Group 접근 가능: \(canAccessAppGroup)")
// 2️⃣ Share Extension 으로부터 전달된 URL 소비
consumeSharedURLIfNeeded()
return true
}
func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
print("🔄 AppDelegate: URL 수신됨: \(url)")
// 2️⃣ 아니면 커스텀 스킴 -> 라우터
if url.scheme == "deeplinkmyapp" {
print("✅ AppDelegate: 커스텀 스킴 URL 감지됨: \(url)")
// open-from-share 패스인 경우 SharedURLRouter를 통해 공유된 URL을 가져옴
if url.host == "open-from-share" {
print("✅ AppDelegate: open-from-share 경로 감지됨")
if let sharedURL = SharedURLRouter.consumeSharedURL() {
print("✅ AppDelegate: 공유된 URL 발견 및 처리: \(sharedURL)")
Router.shared.route(to: .openSharedURL(sharedURL))
} else {
print("⚠️ AppDelegate: open-from-share 요청이지만 공유된 URL이 없음")
}
} else {
// 다른 딥링크 경로인 경우 URL 자체를 라우터에 전달
Router.shared.route(to: .openSharedURL(url))
}
return true
}
print("❌ AppDelegate: 매칭되는 URL 처리자 없음")
return false
}
// 앱이 백그라운드에서 포그라운드로 돌아올 때 호출됨
func applicationWillEnterForeground(_ application: UIApplication) {
print("🔄 AppDelegate: applicationWillEnterForeground")
// 공유된 URL이 있는지 확인하고 처리
consumeSharedURLIfNeeded()
}
// 앱이 활성화될 때 호출됨
func applicationDidBecomeActive(_ application: UIApplication) {
print("🔄 AppDelegate: applicationDidBecomeActive")
// 공유된 URL이 있는지 다시 한번 확인
consumeSharedURLIfNeeded()
}
// MARK: - Private Helpers
/// App-Group UserDefaults 에 저장된 공유 URL 소비 후 삭제
private func consumeSharedURLIfNeeded() {
print("🔄 AppDelegate: consumeSharedURLIfNeeded 시작")
// 1. pendingYoutubeShare 플래그 확인
let defaults = UserDefaults(suiteName: SharedURLRouter.appGroupID)
let hasPendingShare = defaults?.bool(forKey: "pendingYoutubeShare") ?? false
if hasPendingShare {
print("✅ AppDelegate: pendingYoutubeShare 플래그 감지됨")
// 플래그 초기화
defaults?.removeObject(forKey: "pendingYoutubeShare")
defaults?.synchronize()
// sharedURL 확인 및 처리
if let url = SharedURLRouter.consumeSharedURL() {
print("✅ AppDelegate: 공유된 URL 발견: \(url)")
// 홈 화면으로 이동하고 유튜브 링크 뷰 표시 (1초 지연으로 앱 초기화 완료 보장)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
Router.shared.route(to: .openSharedURL(url))
print("✅ AppDelegate: 공유된 URL 라우팅 완료")
}
return
}
}
// 2. 기존 로직 - URL만 단순 확인
if let url = SharedURLRouter.consumeSharedURL() {
print("✅ AppDelegate: 공유된 URL 발견: \(url)")
Router.shared.route(to: .openSharedURL(url))
} else {
print("ℹ️ AppDelegate: 공유된 URL 없음")
}
}
}
- ShareExtensionHandler.swift에서 내 앱을 딥링크(deeplinkmyapp://으로 호출)
- appdelegate가 캐치하여 주요로직 함수 실행
2-2. app group에 공유된 유튜브 링크를 확인하고 가져오기
// General/SharedURLRouter.swift
import Foundation
enum SharedURLRouter {
static let appGroupID = "group.com.mycompany.myapp"
static let sharedKey = "sharedURL"
/// App-Group 에 저장된 URL 있으면 반환 후 바로 삭제
static func consumeSharedURL() -> URL? {
print("🔄 SharedURLRouter: UserDefaults에서 URL 검색 시작")
let defaults = UserDefaults(suiteName: appGroupID)
defer {
defaults?.removeObject(forKey: sharedKey)
print("🔄 SharedURLRouter: UserDefaults에서 URL 삭제됨")
}
let urlString = defaults?.string(forKey: sharedKey)
print(urlString != nil ? "✅ SharedURLRouter: URL 문자열 발견: \(urlString!)" : "ℹ️ SharedURLRouter: URL 문자열 없음")
guard let urlString = urlString, let url = URL(string: urlString) else {
return nil
}
print("✅ SharedURLRouter: URL 파싱 성공: \(url)")
return url
}
}
// General/Router.swift
import Foundation
import Combine
/// 앱 전역 내비게이션/딥링크 허브
@MainActor
final class Router: ObservableObject {
/// 단일 인스턴스
static let shared = Router()
private init() { }
/// 현재 라우팅 명령
@Published var route: Route?
/// 화면 전환‧로직 실행
func route(to destination: Route) {
print("🔄 Router: 라우팅 요청 - \(destination)")
self.route = destination
// 추가 로그
Task { @MainActor in
print("✅ Router: route 속성 설정됨: \(String(describing: self.route))")
}
}
}
/// 라우팅 목적지 정의
enum Route: Equatable {
case openSharedURL(URL) // 📌 Share Extension으로 받은 링크
// case authCallback(...) // 필요하면 계속 추가
static func == (lhs: Route, rhs: Route) -> Bool {
switch (lhs, rhs) {
case (.openSharedURL(let lhsURL), .openSharedURL(let rhsURL)):
return lhsURL == rhsURL
}
}
}
- 전역에서 사용할수 있는 Router를 생성하여, 향후 내가 원하는 페이지에서 Router의 변화를 감지하여, 실행할수 있게 셋팅
#3. 내가 원하는 페이지에서 Router변화 체크 > 들어온 유튜브링크를 원하는 페이지에서 공유
- 전역으로 선언된 Router를 감지합니다
// UserDefaults에서 공유된 URL 확인
private func checkForSharedURL() {
print("🔄 HomeViewModel: UserDefaults에서 공유된 URL 확인")
if let url = SharedURLRouter.consumeSharedURL() {
print("✅ HomeViewModel: 공유된 URL 발견: \(url)")
// 유튜브 URL인지 확인
if url.host?.contains("youtube.com") == true || url.host?.contains("youtu.be") == true {
print("✅ HomeViewModel: 유튜브 URL 감지됨: \(url)")
// 메인 스레드에서 UI 업데이트 보장
DispatchQueue.main.async {
self.inputYoutubeLink = url.absoluteString
self.showYoutubeLinkInput = true
print("✅ HomeViewModel: showYoutubeLinkInput = true 설정됨")
}
} else {
print("ℹ️ HomeViewModel: 유튜브 URL이 아님: \(url)")
}
} else {
print("ℹ️ HomeViewModel: 공유된 URL 없음")
}
}
- 내가 원하는 페이지인 YoutubeLinkInputView.swift가 자동으로 띄워지고, 공유된 링크를 삽입합니다
#4. 다른 앱에서 내앱으로 링크 공유하고, 받은 링크 원하는 페이지에 띄우는 방법 정리
1. ShareExtension을 추가한다
2. shareExtension과 나의 앱에 동일한 App Group을 설정한다
3. ShareExtensionHandler가 딥링크를 통해 내 앱을 호출한다
4. 내앱은 호출되어서, App Group의 데이터를 확인한다
5. Router를 전역으로 설정하여, 공유된 링크를 실시간으로 확인한다
6. Router의 변화를 감지하면, 내가 원하는 페이지에서 반응하게 한다.
'dev > 기능구현' 카테고리의 다른 글
Vercel, AWS(Route53) 도메인 연결하기 (1) | 2025.07.23 |
---|---|
IOS 앱개발 String Catalog로 한국어, 영어 (다국어)동시설정(swiftui) (0) | 2025.04.02 |
ios push알림 기능 설정 (0) | 2025.02.25 |
[ChatGPT, DALLE2] 인공지능 카카오챗봇 만들기 (3) | 2024.02.07 |
[Django, tailwind] AI가 상담글에 자동으로 댓글 달아주기 #2 (react, tailwind) (1) | 2024.01.24 |