Введение
Межпроцессное взаимодействие в iOS - штука хитрая. Apple выстроила целую систему песочниц и ограничений, и просто так передать данные из одного приложения в другое не получится. Зато когда разберёшься, открывается масса возможностей: от банальной передачи изображений до выстраивания целых экосистем приложений. Давай разберем все основные способы обмена данными между приложениями (от URL Schemes до App Groups) с акцентом на безопасность и реальные проблемы, которые могут возникнуть. Покажу код, расскажу, где какой метод уместен, и объясню, как не наделать дыр в защите данных пользователя.
URL Schemes: простота с подвохом
URL Schemes - это самый очевидный способ запустить одно приложение из другого и передать ему какие-то данные. Механика простая: регистрируешь в Info.plist свою схему, например myapp://, и любое приложение может дёрнуть твоё по этому адресу.
// Открытие другого приложения
if let url = URL(string: "myapp://action?param=value") {
UIApplication.shared.open(url)
}В принимающем приложении ловишь это в AppDelegate или SceneDelegate:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
if url.scheme == "myapp" {
handleDeepLink(url)
}
}Проблема в том, что URL Schemes - это публичный интерфейс. Любое приложение может вызвать твою схему, и ты не узнаешь, кто именно. В начале своего пути как разработчика я как-то делал функцию оплаты через URL Scheme - передавал сумму и ID товара прямо в параметрах. Естественно, лид на ревью мне указал, что злоумышленник может подменить параметры и провести транзакцию на копейки. Пришлось переделывать: теперь передаю только токен, а сумму проверяю на бэкенде.
Никогда не доверяй данным из URL Scheme. Валидируй всё, что приходит, и не передавай чувствительную информацию открытым текстом.
Universal Links: цивилизованный подход
Universal Links появились в iOS 9, и это уже совсем другой уровень. Вместо кастомной схемы используется обычный HTTPS-домен. Когда пользователь тапает по ссылке, система сначала проверяет, есть ли у неё приложение, ассоциированное с этим доменом, и если есть - открывает приложение, а не Safari.
Настройка чуть сложнее. Нужен файл apple-app-site-association на твоём сервере:
{
"applinks":
{
"apps": [],
"details":
[
{
"appID": "TEAMID.com.example.app",
"paths": ["/action/*", "/promo/*"]
}
]
}
}Этот файл должен лежать в корне домена или в .well-known/apple-app-site-association и отдаваться по HTTPS без редиректов. Apple проверяет его при установке приложения.
В Xcode добавляешь Associated Domains capability:
applinks:example.comИ обрабатываешь точно так же, как URL Schemes, но через другой метод:
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return }
handleUniversalLink(url)
}Universal Links решают проблему безопасности на уровне инфраструктуры. Только владелец домена может настроить ассоциацию, поэтому подделать такую ссылку намного сложнее. Плюс они работают даже если приложение не установлено - пользова��ель просто попадёт на сайт.
Из минусов: настройка требует контроля над сервером, и если что-то пойдёт не так с сертификатом или файлом ассоциации, отлаживать будешь долго. Apple кэширует эти файлы, и обновление может занять часы.
Custom Document Types: обмен файлами
Иногда нужно не просто передать параметры, а поделиться файлом. Здесь в игру вступают UTI (Uniform Type Identifiers) и Document Types.
Нужно зарегистрировать в Info.plist типы документов, которые твоё приложение может открывать:
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>My Document</string>
<key>LSItemContentTypes</key>
<array>
<string>com.example.mydoc</string>
</array>
<key>LSHandlerRank</key>
<string>Owner</string>
</dict>
</array>И экспортируешь свой UTI:
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>com.example.mydoc</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>mydoc</string>
</array>
</dict>
</dict>
</array>Теперь, когда другое приложение отправляет файл через Share Sheet или Files, твоё приложение появится в списке.
Обработка происходит через тот же метод, что и URL Schemes:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
// Это security-scoped URL, работать с ним нужно аккуратно
guard url.startAccessingSecurityScopedResource() else { return }
defer { url.stopAccessingSecurityScopedResource() }
// Копируем файл в своё хранилище
let data = try? Data(contentsOf: url)
// Обрабатываем...
}Важный момент: файл, который тебе передали, находится в чужой песочнице. Система даёт временный доступ через security-scoped bookmark. Если тебе нужно сохранить файл надолго, копируй его в Documents или Cache своего приложения. И всегда проверяй содержимое - кто знает, что там на самом деле лежит.
App Groups: общие данные для своих
Когда у тебя несколько приложений или экстеншенов, и им нужно делиться данными напрямую, App Groups - это то что надо. Включаем capability в Xcode, создаём группу с идентификатором типа group.com.example.shared, и получаем общий контейнер для файлов и UserDefaults.
// Запись в shared UserDefaultsif
let sharedDefaults = UserDefaults(suiteName: "group.com.example.shared") {
sharedDefaults.set("some value", forKey: "sharedKey")
}
// Путь к общей директории
if let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.example.shared") {
let filePath = containerURL.appendingPathComponent("data.json")
try? someData.write(to: filePath)
}Это удобно для виджетов, клавиатурных расширений, Share Extensions. Например, у меня было приложение с виджетом. Основное приложение загружало данные и складывало в App Group, а виджет просто читал оттуда.
Но есть нюанс: App Groups не дают синхронизации между процессами. Если оба приложения одновременно пишут в один файл, получишь data race. Нужно либо использовать блокировки через NSFileCoordinator, либо организовывать обмен через уведомления.
// Уведомляем другое приложение об изменениях
extension Notification.Name {
static let dataUpdated = Notification.Name("com.example.dataUpdated")
}
// В приложении-источнике
NotificationCenter.default.post(name: .dataUpdated, object: nil)
// В приложении-получателе (или extension)
CFNotificationCenterAddObserver(
CFNotificationCenterGetDarwinNotifyCenter(),
Unmanaged.passRetained(self).toOpaque(), { _, observer, name, _, _ in
// Обрабатываем изменения
},
"com.example.dataUpdated" as CFString,
nil,
.deliverImmediately
)Darwin Notifications работают между процессами, но не передают payload. Это просто сигнал, что пора перечитать данные.
Keychain Sharing: для настоящих секретов
Если нужно расшарить токены, пароли или другие чувствительные данные между приложениями одного разработчика, есть Keychain Sharing. Работает способом похожим на App Groups, но для Keychain.
Нужно включить Keychain Sharing capability и указать группу (обычно совпадает с Team ID):
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "userToken",
kSecAttrAccessGroup as String: "TEAMID.com.example.shared",
kSecValueData as String: tokenData,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
]
SecItemAdd(query as CFDictionary, nil)Keychain автоматически шифруется системой, и данные защищены hardware encryption на устройствах с Secure Enclave. Это единственный нормальный способ хранить секреты в iOS.
Я всегда использую обёртку вроде KeychainAccess или пишу свою, потому что работать с Security framework напрямую - то ещё удовольствие. Но суть в том, что через kSecAttrAccessGroup твои приложения видят одни и те же записи.
Share Extension: интеграция в систему
Share Extension позволяет твоему приложению появляться в системном Share Sheet. Это мощный механизм для получения данных от других приложений - текста, картинок, ссылок, всего что угодно.
Создаёшь новый target типа Share Extension в Xcode, и получаешь стандартный ShareViewController:
class ShareViewController: SLComposeServiceViewController {
override func isContentValid() -> Bool {
// Валидация контента
return true
}
override func didSelectPost() {
guard let item = extensionContext?.inputItems.first as? NSExtensionItem,
let attachments = item.attachments else { return }
for attachment in attachments {
if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
attachment.loadItem(forTypeIdentifier: UTType.url.identifier) { url, error in
if let url = url as? URL {
// Сохраняем в App Group для основного приложения
self.saveSharedURL(url)
}
}
}
}
extensionContext?.completeRequest(
returningItems: [],
completionHandler: nil
)
}
}Extension работает в отдельном процессе с жёсткими ограничениями по памяти (обычно 120 МБ). Если превысишь, то система убьёт процесс без предупреждения. Поэтому тяжёлую обработку лучше отложить и делать в основном приложении.
Данные передаются через App Group, а основное приложение можно разбудить через URL Scheme или background fetch (предпочитаю этот способ). Я обычно сохраняю полученные данные в shared container, записываю флаг в UserDefaults, и при следующем запуске основное приложение забирает их на обработку.
XPC Services: для продвинутых
XPC (межпроцессное взаимодействие через mach ports) - это низкоуровневый механизм для общения между процессами в macOS и iOS. В iOS он доступен только для собственных приложений и экстеншенов, но даёт очень чёткий контроль.
Создаёшь протокол для коммуникации:
@objc protocol DataServiceProtocol {
func fetchData(completion: @escaping ([String]) -> Void)
}И реализуешь его в XPC Service:
class DataService: NSObject, DataServiceProtocol {
func fetchData(completion: @escaping ([String]) -> Void) {
// Какая-то тяжёлая работа
completion(["data1", "data2"])
}
}Это полезно, когда нужно изолировать опасный или ресурсоёмкий код. Например, парсинг ненадёжных данных можно вынести в отдельный процесс. Если он крашнется, основное приложение продолжит работать.
Правда, в повседневной разработке iOS-приложений XPC почти не используется. Это скорее для system-level задач или macOS. Но знать о нём полезно.
На что обратить внимание
Защита данных при обмене - это не только правильный выбор API. Вот несколько вещей, которые я усвоил на практике:
Валидация входных данных. Что бы ни приходило из другого приложения - проверяй формат, размер, содержимое. Особенно если это файлы. Никогда не знаешь, подсунут тебе 100 МБ картинку или замаскированный исполняемый файл.
Data Protection классы. Когда сохраняешь файлы в shared container, не забывай про атрибуты защиты:
try data.write(to: fileURL, options: .completeFileProtection)completeFileProtection означает, что файл зашифрован и недоступен, пока устройство заблокировано. Для большинства случаев это правильный выбор.
Минимизация прав. Если экстеншену не нужен доступ к фото - не запрашивай. Если можешь обойтись без background modes - обходись. Apple очень придирчиво смотрит на permissions в App Review.
Очистка временных данных. Share Extension и другие расширения должны убирать за собой. Не забывай удалять временные файлы после обработки:
defer {
try? FileManager.default.removeItem(at: tempURL)
}Что использовать и когда
Обычно выбор метода продиктован задачей:
Нужно просто открыть другое приложение с параметрами? URL Scheme достаточно, только не передавай ничего критичного.
Хочешь цивилизованную интеграцию с возможностью fallback на веб? Universal Links.
Работаешь с файлами, которые пользователь может отк��ыть из разных приложений? Document Types.
Свои приложения должны делиться данными? App Groups для файлов и UserDefaults, Keychain Sharing для секретов.
Хочешь, чтобы пользователь мог шарить контент в твоё приложение из любого места? Share Extension.
Я обычно начинаю с самого простого решения и усложняю только если нужно. Universal Links круче URL Schemes, но требуют больше настройки. App Groups удобны, но добавляют зависимость между приложениями. Всё это надо учитывать. Каждый из методов имеет свою нишу, и важно понимать не только как их использовать, но и какие угрозы они несут. Главное - всегда думай о том, кто и как может использовать твой интерфейс обмена данными. Валидируй, шифруй, ограничивай права. User data protection - это не галочка в чек-листе, а основа доверия пользователей к твоему приложению.
