Inter-process communication in iOS is a tricky business. Apple has built an entire system of sandboxes and restrictions, and you can't just transfer data from one app to another willy-nilly. But once you figure it out, a world of possibilities opens up: from basic image sharing to building entire app ecosystems. Let's break down all the main methods of data exchange between apps (from URL Schemes to App Groups) with a focus on security and real problems you might encounter. I'll show you code, explain where each method fits, and teach you how to avoid creating holes in user data protection.
URL Schemes: Simplicity with a Catch
URL Schemes are the most obvious way to launch one app from another and pass it some data. The mechanics are simple: you register your scheme in Info.plist, like myapp://, and any app can call yours at this address.
// Opening another app
if let url = URL(string: "myapp://action?param=value") {
UIApplication.shared.open(url)
}
In the receiving app, you catch this in AppDelegate or SceneDelegate:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
if url.scheme == "myapp" {
handleDeepLink(url)
}
}
The problem is that URL Schemes are a public interface. Any app can call your scheme, and you won't know who exactly. Early in my development career, I once made a payment function via URL Scheme - passing the amount and product ID right in the parameters. Naturally, my lead pointed out in code review that an attacker could substitute the parameters and conduct a transaction for pennies. Had to redo it: now I only pass a token and verify the amount on the backend.
Never trust data from URL Schemes. Validate everything that comes in, and don't pass sensitive information in plain text.
Universal Links: The Civilized Approach
Universal Links appeared in iOS 9, and it's a completely different level. Instead of a custom scheme, you use a regular HTTPS domain. When a user taps a link, the system first checks if there's an app associated with this domain, and if there is - opens the app instead of Safari.
Setup is a bit more complex. You need an apple-app-site-association file on your server:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.example.app",
"paths": ["/action/*", "/promo/*"]
}
]
}
}
This file must be at the domain root or in .well-known/apple-app-site-association and served over HTTPS without redirects. Apple verifies it when the app is installed.
In Xcode, you add the Associated Domains capability:
applinks:example.com
And handle it the same way as URL Schemes, but through a different method:
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else { return }
handleUniversalLink(url)
}
Universal Links solve the security problem at the infrastructure level. Only the domain owner can configure the association, so faking such a link is much harder. Plus they work even if the app isn't installed - the user simply lands on the website.
Downsides: setup requires server control, and if something goes wrong with the certificate or association file, you'll be debugging for a long time. Apple caches these files, and updates can take hours.
Custom Document Types: File Exchange
Sometimes you need not just to pass parameters, but to share a file. This is where UTI (Uniform Type Identifiers) and Document Types come into play.
You need to register in Info.plist the document types your app can open:
<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>
And export your 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>
Now, when another app sends a file through Share Sheet or Files, your app will appear in the list.
Handling happens through the same method as URL Schemes:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
// This is a security-scoped URL, need to work with it carefully
guard url.startAccessingSecurityScopedResource() else { return }
defer { url.stopAccessingSecurityScopedResource() }
// Copy the file to our own storage
let data = try? Data(contentsOf: url)
// Process...
}
Important point: the file you received is in someone else's sandbox. The system grants temporary access through a security-scoped bookmark. If you need to save the file long-term, copy it to your app's Documents or Cache. And always verify the contents - who knows what's actually in there.
App Groups: Shared Data for Your Own Apps
When you have multiple apps or extensions that need to share data directly, App Groups is what you need. Enable the capability in Xcode, create a group with an identifier like group.com.example.shared, and you get a shared container for files and UserDefaults.
// Writing to shared UserDefaults
if let sharedDefaults = UserDefaults(suiteName: "group.com.example.shared") {
sharedDefaults.set("some value", forKey: "sharedKey")
}
// Path to shared directory
if let containerURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.example.shared"
) {
let filePath = containerURL.appendingPathComponent("data.json")
try? someData.write(to: filePath)
}
This is convenient for widgets, keyboard extensions, Share Extensions. For example, I had an app with a widget. The main app would download data and put it in the App Group, and the widget would simply read from there.
But there's a nuance: App Groups don't provide inter-process synchronization. If both apps write to the same file simultaneously, you'll get a data race. You need to either use locks via NSFileCoordinator or organize exchange through notifications.
// Notifying another app about changes
extension Notification.Name {
static let dataUpdated = Notification.Name("com.example.dataUpdated")
}
// In the source app
NotificationCenter.default.post(name: .dataUpdated, object: nil)
// In the receiving app (or extension)
CFNotificationCenterAddObserver(
CFNotificationCenterGetDarwinNotifyCenter(),
Unmanaged.passRetained(self).toOpaque(),
{ _, observer, name, _, _ in
// Handle changes
},
"com.example.dataUpdated" as CFString,
nil,
.deliverImmediately
)
Darwin Notifications work between processes but don't carry a payload. It's just a signal that it's time to re-read the data.
Keychain Sharing: For Real Secrets
If you need to share tokens, passwords, or other sensitive data between apps from the same developer, there's Keychain Sharing. Works similarly to App Groups, but for Keychain.
You need to enable the Keychain Sharing capability and specify a group (usually matches the 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 is automatically encrypted by the system, and data is protected by hardware encryption on devices with Secure Enclave. This is the only proper way to store secrets in iOS.
I always use a wrapper like KeychainAccess or write my own, because working with the Security framework directly is quite an experience. But the point is that through kSecAttrAccessGroup your apps see the same records.
Share Extension: System Integration
Share Extension allows your app to appear in the system Share Sheet. This is a powerful mechanism for receiving data from other apps - text, images, links, anything.
You create a new target of type Share Extension in Xcode and get a standard ShareViewController:
class ShareViewController: SLComposeServiceViewController {
override func isContentValid() -> Bool {
// Content validation
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 {
// Save to App Group for main app
self.saveSharedURL(url)
}
}
}
}
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
}
Extensions run in a separate process with strict memory limits (usually 120 MB). If you exceed it, the system will kill the process without warning. So heavy processing is better postponed and done in the main app.
Data is passed through App Groups, and the main app can be woken up via URL Scheme or background fetch (I prefer this method). I usually save received data to the shared container, write a flag to UserDefaults, and on next launch the main app picks them up for processing.
XPC Services: For Advanced Users
XPC (inter-process communication via mach ports) is a low-level mechanism for communication between processes in macOS and iOS. In iOS it's only available for your own apps and extensions, but gives very precise control.
You create a protocol for communication:
@objc protocol DataServiceProtocol {
func fetchData(completion: @escaping ([String]) -> Void)
}
And implement it in the XPC Service:
class DataService: NSObject, DataServiceProtocol {
func fetchData(completion: @escaping ([String]) -> Void) {
// Some heavy work
completion(["data1", "data2"])
}
}
This is useful when you need to isolate dangerous or resource-intensive code. For example, parsing untrusted data can be moved to a separate process. If it crashes, the main app will continue working.
Truth is, in everyday iOS app development, XPC is almost never used. It's more for system-level tasks or macOS. But it's useful to know about.
What to Pay Attention To
Data protection during exchange isn't just about choosing the right API. Here are some things I've learned from practice:
Input data validation. Whatever comes from another app - verify the format, size, content. Especially if it's files. You never know if they'll slip you a 100 MB image or a disguised executable.
Data Protection classes. When saving files to a shared container, don't forget about protection attributes:
try data.write(to: fileURL, options: .completeFileProtection)
completeFileProtection means the file is encrypted and inaccessible while the device is locked. For most cases, this is the right choice.
Minimizing permissions. If an extension doesn't need photo access - don't request it. If you can do without background modes - do without. Apple is very picky about permissions in App Review.
Cleaning up temporary data. Share Extensions and other extensions should clean up after themselves. Don't forget to delete temporary files after processing:
defer {
try? FileManager.default.removeItem(at: tempURL)
}
What to Use and When
Usually the choice of method is dictated by the task:
- Just need to open another app with parameters? URL Scheme is enough, just don't pass anything critical.
- Want civilized integration with the ability to fallback to web? Universal Links.
- Working with files that users can open from different apps? Document Types.
- Your own apps need to share data? App Groups for files and UserDefaults, Keychain Sharing for secrets.
- Want users to be able to share content to your app from anywhere? Share Extension.
I usually start with the simplest solution and only complicate if needed. Universal Links are cooler than URL Schemes, but require more setup. App Groups are convenient, but add dependencies between apps. All this needs to be considered. Each method has its niche, and it's important to understand not only how to use them, but what threats they carry. The main thing - always think about who and how can use your data exchange interface. Validate, encrypt, restrict permissions. User data protection isn't a checkbox on a checklist, it's the foundation of user trust in your app.
Top comments (0)