引言

在iOS应用开发中,分享功能是提升用户参与度和应用传播的关键特性。无论是社交媒体分享、文件传输还是跨应用数据交换,iOS提供了多种分享接口来满足不同场景的需求。本文将从基础概念出发,逐步深入到高级应用,并提供完整的开发流程解析和常见问题解决方案,帮助开发者构建高效、稳定的分享功能。

一、基础概念与准备工作

1.1 iOS分享框架概述

iOS平台主要提供以下几种分享方式:

  • UIActivityViewController:系统标准分享界面,支持多种分享方式
  • UIPasteboard:剪贴板分享,适用于简单文本或数据
  • UIDocumentInteractionController:文档交互控制器,用于文件分享
  • Share Extension:系统级分享扩展,允许应用在其他应用中显示分享选项
  • Custom Share Sheet:自定义分享界面,使用第三方库或自定义UI

1.2 开发环境配置

在开始开发前,确保满足以下条件:

// 1. 确保项目支持iOS 8.0+(UIActivityViewController最低版本)
// 2. 在Info.plist中添加必要的权限声明(如需要访问相册、文件等)
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册以分享图片</string>
<key>NSFileProviderDomainUsageDescription</key>
<string>需要访问文件以分享文档</string>

// 3. 导入必要的框架
import UIKit
import MobileCoreServices  // 用于UTI类型定义

1.3 分享数据类型准备

在分享前,需要明确要分享的数据类型和格式:

// 常见的分享数据类型
enum ShareContentType {
    case text(String)          // 纯文本
    case image(UIImage)        // 图片
    case url(URL)              // 链接
    case file(URL)             // 文件
    case mixed([Any])          // 混合类型
}

// 示例:准备分享数据
func prepareShareData() -> [Any] {
    var shareItems: [Any] = []
    
    // 文本内容
    shareItems.append("这是一段分享的文本内容")
    
    // 图片(从本地或网络获取)
    if let image = UIImage(named: "share_image") {
        shareItems.append(image)
    }
    
    // URL链接
    if let url = URL(string: "https://www.example.com") {
        shareItems.append(url)
    }
    
    // 文件(需要先保存到临时目录)
    let tempFileURL = FileManager.default.temporaryDirectory.appendingPathComponent("document.pdf")
    // ... 创建或复制文件到tempFileURL
    
    return shareItems
}

二、基础分享功能实现

2.1 使用UIActivityViewController实现标准分享

这是最常用、最简单的分享方式,系统自动处理界面和分享渠道。

import UIKit

class ShareViewController: UIViewController {
    
    // MARK: - 基础分享方法
    func showBasicShareSheet() {
        // 1. 准备分享内容
        let textToShare = "这是要分享的文本内容"
        let urlToShare = URL(string: "https://www.example.com")!
        
        // 2. 创建分享项目数组
        let shareItems: [Any] = [textToShare, urlToShare]
        
        // 3. 创建UIActivityViewController
        let activityViewController = UIActivityViewController(
            activityItems: shareItems,
            applicationActivities: nil
        )
        
        // 4. 设置排除的分享选项(可选)
        activityViewController.excludedActivityTypes = [
            .assignToContact,  // 排除"指定联系人"
            .print,            // 排除"打印"
            .saveToCameraRoll  // 排除"存储到相册"
        ]
        
        // 5. 设置完成回调(iOS 13+)
        if #available(iOS 13.0, *) {
            activityViewController.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
                if completed {
                    print("分享完成,类型:\(activityType?.rawValue ?? "未知")")
                } else {
                    print("分享取消或失败")
                }
            }
        }
        
        // 6. 显示分享界面(在iPad上需要设置popoverPresentationController)
        if let popoverController = activityViewController.popoverPresentationController {
            popoverController.sourceView = self.view
            popoverController.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0)
            popoverController.permittedArrowDirections = []
        }
        
        self.present(activityViewController, animated: true, completion: nil)
    }
    
    // MARK: - 分享图片示例
    func shareImage() {
        guard let image = UIImage(named: "example_image") else {
            print("图片未找到")
            return
        }
        
        let activityViewController = UIActivityViewController(
            activityItems: [image],
            applicationActivities: nil
        )
        
        // 可以添加自定义的分享活动(见高级部分)
        // activityViewController.applicationActivities = [CustomShareActivity()]
        
        self.present(activityViewController, animated: true)
    }
    
    // MARK: - 分享文件示例
    func shareFile() {
        // 1. 创建临时文件
        let tempDir = FileManager.default.temporaryDirectory
        let fileURL = tempDir.appendingPathComponent("report.pdf")
        
        // 2. 创建示例PDF内容(实际项目中可能需要生成或下载)
        let pdfContent = "这是一个PDF文档的内容"
        do {
            try pdfContent.write(to: fileURL, atomically: true, encoding: .utf8)
            
            // 3. 分享文件
            let activityViewController = UIActivityViewController(
                activityItems: [fileURL],
                applicationActivities: nil
            )
            
            self.present(activityViewController, animated: true)
        } catch {
            print("文件创建失败: \(error)")
        }
    }
}

