YunDaeHyeon
← Lab

Airdrop 구현 방법을 알아보자

스터디2025.12.15
  • UIKit
  • Swift

🍚 에어드랍(AirDrop)에 대해

에어드랍(Airdrop) 은 Apple 기기 간 직접적인 무선 파일 공유 기능이다.

아이폰, 아이패드, 맥 기기에서 사진이나 동영상, 문서 혹은 앱 데이터 등을 WiFi와 Bluetooth로 1m 이내 기기에 초고속으로 전송하는 기능이다.

에어드롭은 무선이라는 특징을 가지고 있으며 케이블이나 인터넷 연결 없이 근처에 있는 Apple 기기라면 파일을 즉시 전송할 수 있다.

동작은 쉽게 설명하자면 Bluetooth로 기기를 발견한 뒤 WiFi로 데이터를 전송하는 흐름이다. (속도는 최대 30 ~50MB/s 까지 나온다고 한다.)

주요 특징을 아래 표에 정리해보았다.

특징설명
지원 기기iOS 7+, iPadOS, macOS Yosemite+
필요 조건WiFi/Bluetooth ON, 개인 핫스팟 OFF
수신 모드"받지 않음" / "연락처만" / "모두"
전송 데이터사진, 동영상, PDF, 앱 콘텐츠, 위치 등

🤔 Swift로 구현하는 방법

그래서 이 에어드랍을 코드로 어떻게 구현해야할까?

💡 Apple 개발자 공식 문서엔 직접적인 AirDrop 클래스와 API가 존재하지 않으며 공유 시스템 의 일부로 다뤄진다고 한다.

에어드랍은 독립적인 API가 아니라 UIActivityViewController가 띄우는 공유 시트에서 자동으로 나타나는 옵션이다.

즉, UIActivityViewController나 SwifUI의 ShareLink 같은 공유 메커니즘 안에 포함된 Activity 형태로 제공되며 에어드랍만 콕 찝어서 선택하거나.. 곧바로 근처 디바이스 리스트를 보여주는 전용 컨트롤러는 존재하지 않는다..

1. UIKit에서 UIActivityViewController를 직접 Present 하기

Activity views | Apple Developer Documentation

가장 단순하면서도 UIActivityViewController를 사용하는 기본적인 방법이라고 한다.

직접 구현해보자.

class ViewController: UIViewController {

    private let shareButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("에어드랍 공유", for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 20, weight: .bold)
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
				...

        shareButton.addTarget(self, action: #selector(shareButtonTapped), for: .touchUpInside)
    }

    @objc private func shareButtonTapped() {
        guard let image = UIImage(named: "mong") else { return }

        let activityVC = UIActivityViewController(
            activityItems: [image],
            applicationActivities: nil
        )

        // iPad 대응
        activityVC.popoverPresentationController?.sourceView = shareButton
        activityVC.popoverPresentationController?.sourceRect = shareButton.bounds

        present(activityVC, animated: true)
    }
}

위 코드를 실행해서 버튼을 클릭할 시 아래와 같이 에어드랍 창이 출력되며

AirDrop 버튼을 클릭 시 맥으로 이미지가 잘 넘어옴을 확인할 수 있다.

에어드랍 창

따로 설정을 넣은 게 없지만 카카오톡 이나 _슬랙_에 공유가 가능하다!

한 번 UIActivityController의 생성자를 확인해보자.

activityItems 는 [Any] 타입이다. 이는 공유할 실제 데이터 리스트를 말한다.

→ activityItems는 내부에서 각 데이터의 타입을 보고(UIImage, URL, String, NSAttributedString, UIActivityItemSource 등) 어떤 액티비티에서 처리 가능한지 판단한다.

예를 들어서 아래와 같이 자동으로 처리된다.

  • UIImage → 사진, 에어드랍, 메시지 등으로 처리 가능
  • URL → 사파리, 브라우저, 메신저, 에어드랍 등으로 처리 가능
  • String → 메모, 메일, 메시지, 복사 및 붙여넣기 등으로 처리 가능
  • 커스텀 타입 → 원하는 커스텀 타입을 만들고 싶다면 UIActivityItemSource로 감싸서 지원할 수 있다.

이때 UIActivityItemSource 는 액티비티 타입에 따라 다른 데이터, 제목, 썸네일을 제공하고 싶을 때 사용하는 프로토콜이다.

그렇다면 applicationActivities 는 뭘까?

→ 이는 UIActivity를 상속할 커스텀 액션들을 넣는 배열이다.

사용자가 시스템 액션(에어드랍이나 메시지, 메일 등을 뜻한다.) 외에 앱 전용 공유, 처리 버튼을 추가할 때 사용한다.

예를 들어서 아래와 같이 추가할 수 있다.

  • 미러링 부스 앱 내부에 저장한다.
  • PDF로 내보낸다.
  • 서버로 업로드한다.

한 번 멀티 아이템을 전송시켜보자.