2.2 使用UIPasteboard实现剪贴板分享

适用于简单文本或数据的快速分享,用户可以手动粘贴到其他应用。

import UIKit

class PasteboardShareViewController: UIViewController {
    
    // MARK: - 基本剪贴板操作
    func copyTextToPasteboard() {
        let text = "要复制的文本内容"
        
        // 使用UIPasteboard.general进行操作
        let pasteboard = UIPasteboard.general
        
        // 设置字符串
        pasteboard.string = text
        
        // 可选:设置过期时间(iOS 10+)
        if #available(iOS 10.0, *) {
            pasteboard.setItems([text], options: [.expirationDate: Date().addingTimeInterval(3600)]) // 1小时后过期
        }
        
        // 显示提示
        showToast(message: "文本已复制到剪贴板")
    }
    
    // MARK: - 分享图片到剪贴板
    func copyImageToPasteboard() {
        guard let image = UIImage(named: "example_image") else { return }
        
        let pasteboard = UIPasteboard.general
        
        // 设置图片
        pasteboard.image = image
        
        // 也可以设置多种格式
        pasteboard.setData(UIImage.jpegData(image, compressionQuality: 0.8)!, forPasteboardType: "public.jpeg")
        
        showToast(message: "图片已复制到剪贴板")
    }
    
    // MARK: - 监听剪贴板变化(高级功能)
    func monitorPasteboardChanges() {
        // 添加观察者监听剪贴板变化
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handlePasteboardChange),
            name: UIPasteboard.changedNotification,
            object: nil
        )
    }
    
    @objc func handlePasteboardChange() {
        let pasteboard = UIPasteboard.general
        
        if let string = pasteboard.string {
            print("剪贴板有文本: \(string)")
        }
        
        if let image = pasteboard.image {
            print("剪贴板有图片,尺寸: \(image.size)")
        }
        
        if let url = pasteboard.url {
            print("剪贴板有URL: \(url)")
        }
    }
    
    // MARK: - 自定义剪贴板类型(用于复杂数据)
    func copyCustomData() {
        let customData: [String: Any] = [
            "id": "12345",
            "name": "示例数据",
            "timestamp": Date().timeIntervalSince1970
        ]
        
        // 转换为Data
        if let jsonData = try? JSONSerialization.data(withJSONObject: customData, options: []) {
            let pasteboard = UIPasteboard.general
            pasteboard.setData(jsonData, forPasteboardType: "com.example.customdata")
            
            showToast(message: "自定义数据已复制")
        }
    }
}

2.3 使用UIDocumentInteractionController实现文件分享

适用于需要预览和分享文件的场景,特别是文档类文件。

import UIKit

class DocumentShareViewController: UIViewController, UIDocumentInteractionControllerDelegate {
    
    private var documentInteractionController: UIDocumentInteractionController?
    
    // MARK: - 分享文件并预览
    func shareDocument() {
        // 1. 准备文件URL(确保文件存在)
        guard let fileURL = Bundle.main.url(forResource: "example", withExtension: "pdf") else {
            print("文件不存在")
            return
        }
        
        // 2. 创建UIDocumentInteractionController
        let documentController = UIDocumentInteractionController(url: fileURL)
        documentController.delegate = self
        
        // 3. 设置选项
        documentController.uti = "com.adobe.pdf"  // 文件类型标识符
        documentController.name = "示例文档.pdf"
        
        // 4. 显示分享菜单(在指定视图上)
        let sourceView = self.view
        let sourceRect = CGRect(x: sourceView.bounds.midX, y: sourceView.bounds.midY, width: 0, height: 0)
        
        // 5. 显示选项菜单
        let presentResult = documentController.presentOptionsMenu(from: sourceRect, in: sourceView, animated: true)
        
        if !presentResult {
            print("无法显示选项菜单")
        }
        
        self.documentInteractionController = documentController
    }
    
    // MARK: - 仅预览文件
    func previewDocument() {
        guard let fileURL = Bundle.main.url(forResource: "example", withExtension: "pdf") else { return }
        
        let documentController = UIDocumentInteractionController(url: fileURL)
        documentController.delegate = self
        
        // 显示预览
        let sourceView = self.view
        let sourceRect = CGRect(x: sourceView.bounds.midX, y: sourceView.bounds.midY, width: 0, height: 0)
        
        documentController.presentPreview(animated: true)
    }
    
    // MARK: - UIDocumentInteractionControllerDelegate 方法
    func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
        return self
    }
    
    func documentInteractionControllerViewForPreview(_ controller: UIDocumentInteractionController) -> UIView? {
        return self.view
    }
    
    func documentInteractionControllerRectForPreview(_ controller: UIDocumentInteractionController) -> CGRect {
        return self.view.bounds
    }
    
    func documentInteractionControllerWillBeginPreview(_ controller: UIDocumentInteractionController) {
        print("即将开始预览")
    }
    
    func documentInteractionControllerDidEndPreview(_ controller: UIDocumentInteractionController) {
        print("预览结束")
    }
    
    func documentInteractionController(_ controller: UIDocumentInteractionController, willBeginSendingToApplication application: String?) {
        print("即将发送到应用: \(application ?? "未知")")
    }
    
    func documentInteractionController(_ controller: UIDocumentInteractionController, didEndSendingToApplication application: String?) {
        print("发送完成,目标应用: \(application ?? "未知")")
    }
}

三、高级分享功能实现

3.1 自定义分享活动(Custom Share Activity)

创建自定义的分享选项,扩展系统分享功能。

import UIKit

// MARK: - 自定义分享活动类
class CustomShareActivity: UIActivity {
    
    // MARK: - 属性
    private var activityItems: [Any] = []
    private var applicationActivities: [UIActivity]? = nil
    
    // MARK: - UIActivity 必需方法
    override var activityType: UIActivity.ActivityType? {
        return UIActivity.ActivityType("com.example.customshare")
    }
    
    override var activityTitle: String? {
        return "自定义分享"
    }
    
    override var activityImage: UIImage? {
        return UIImage(systemName: "star.fill")  // 使用SF Symbols
    }
    
    override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
        // 检查是否可以处理这些活动项
        for item in activityItems {
            if item is String || item is UIImage || item is URL {
                return true
            }
        }
        return false
    }
    
    override func prepare(withActivityItems activityItems: [Any]) {
        // 准备分享数据
        self.activityItems = activityItems
    }
    
    override func perform() {
        // 执行自定义分享逻辑
        print("执行自定义分享")
        
        // 处理分享数据
        for item in activityItems {
            if let text = item as? String {
                print("处理文本: \(text)")
                // 这里可以调用自定义的分享API
                shareTextToCustomService(text)
            } else if let image = item as? UIImage {
                print("处理图片,尺寸: \(image.size)")
                // 处理图片分享
                shareImageToCustomService(image)
            } else if let url = item as? URL {
                print("处理URL: \(url)")
                // 处理URL分享
                shareURLToCustomService(url)
            }
        }
        
        // 通知系统分享完成
        activityDidFinish(true)
    }
    
    // MARK: - 自定义分享方法
    private func shareTextToCustomService(_ text: String) {
        // 示例:调用自定义API
        let apiURL = URL(string: "https://api.example.com/share")!
        var request = URLRequest(url: apiURL)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body: [String: Any] = ["type": "text", "content": text]
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                print("分享失败: \(error)")
                return
            }
            print("分享成功")
        }.resume()
    }
    
    private func shareImageToCustomService(_ image: UIImage) {
        // 示例:上传图片到服务器
        guard let imageData = image.jpegData(compressionQuality: 0.8) else { return }
        
        let apiURL = URL(string: "https://api.example.com/upload")!
        var request = URLRequest(url: apiURL)
        request.httpMethod = "POST"
        
        // 创建multipart/form-data
        let boundary = "Boundary-\(UUID().uuidString)"
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        
        var body = Data()
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"image\"; filename=\"image.jpg\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
        body.append(imageData)
        body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
        
        request.httpBody = body
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                print("图片上传失败: \(error)")
                return
            }
            print("图片上传成功")
        }.resume()
    }
    
    private func shareURLToCustomService(_ url: URL) {
        // 示例:处理URL分享
        print("处理URL分享: \(url)")
        // 可以在这里实现URL缩短、统计等功能
    }
    
    // MARK: - 可选:自定义分享界面
    override var activityViewController: UIViewController? {
        // 返回自定义的分享视图控制器
        let customVC = CustomShareViewController()
        customVC.shareItems = activityItems
        return customVC
    }
}

// MARK: - 自定义分享视图控制器
class CustomShareViewController: UIViewController {
    var shareItems: [Any] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    private func setupUI() {
        view.backgroundColor = .systemBackground
        
        let label = UILabel()
        label.text = "自定义分享界面"
        label.textAlignment = .center
        label.font = .systemFont(ofSize: 24, weight: .bold)
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)
        
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
        
        // 添加分享按钮
        let shareButton = UIButton(type: .system)
        shareButton.setTitle("确认分享", for: .normal)
        shareButton.addTarget(self, action: #selector(confirmShare), for: .touchUpInside)
        shareButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(shareButton)
        
        NSLayoutConstraint.activate([
            shareButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            shareButton.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 20)
        ])
    }
    
    @objc private func confirmShare() {
        // 执行分享逻辑
        print("确认分享,内容: \(shareItems)")
        
        // 通知父控制器
        if let presentingVC = presentingViewController as? ShareViewController {
            // 处理分享完成
        }
        
        // 关闭界面
        dismiss(animated: true)
    }
}

3.2 Share Extension开发

Share Extension允许你的应用在其他应用的分享菜单中显示,是系统级的分享集成。

3.2.1 创建Share Extension Target

  1. 在Xcode中,选择File > New > Target
  2. 选择”Share Extension”
  3. 填写基本信息(Bundle Identifier等)