기존 몽이 사진을 드랍하는 코디에 String과 URL을 추가해서 activityItems에 추가한다.

    @objc private func shareButtonTapped() {
        guard let image = UIImage(named: "mong") else { return }
        let name = "mong"
        let url = URL(string: "https://naver.com")!

        let activityVC = UIActivityViewController(
            activityItems: [image, name, url],
            applicationActivities: nil
        )

        // iPad 대응
        activityVC.popoverPresentationController?.sourceView = shareButton
        activityVC.popoverPresentationController?.sourceRect = shareButton.bounds

        present(activityVC, animated: true)
    }

에어드랍 → 카카오톡 → 나에게 전송하기

이렇게 총 3개의 데이터(String, URL, Image)가 에어드랍으로 전송되는 것을 확인할 수 있다.

🧐 아이패드에서 popover 설정이 필요한 이유

예시 코드에는 아래와 같은 코드가 존재한다.

activityVC.popoverPresentationController?.sourceView = shareButton
activityVC.popoverPresentationController?.sourceRect = shareButton.bounds

이는 아이패드 환경에서 activityVC가 발생될 위치를 지정하는 코드이다.

기본적으로 아이폰과 아이패드의 모달 표시 방식은 서로 상이하다.

  • 아이폰의 경우 기본적으로 전체 화면 또는 카드 형태를 가지는 일종의 모달로 출력된다.

→ 이 때문에 별도의 소스가 되는 뷰를 연결하거나 ActivityVC의 위치를 지정하지 않아도 괜찮다.

  • 아이패드의 경우 대부분의 모달 표시 방식이 popover로 표시된다.
  • UIActivityViewController로 기본적으로 popover presentation을 사용하기 때문에 어디서부터 팝오버를 띄워야 할 지(화면상의 기준점이라고 이해하자.) 시스템에 알려줘야한다.
  • ⭐️ 그렇지 않으면 런타임 상에서 크래시가 발생한다!

이러한 이유가 존재하여 아이패드에서 activityVC를 안전하게 사용하려면 아래와 같이 공유 화면이 어디에 출력될 지를 지정해줘야한다.

if let popover = activityVC.popoverPresentationController {
    popover.sourceView = sender
    popover.sourceRect = sender.bounds
}
  • popoverPresentationController
  • UIViewController의 presentationController 중 하나로 팝 오버를 제어하는 컨트롤러다.
  • UIActivityController가 아이패드에서 표시될 떄 해당 컨트롤러를 사용하여 여기서 어디에 어떻게 표시할지, 방향은 어떻게 할지 등을 결정한다.
  • sourceView
  • popover가 어떤 뷰를 기준으로 위치를 잡을지 결정한다.
  • 일반적으로 버튼(UIButton)이 있다면 그 버튼을 기준점으로 잡거나 self.view와 같이 현재 화면 전체를 기준점으로 잡을 수 있을 것이다.
  • sender에 들어갈 수 있는 건 UIView 의 인스턴스라면 그 무엇이든 가능하다!

→ 보통 sender가 버튼이라면 그대로 사용하지만 SwiftUI라면 임의의 UIView() 혹은 window의 rootView 등으로 지정한다.

  • sourceRect
  • sourceView 내부에서 어느 좌표 혹은 영역을 기준점으로 잡을지 결정한다.
  • CGRect로 표현되며 sender.bounds를 대입하면 “버튼의 전체 사각형을 기준으로 팝 오버가 그 위 혹은 옆에 표시된다는 의미이다.

👀 UIActivityViewController 딥다이브!

UIActivityViewController는 excludedActivityTypes 라는 프로퍼티가 존재한다.

이는 ActivityView에서 제외하고 싶은 작업을 배열로 지정하는 프로퍼티다.

예를 들어 아래와 같이 기본으로 제공되고 있는 활동(Activity)에 대해 표출되지 않도록 설정할 수 있다.

activityViewController.excludedActivityTypes = [
    .copyToPasteboard,
    .addToReadingList
]

이미지 출처 : https://mini-min-dev.tistory.com/300

지울 수 있는 항목은 아래 링크에서 확인할 수 있다.

페이스북.. 웨이보.. 트위터.. 신기한게 많이 있다!

UIActivity.ActivityType | Apple Developer Documentation

또한 Activity View가 닫힌 이후 실행하고 싶은 코드가 존재한다면 CompletionWithItemsHandler 를 통해 completion 클로저 형태로 로직을 담을 수 있다.


2. SwiftUI에서 ShareLink를 사용해 공유하기

UIKit에서 UIActivityViewController를 사용해서 구현했다면 iOS 16 이상의 SwiftUI에선 ShareLink 라는 네이티브 공유 버튼이 존재한다.

SharedLink는 일종의 버튼처럼 동작하며 내부적으로 공유 시트를 자동으로 호출한다.

→ 이때 아이패드는 자동으로 처리된다!!

한 번 직접 코드를 작성해보자.

import SwiftUI

struct ContentView: View {
    var body: some View {
        if let image = UIImage(named: "mong"),
           let _ = image.pngData() {
            ShareLink(
                item: Image(uiImage: image),
                preview: SharePreview(
                    "몽이",
                    image: Image(uiImage: image)
                )
            ) {
                HStack {
                    Image(systemName: "square.and.arrow.up")
                    Text("에어드랍")
                }
            }
        }
    }
}