3.2.2 Share Extension配置

// Share Extension的Info.plist配置示例
/*
<key>NSExtension</key>
<dict>
    <key>NSExtensionAttributes</key>
    <dict>
        <key>NSExtensionActivationRule</key>
        <dict>
            <key>NSExtensionActivationSupportsTextWithMaxCount</key>
            <integer>1</integer>
            <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
            <integer>1</integer>
            <key>NSExtensionActivationSupportsImageWithMaxCount</key>
            <integer>1</integer>
            <key>NSExtensionActivationSupportsFileWithMaxCount</key>
            <integer>1</integer>
        </dict>
    </dict>
    <key>NSExtensionMainStoryboard</key>
    <string>MainInterface</string>
    <key>NSExtensionPointIdentifier</key>
    <string>com.apple.share-services</string>
</dict>
*/

3.2.3 Share Extension主控制器实现

import UIKit
import Social
import MobileCoreServices

class ShareExtensionViewController: SLComposeServiceViewController {
    
    // MARK: - 属性
    private var selectedImage: UIImage?
    private var selectedURL: URL?
    private var selectedText: String?
    
    // MARK: - 生命周期
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        loadSharedItems()
    }
    
    private func setupUI() {
        // 自定义标题
        title = "分享到我的应用"
        
        // 设置占位符
        placeholder = "添加分享内容..."
        
        // 自定义导航栏按钮
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            title: "发送",
            style: .done,
            target: self,
            action: #selector(sendTapped)
        )
    }
    
    // MARK: - 加载共享内容
    private func loadSharedItems() {
        guard let extensionContext = extensionContext else { return }
        
        for item in extensionContext.inputItems {
            guard let itemProvider = item as? NSItemProvider else { continue }
            
            // 检查文本
            if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
                itemProvider.loadItem(forTypeIdentifier: kUTTypeText as String) { [weak self] item, error in
                    if let text = item as? String {
                        DispatchQueue.main.async {
                            self?.selectedText = text
                            self?.textView.text = text
                        }
                    }
                }
            }
            
            // 检查URL
            if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
                itemProvider.loadItem(forTypeIdentifier: kUTTypeURL as String) { [weak self] item, error in
                    if let url = item as? URL {
                        DispatchQueue.main.async {
                            self?.selectedURL = url
                            self?.textView.text = url.absoluteString
                        }
                    }
                }
            }
            
            // 检查图片
            if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
                itemProvider.loadItem(forTypeIdentifier: kUTTypeImage as String) { [weak self] item, error in
                    if let image = item as? UIImage {
                        DispatchQueue.main.async {
                            self?.selectedImage = image
                            // 可以在这里显示图片预览
                        }
                    }
                }
            }
            
            // 检查文件
            if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeData as String) {
                itemProvider.loadItem(forTypeIdentifier: kUTTypeData as String) { [weak self] item, error in
                    if let url = item as? URL {
                        DispatchQueue.main.async {
                            // 处理文件
                            self?.handleFileURL(url)
                        }
                    }
                }
            }
        }
    }
    
    // MARK: - 处理文件URL
    private func handleFileURL(_ url: URL) {
        // 保存文件到应用容器
        let fileManager = FileManager.default
        let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.com.example.app")
        
        if let containerURL = containerURL {
            let destinationURL = containerURL.appendingPathComponent(url.lastPathComponent)
            
            do {
                try fileManager.copyItem(at: url, to: destinationURL)
                print("文件已保存到: \(destinationURL)")
            } catch {
                print("文件保存失败: \(error)")
            }
        }
    }
    
    // MARK: - 发送按钮处理
    @objc private func sendTapped() {
        // 验证内容
        guard let text = textView.text, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
            showError(message: "请输入分享内容")
            return
        }
        
        // 准备分享数据
        let shareData: [String: Any] = [
            "text": text,
            "timestamp": Date().timeIntervalSince1970,
            "hasImage": selectedImage != nil,
            "hasURL": selectedURL != nil
        ]
        
        // 保存到UserDefaults(主应用可以读取)
        let userDefaults = UserDefaults(suiteName: "group.com.example.app")
        userDefaults?.set(shareData, forKey: "shareData")
        userDefaults?.synchronize()
        
        // 完成扩展
        extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
    }
    
    // MARK: - 验证内容
    override func isContentValid() -> Bool {
        // 可以在这里添加验证逻辑
        let text = textView.text ?? ""
        return text.count > 0
    }
    
    // MARK: - 错误提示
    private func showError(message: String) {
        let alert = UIAlertController(title: "错误", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "确定", style: .default))
        present(alert, animated: true)
    }
}

3.3 跨应用数据共享(App Groups)

使用App Groups实现应用间数据共享,适用于需要在主应用和Share Extension之间传递数据的场景。

3.3.1 配置App Groups

  1. 在Xcode中,选择项目 > Targets > 你的应用 > Signing & Capabilities
  2. 点击”+ Capability”,添加”App Groups”
  3. 添加新的App Group标识符(格式:group.com.example.app)
  4. 在Share Extension Target中重复此步骤,使用相同的App Group标识符

3.3.2 实现数据共享

import Foundation

// MARK: - App Groups数据共享管理器
class AppGroupsManager {
    
    // MARK: - 单例
    static let shared = AppGroupsManager()
    private init() {}
    
    // MARK: - 常量
    private let appGroupIdentifier = "group.com.example.app"
    private let sharedDefaultsKey = "sharedData"
    
    // MARK: - 获取共享的UserDefaults
    private var sharedDefaults: UserDefaults? {
        return UserDefaults(suiteName: appGroupIdentifier)
    }
    
    // MARK: - 保存数据到共享区域
    func saveSharedData(_ data: [String: Any]) {
        guard let defaults = sharedDefaults else {
            print("无法访问共享的UserDefaults")
            return
        }
        
        defaults.set(data, forKey: sharedDefaultsKey)
        defaults.synchronize()
        
        print("数据已保存到共享区域")
    }
    
    // MARK: - 从共享区域读取数据
    func loadSharedData() -> [String: Any]? {
        guard let defaults = sharedDefaults else {
            print("无法访问共享的UserDefaults")
            return nil
        }
        
        return defaults.dictionary(forKey: sharedDefaultsKey)
    }
    
    // MARK: - 保存文件到共享容器
    func saveFileToSharedContainer(_ data: Data, fileName: String) -> URL? {
        guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
            print("无法获取共享容器URL")
            return nil
        }
        
        let fileURL = containerURL.appendingPathComponent(fileName)
        
        do {
            try data.write(to: fileURL)
            print("文件已保存到: \(fileURL)")
            return fileURL
        } catch {
            print("文件保存失败: \(error)")
            return nil
        }
    }
    
    // MARK: - 从共享容器读取文件
    func loadFileFromSharedContainer(_ fileName: String) -> Data? {
        guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
            return nil
        }
        
        let fileURL = containerURL.appendingPathComponent(fileName)
        
        do {
            let data = try Data(contentsOf: fileURL)
            return data
        } catch {
            print("文件读取失败: \(error)")
            return nil
        }
    }
    
    // MARK: - 监听共享数据变化(可选)
    func observeSharedDataChanges() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleSharedDefaultsChange),
            name: UserDefaults.didChangeNotification,
            object: nil
        )
    }
    
    @objc private func handleSharedDefaultsChange() {
        if let data = loadSharedData() {
            print("共享数据已更新: \(data)")
            // 处理更新的数据
        }
    }
}

四、常见问题与解决方案

4.1 分享界面显示问题

问题1:在iPad上分享界面显示异常

原因:UIActivityViewController在iPad上需要设置popoverPresentationController。

解决方案

func showShareSheetOniPad() {
    let activityViewController = UIActivityViewController(
        activityItems: ["分享内容"],
        applicationActivities: nil
    )
    
    // 设置popoverPresentationController
    if let popoverController = activityViewController.popoverPresentationController {
        // 设置源视图(通常是按钮或触发视图)
        popoverController.sourceView = self.view
        
        // 设置源矩形(通常是按钮的frame)
        let sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0)
        popoverController.sourceRect = sourceRect
        
        // 设置箭头方向(可选)
        popoverController.permittedArrowDirections = .any
        
        // 或者直接使用按钮作为源
        // popoverController.barButtonItem = navigationItem.rightBarButtonItem
    }
    
    self.present(activityViewController, animated: true)
}

问题2:分享界面在iPhone上显示不全

原因:内容过多导致界面超出屏幕。

解决方案

func showShareSheetWithLimitedItems() {
    let items = ["文本1", "文本2", "文本3", "文本4", "文本5"]
    
    let activityViewController = UIActivityViewController(
        activityItems: items,
        applicationActivities: nil
    )
    
    // 限制分享选项数量
    activityViewController.excludedActivityTypes = [
        .assignToContact,
        .print,
        .saveToCameraRoll,
        .addToReadingList,
        .postToVimeo,
        .postToFlickr,
        .postToWeibo,
        .postToTencentWeibo,
        .airDrop,
        .openInIBooks
    ]
    
    self.present(activityViewController, animated: true)
}

4.2 分享内容处理问题

问题3:分享图片时出现内存问题

原因:大尺寸图片直接分享导致内存溢出。

解决方案

func shareLargeImageSafely() {
    guard let originalImage = UIImage(named: "large_image") else { return }
    
    // 1. 压缩图片
    let maxSize: CGFloat = 1024  // 最大尺寸1024x1024
    let resizedImage = resizeImage(originalImage, to: CGSize(width: maxSize, height: maxSize))
    
    // 2. 降低质量
    guard let imageData = resizedImage.jpegData(compressionQuality: 0.7) else { return }
    
    // 3. 保存到临时文件(避免内存问题)
    let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("share_image.jpg")
    do {
        try imageData.write(to: tempURL)
        
        // 4. 分享文件URL而不是图片对象
        let activityViewController = UIActivityViewController(
            activityItems: [tempURL],
            applicationActivities: nil
        )
        
        self.present(activityViewController, animated: true)
    } catch {
        print("文件保存失败: \(error)")
    }
}