위 코드를 실행하면 아래와 같이 실제 기기에서 표시된다.

오.. 신기하다.

한 번 ShareLink의 사용 형태를 살펴보자.

ShareLink는 여러 생성자를 기본적으로 제공하는데, 그 중 가장 기본적인 형태는 아래와 같다.

실제 공식 문서를 확인해보면 ShareLink는 크게 2가지 형태로 사용할 수 있음을 알 수 있습니다.
https://developer.apple.com/documentation/swiftui/sharelink

  • 기본 형태 (텍스트만 공유하고 싶을 때)
ShareLink(item: URL(string: "https://naver.com")!)
  • 커스텀 레이블 형태 (위에서 사용한 형태)
ShareLink(
    item: Image(uiImage: image),
    preview: SharePreview("몽이", image: Image(uiImage: image))
) {
    HStack {
        Image(systemName: "square.and.arrow.up")
        Text("에어드랍")
    }
}

이때 각 매개변수가 어떤 역할을 수행하는지 하나씩 뜯어보자!

  • item : 이는 공유하고자 하는 실제 데이터를 의미한다.
  • ⭐ 이때 중요한 점은, 해당 Item은 반드시 Transferable 프로토콜을 준수해야한다는 점이다.
  • iOS 16+ 에서 기본적으로 Transferable을 지원하는 타입은 아래와 같다.
  • String, UIImage, Data, Image, AttributedString
  • preview : 이는 공유 시트에 표시될 미리보기 를 의미한다.

→ 위쪽 이미지를 보면 공유 시트 상단에 몽이 사진과 몽이 이름이 그 역할을 수행한다.

  • preview는 SharePreview 타입으로, 공유 시트가 열려있을 때 사용자에게 보여줄 미리보기 정보 를 담는다.

  • SharePreview의 생성자는 아래와 같다.

    SharePreview(
        _ titleKey: LocalizedStringKey,  // 공유 항목의 제목
        image: Image,                     // 미리보기 이미지
        icon: Image                       // 작은 아이콘 (이건 선택사항이다.)
    )
    

⚠️ ShareLink의 주의사항

  • ShareLink는 기본적으로 iOS 16+ 에서만 동작하며 그 이하의 버전은 UIActivityViewController를 사용해야한다.

  • 커스텀 타입을 공유하고자 하면 반드시 Transferable 을 채택해야한다.

    struct CustomData: Transferable {
        let title: String
        let content: String
        
        static var transferRepresentation: some TransferRepresentation {
            CodableRepresentation(contentType: .json)
        }
    }
    
  • 위에서 설명한 UIActivityViewController와 달리 ShareLink는 아이패드에서 popover 설정을 자동으로 처리해준다.

→ 즉, 별도의 sourceView나 sourceRect 세팅이 없어도 해당 버튼 위치를 기준으로 알아서 팝 오버가 출력된다.

3. SwiftUI의 sheet를 이용해보기 (커스텀)

UIActivityViewController를 UIViewControllerRepresentable 로 래핑한다면 .sheet로 감싸서 드래그 제스처나 애니메이션을 활용할 수 있다!

stackoverflow.com/questions/58286344/presenting-uiactivityviewcontroller-from-swiftui-view

// MARK: - ShareSheet (재사용 컴포넌트)
struct ShareSheet: UIViewControllerRepresentable {
    let activityItems: [Any]
    
    func makeUIViewController(context: Context) -> UIActivityViewController {
        let controller = UIActivityViewController(
            activityItems: activityItems,
            applicationActivities: nil
        )
        
        // iPad 대비 (SwiftUI가 자동 처리하지만 그래도 명시한다.)
        if let popover = controller.popoverPresentationController {
            popover.sourceRect = CGRect(x: UIScreen.main.bounds.midX, 
                                       y: UIScreen.main.bounds.midY, 
                                       width: 0, height: 0)
        }
        return controller
    }
    
    func updateUIViewController(_ controller: UIActivityViewController, context: Context) {}
}

// MARK: - 사용 예시
struct ContentView: View {
    @State private var showSheet = false
    @State private var selectedImage = UIImage(named: "sample")!
    
    var body: some View {
        VStack(spacing: 20) {
            Image(uiImage: selectedImage)
                .resizable()
                .scaledToFit()
                .frame(height: 200)
            
            Button("에어드롭 공유") {
                showSheet = true  // 바인딩으로 제어한다.
            }
            .buttonStyle(.borderedProminent)
        }
        .sheet(isPresented: $showSheet) {
            ShareSheet(activityItems: [selectedImage])
        }
    }
}


참고 문서

[UIActivityViewController] 우리 앱에 "공유하기" 기능 (Share Sheet) 구현하는 방법

Adding AirDrop File Sharing Feature to Your iOS Apps

AirDrop으로 근처 Apple 기기에 항목 보내기 - Apple 지원 (KR)

UIActivityViewController | Apple Developer Documentation

airDrop | Apple Developer Documentation

UIPopoverPresentationController | Apple Developer Documentation

ShareLink | Apple Developer Documentation

Information Property List | Apple Developer Documentation

Activity views | Apple Developer Documentation