// 图片缩放函数
func resizeImage(_ image: UIImage, to size: CGSize) -> UIImage {
    UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
    image.draw(in: CGRect(origin: .zero, size: size))
    let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return resizedImage ?? image
}

问题4:分享文件时权限问题

原因:应用没有访问文件的权限。

解决方案

func shareFileWithPermissionCheck() {
    let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent("document.pdf")
    
    // 1. 检查文件是否存在
    guard FileManager.default.fileExists(atPath: fileURL.path) else {
        print("文件不存在")
        return
    }
    
    // 2. 检查文件是否可读
    guard FileManager.default.isReadableFile(atPath: fileURL.path) else {
        print("文件不可读")
        return
    }
    
    // 3. 如果文件在应用容器外,需要先复制到临时目录
    let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileURL.lastPathComponent)
    
    do {
        try FileManager.default.copyItem(at: fileURL, to: tempURL)
        
        // 4. 分享临时文件
        let activityViewController = UIActivityViewController(
            activityItems: [tempURL],
            applicationActivities: nil
        )
        
        self.present(activityViewController, animated: true)
    } catch {
        print("文件复制失败: \(error)")
    }
}

4.3 Share Extension相关问题

问题5:Share Extension无法获取主应用数据

原因:App Groups配置不正确或数据同步问题。

解决方案

// 在Share Extension中
class ShareExtensionViewController: SLComposeServiceViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 1. 验证App Groups配置
        let appGroupIdentifier = "group.com.example.app"
        let defaults = UserDefaults(suiteName: appGroupIdentifier)
        
        if defaults == nil {
            print("错误:无法访问共享的UserDefaults。请检查App Groups配置。")
            return
        }
        
        // 2. 从主应用读取配置
        if let config = defaults?.dictionary(forKey: "appConfig") {
            print("成功读取主应用配置: \(config)")
            // 使用配置
        } else {
            print("未找到主应用配置,使用默认值")
        }
        
        // 3. 保存数据供主应用读取
        let shareData: [String: Any] = [
            "extensionData": "来自扩展的数据",
            "timestamp": Date().timeIntervalSince1970
        ]
        defaults?.set(shareData, forKey: "extensionShareData")
        defaults?.synchronize()
    }
}

// 在主应用中
class MainViewController: UIViewController {
    
    func readExtensionData() {
        let appGroupIdentifier = "group.com.example.app"
        let defaults = UserDefaults(suiteName: appGroupIdentifier)
        
        if let extensionData = defaults?.dictionary(forKey: "extensionShareData") {
            print("读取到扩展数据: \(extensionData)")
            
            // 清理数据(可选)
            defaults?.removeObject(forKey: "extensionShareData")
            defaults?.synchronize()
        }
    }
}

问题6:Share Extension审核被拒

常见原因及解决方案

  1. 功能描述不清晰

    // 在Info.plist中添加清晰的描述
    <key>NSExtensionAttributes</key>
    <dict>
       <key>NSExtensionActivationRule</key>
       <dict>
           <key>NSExtensionActivationSupportsTextWithMaxCount</key>
           <integer>1</integer>
           <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
           <integer>1</integer>
           <key>NSExtensionActivationSupportsImageWithMaxCount</key>
           <integer>1</integer>
           <key>NSExtensionActivationSupportsFileWithMaxCount</key>
           <integer>1</integer>
       </dict>
    </dict>
    
  2. 隐私问题

    // 在Info.plist中添加隐私描述
    <key>NSUserActivityTypes</key>
    <array>
       <string>com.example.app.share</string>
    </array>
    
  3. 功能限制

    // 确保Share Extension只做分享,不做其他操作
    override func isContentValid() -> Bool {
       // 只允许分享文本、URL、图片、文件
       let text = textView.text ?? ""
       return text.count > 0
    }
    

4.4 性能优化问题

问题7:分享大量数据时卡顿

解决方案

class OptimizedShareManager {
    
    // MARK: - 异步处理分享数据
    func shareLargeDatasetAsync(_ items: [Any], completion: @escaping (Bool) -> Void) {
        DispatchQueue.global(qos: .userInitiated).async {
            // 1. 预处理数据
            let processedItems = self.preprocessItems(items)
            
            // 2. 在主线程显示分享界面
            DispatchQueue.main.async {
                let activityViewController = UIActivityViewController(
                    activityItems: processedItems,
                    applicationActivities: nil
                )
                
                // 设置完成回调
                activityViewController.completionWithItemsHandler = { _, completed, _, _ in
                    completion(completed)
                }
                
                // 显示界面
                if let viewController = UIApplication.shared.windows.first?.rootViewController {
                    viewController.present(activityViewController, animated: true)
                }
            }
        }
    }
    
    // MARK: - 数据预处理
    private func preprocessItems(_ items: [Any]) -> [Any] {
        var processedItems: [Any] = []
        
        for item in items {
            if let image = item as? UIImage {
                // 压缩图片
                if let compressedImage = compressImage(image) {
                    processedItems.append(compressedImage)
                }
            } else if let url = item as? URL {
                // 验证URL
                if url.isFileURL {
                    // 文件URL需要确保文件存在
                    if FileManager.default.fileExists(atPath: url.path) {
                        processedItems.append(url)
                    }
                } else {
                    processedItems.append(url)
                }
            } else {
                processedItems.append(item)
            }
        }
        
        return processedItems
    }
    
    // MARK: - 图片压缩
    private func compressImage(_ image: UIImage) -> UIImage? {
        let maxSize: CGFloat = 800
        let quality: CGFloat = 0.8
        
        // 缩放
        let size = image.size
        let scale = min(maxSize / size.width, maxSize / size.height, 1.0)
        
        if scale >= 1.0 {
            return image
        }
        
        let newSize = CGSize(width: size.width * scale, height: size.height * scale)
        
        UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0)
        image.draw(in: CGRect(origin: .zero, size: newSize))
        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        guard let finalImage = resizedImage else { return nil }
        
        // 压缩质量
        guard let jpegData = finalImage.jpegData(compressionQuality: quality) else { return nil }
        
        return UIImage(data: jpegData)
    }
}

五、最佳实践与性能优化

5.1 分享策略选择

根据不同的场景选择合适的分享方式:

场景 推荐方式 优点 缺点
简单文本/链接分享 UIActivityViewController 简单易用,支持多种渠道 界面固定,无法自定义
文件分享 UIDocumentInteractionController 支持预览,专业文件处理 界面较复杂
剪贴板分享 UIPasteboard 快速,用户可手动粘贴 需要用户主动操作
系统级集成 Share Extension 深度集成,用户体验好 开发复杂,审核严格
自定义分享 Custom Share Activity 完全自定义,灵活 需要自己实现所有逻辑

5.2 内存管理最佳实践

class MemoryOptimizedShareManager {
    
    // MARK: - 使用弱引用避免循环引用
    private weak var delegate: ShareDelegate?
    
    // MARK: - 及时清理资源
    func shareWithCleanup() {
        // 1. 创建分享活动
        let activityViewController = UIActivityViewController(
            activityItems: ["分享内容"],
            applicationActivities: nil
        )
        
        // 2. 设置完成回调
        activityViewController.completionWithItemsHandler = { [weak self] activityType, completed, returnedItems, error in
            // 3. 清理临时文件
            self?.cleanupTemporaryFiles()
            
            // 4. 释放强引用
            self?.delegate = nil
            
            // 5. 处理结果
            if completed {
                print("分享完成")
            }
        }
        
        // 6. 显示界面
        present(activityViewController, animated: true)
    }
    
    // MARK: - 清理临时文件
    private func cleanupTemporaryFiles() {
        let tempDir = FileManager.default.temporaryDirectory
        
        do {
            let contents = try FileManager.default.contentsOfDirectory(at: tempDir, includingPropertiesForKeys: nil)
            
            for fileURL in contents {
                // 删除超过1小时的临时文件
                if let attributes = try? fileURL.resourceValues(forKeys: [.creationDateKey]),
                   let creationDate = attributes.creationDate,
                   Date().timeIntervalSince(creationDate) > 3600 {
                    try? FileManager.default.removeItem(at: fileURL)
                }
            }
        } catch {
            print("清理临时文件失败: \(error)")
        }
    }
}

5.3 安全性考虑

class SecureShareManager {
    
    // MARK: - 分享前验证数据
    func shareWithValidation(_ items: [Any]) -> Bool {
        for item in items {
            if let string = item as? String {
                // 检查文本长度
                if string.count > 10000 {
                    print("文本过长")
                    return false
                }
                
                // 检查敏感信息(示例)
                if string.contains("密码") || string.contains("密钥") {
                    print("检测到敏感信息")
                    return false
                }
            } else if let url = item as? URL {
                // 验证URL
                if !isValidURL(url) {
                    print("无效的URL")
                    return false
                }
            } else if let image = item as? UIImage {
                // 检查图片大小
                if image.size.width > 5000 || image.size.height > 5000 {
                    print("图片尺寸过大")
                    return false
                }
            }
        }
        
        return true
    }
    
    // MARK: - URL验证
    private func isValidURL(_ url: URL) -> Bool {
        // 检查URL方案
        guard let scheme = url.scheme?.lowercased() else { return false }
        
        let allowedSchemes = ["http", "https", "ftp", "file"]
        if !allowedSchemes.contains(scheme) {
            return false
        }
        
        // 检查主机名
        if scheme == "http" || scheme == "https" {
            guard let host = url.host else { return false }
            
            // 简单的域名验证
            if host.isEmpty || host.contains("..") {
                return false
            }
        }
        
        return true
    }
}

六、调试与测试

6.1 调试技巧

// MARK: - 分享调试工具
class ShareDebugTool {
    
    // MARK: - 打印分享信息
    static func logShareInfo(_ items: [Any], activityType: UIActivity.ActivityType?) {
        print("=== 分享调试信息 ===")
        print("时间: \(Date())")
        print("分享类型: \(activityType?.rawValue ?? "未知")")
        print("分享内容:")
        
        for (index, item) in items.enumerated() {
            print("  \(index + 1). \(type(of: item)): \(item)")
            
            if let string = item as? String {
                print("     文本长度: \(string.count)")
            } else if let image = item as? UIImage {
                print("     图片尺寸: \(image.size)")
            } else if let url = item as? URL {
                print("     URL: \(url)")
            }
        }
        
        print("====================")
    }
    
    // MARK: - 模拟分享测试
    static func simulateShareTest() {
        let testItems: [Any] = [
            "测试文本",
            URL(string: "https://www.example.com")!,
            UIImage(systemName: "star.fill")!
        ]
        
        let activityViewController = UIActivityViewController(
            activityItems: testItems,
            applicationActivities: nil
        )
        
        activityViewController.completionWithItemsHandler = { activityType, completed, returnedItems, error in
            if let error = error {
                print("分享错误: \(error)")
            } else {
                print("分享完成: \(completed), 类型: \(activityType?.rawValue ?? "未知")")
            }
        }
        
        // 显示分享界面
        if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
           let window = windowScene.windows.first,
           let rootViewController = window.rootViewController {
            rootViewController.present(activityViewController, animated: true)
        }
    }
}

6.2 单元测试示例

import XCTest
@testable import YourApp

class ShareManagerTests: XCTestCase {
    
    var shareManager: ShareManager!
    
    override func setUp() {
        super.setUp()
        shareManager = ShareManager()
    }
    
    override func tearDown() {
        shareManager = nil
        super.tearDown()
    }
    
    // MARK: - 测试文本分享
    func testShareText() {
        let expectation = self.expectation(description: "分享文本")
        let testText = "测试文本"
        
        shareManager.shareText(testText) { success in
            XCTAssertTrue(success, "文本分享应该成功")
            expectation.fulfill()
        }
        
        waitForExpectations(timeout: 5.0)
    }
    
    // MARK: - 测试图片分享
    func testShareImage() {
        let expectation = self.expectation(description: "分享图片")
        
        // 创建测试图片
        let size = CGSize(width: 100, height: 100)
        UIGraphicsBeginImageContext(size)
        UIColor.blue.setFill()
        UIRectFill(CGRect(origin: .zero, size: size))
        let testImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        guard let image = testImage else {
            XCTFail("无法创建测试图片")
            return
        }
        
        shareManager.shareImage(image) { success in
            XCTAssertTrue(success, "图片分享应该成功")
            expectation.fulfill()
        }
        
        waitForExpectations(timeout: 5.0)
    }
    
    // MARK: - 测试文件分享
    func testShareFile() {
        let expectation = self.expectation(description: "分享文件")
        
        // 创建测试文件
        let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("test.txt")
        let testContent = "测试文件内容"
        
        do {
            try testContent.write(to: tempURL, atomically: true, encoding: .utf8)
            
            shareManager.shareFile(tempURL) { success in
                XCTAssertTrue(success, "文件分享应该成功")
                
                // 清理测试文件
                try? FileManager.default.removeItem(at: tempURL)
                
                expectation.fulfill()
            }
        } catch {
            XCTFail("创建测试文件失败: \(error)")
        }
        
        waitForExpectations(timeout: 5.0)
    }
    
    // MARK: - 测试无效输入
    func testShareWithInvalidInput() {
        let expectation = self.expectation(description: "无效输入测试")
        
        // 测试空文本
        shareManager.shareText("") { success in
            XCTAssertFalse(success, "空文本分享应该失败")
            expectation.fulfill()
        }
        
        waitForExpectations(timeout: 5.0)
    }
}

七、总结

iOS分享接口开发涉及多个层面,从简单的UIActivityViewController到复杂的Share Extension,每种方式都有其适用场景。在实际开发中,需要根据应用需求选择合适的分享方式,并注意性能、安全性和用户体验。

关键要点回顾:

  1. 基础分享:UIActivityViewController是最简单的方式,适合大多数场景
  2. 文件分享:UIDocumentInteractionController提供专业的文件处理
  3. 剪贴板分享:UIPasteboard适合快速、简单的数据传递
  4. 系统集成:Share Extension提供深度系统集成,但开发复杂
  5. 自定义扩展:Custom Share Activity和App Groups提供灵活的自定义能力

最佳实践建议:

  • 始终考虑用户体验,避免过度分享
  • 注意内存管理,特别是处理大文件或图片时
  • 遵循苹果的审核指南,特别是Share Extension
  • 提供清晰的错误处理和用户反馈
  • 进行充分的测试,包括不同设备和iOS版本

通过本文的指南,你应该能够构建出功能完善、性能优异的iOS分享功能,满足各种复杂场景的需求。记住,优秀的分享功能不仅能提升用户体验,还能有效促进应用的传播和用户增长。