Skip to main content
Back to Blog

Sharing Data Between Share Extension and App Swift iOS: How-To & Tips

Reading Time: 8 minutes
Communication between an App and its App extensions

When developing an application with some extension on iOS, a common issue is their intercommunication and reaction to changes made in one another.

Here at Fleksy, we are used to working around it as we develop device keyboards and want to provide all our help to those in need.

The main obstacle

Usually, when adding an app extension to your app project, there exists some one-way communication strictly related to the usage of the extension (i.e., for keyboards, you can access the proxy document settings about the text input), but that’s the best Apple can give to the developer user without getting into some more obscure documentation.

There is also the consideration that, even though an app extension’s bundle is nested within the hosting app’s bundle, they cannot access each other’s container directly, so sharing files and UserDefaults settings becomes troublesome.

index 2

Some tools in the shed

To be able to have proper communication between apps, we need at least two features:

  1. Transfer information
  2. Notify changes

Establishing this is the bare minimum for two-way communication.

Using App groups to share

You might be asking, what is an App Group? For those who haven’t got a clue, this is our first step to intertwine a hosting app and its extensions. Multiple apps and extensions can share a container or folder and other processes under the same app group.

We will first need to create an app group for both our targets, the hosting app, and the app extension. For this, go into Signing and Capabilities in your project settings and add an app group capability. After this, you can create one and add both to this group.

Now you can access your shared folder with the FileManager in the following way.

let sharedFolder: URL? = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: <Your-App-Group-Name>)

Over this sharedFolder we will be able to find anything we want our apps to have in common, even the UserDefaults settings dictionary they share, which is different from the one on each app. That’s right, and the standard UserDefaults can only be accessed by the app owning it. If we want to read on the shared one, we need to call it using the app group.

let localUserDefaults: UserDefaults = UserDefaults.standard
let sharedUserDefaults: UserDefaults? = UserDefaults(suiteName: <Your-App-Group-Name>)

This works similarly because the UserDefaults is no more than a .plist file saved on a container.

Establishing communication with Darwin notifications

We have one task remaining, to notify, and what’s better for that than a Notification Centre? I’m sure most of you know about the usual NotificafionCenter and how to use it to communicate different parts of your app that don’t have a direct connection.

The Darwin Notification Centre‘s main feature is to distribute notifications across the whole system, meaning you can communicate with other apps and, in our case, with app extensions. There are some differences between the two centers.

Default Notification CentreDarwin Notifications Centre
Notifies within the appNotifies across the system
Knows the object sending the notificationIs oblivious about who sends a notification
Knows who is observing the notification
Can send a userInfo dictionaryCannot send a userInfo dictionary
Can create and subclass multiple centersThere exists only one per App

As you can see, one major drawback in using Darwin notifications is the inability to send extra information when posting, but here is when we can use the shared folder for the App Group we mentioned earlier. Another one is the inability to see who is sending a notification, as other apps’ private information, such as classes and other objects, is only accessible to them.

You can use this center directly, but we recommend using a manager to ease this work and try to have a cleaner code. Of course, we will guide you a little bit with the job giving some basic direction on how to use and implement it.

Here is the minimum you need to set up the notification center

  • Notification structure
  • Notification center
    • Add notification observations.
    • Post notification to observers.
    • Remove notification from the observer.

Notifications

The notification should be able to recognize the object we are going to use as userInfo. In our case, we will call it a Payload. The payload will be saved as a string on a file we can read in our shared folder. Most structures and objects can be expressed as a dictionary, so they will not face many problems.

protocol DarwinPayload {
  // Read the Payload from a string
  init?(payloadString: String?)
  // Write the Payload to string
  func toString() -> String
}

struct DarwinNotification<Payload> where Payload: DarwinPayload {
  var name: Name
    
  struct Name: Equatable {
    fileprivate var rawValue: CFString
    var title: String { self.rawValue as String }
  }
    
  fileprivate init(_ name: Name) {
    self.name = name
  }
    
  init(_ rawValue: String) {
    self.name = Name(rawValue: rawValue as CFString)
  }
  
  init(_ cfNotificationName: CFNotificationName) {
    self.name = Name(rawValue: cfNotificationName.rawValue)
  }
  
  // Equatable is not applicable for generic class using different types
  func isEqual<Other>(to other: DarwinNotification<Other>) -> Bool {
    if Other.self == Payload.self,
      let otherAsPayload = other as? DarwinNotification<Payload> {
        return self.name == otherPlAsPayload.name
      }
    return false
  }
}

As an example, we will implement it like this:

extension String: DarwinPayload {
  init?(payloadString: String?) {
    guard let payloadString = payloadString else { return nil }
    self = payloadString
  }
  
  func toString() -> String {
    return self
  }
}

extension DarwinNotification {
  static var exampleDarwinNotification: DarwinNotification<String> {
    DarwinNotification<String>("EXAMPLE_DARWIN_NOTIFICATION")
  }
}

Notification Centre

The notification center should be able to handle all the observations we pass, read and write in the shared folder to send the payloads we saved. We recommend having all payloads under a subfolder to avoid conflicts. We will also have a specific queue for these notifications, so we don’t overload the main thread.

final class DarwinNotificationCenter {
  static let shared = DarwinNotificationCenter()
    
  private let center = CFNotificationCenterGetDarwinNotifyCenter()
    
  private let queue = DispatchQueue(label: "Darwin_queue", qos: .default, autoreleaseFrequency: .workItem)
    
  private typealias NotificationHandler = (String?) -> Void
    
  private var handlers = [String : NotificationHandler]()
    
  private init() {}
    
  private func getNotificationsFolder() -> URL? {
    // Return your payload folder under the shared one
  }
    
  private func payloadPath(forNotification name: String) -> String? {
    // Return the path for the payload assoicated with a notification
  }
    
  func addObserver<Payload: DarwinPayload>(_ notification: DarwinNotification<Payload>, using completion: @escaping ((Payload?) -> Void)) {
    self.queue.async {
      let name = notification.name
      let handler: NotificationHandler = {
        let payload = Payload(payloadString: $0)
        completion(payload)
      }
      self.handlers[name.title] = handler
      
      CFNotificationCenterAddObserver(self.center, Unmanaged.passRetained(self).toOpaque(), self.notificationHandler, name.rawValue, nil, .coalesce)
    }
  }
  
  func post<Payload: DarwinPayload>(_ notification: DarwinNotification<Payload>, payload: Payload) {
    self.queue.async {
      let name = notification.name
      guard let payloadFile = self.payloadPath(forNotification: name.title)
      else { return }
      do {
        try payload.toString().write(toFile: payloadFile, atomically: true, encoding: .utf8)
      } catch {
        // Manage your errors
      }
      CFNotificationCenterPostNotification(self.center, CFNotificationName(name.rawValue), nil, nil, false)
    }
  }
  
  func removeObserver<Payload: DarwinPayload>(_ notification: DarwinNotification<Payload>) {
    self.queue.async {
      let name = notification.name
      self.handlers[name.title] = nil
      guard let payloadPath = self.payloadPath(forNotification: name.title)
      else { return }
      
      if FileManager.default.fileExists(atPath: payloadPath) {
        do {
          try FileManager.default.removeItem(atPath: payloadPath)
        } catch {
          // Manage your errors
        }
      }
    }
  }
  
  // This is the actual callback when we register a notification
  private var notificationHandler: CFNotificationCallback {
    return { (_, observer, notfName, _, _) in
      guard let name = notfName?.rawValue as String?,
        let observer = observer
      else { return }
      
      let observedSelf = Unmanaged<DarwinNotificationCenter>.fromOpaque(observer).takeUnretainedValue()
      if let handler = observedSelf.handlers[name],
        let payloadPath = observedSelf.payloadPath(forNotification: name) {
        let content = try? String(contentsOfFile: payloadPath, encoding: .utf8)
        handler(content)
      }
    }
  }
  ...
}

Now all you need is to enable the different targets on the file inspector and try for yourself in the following way:

index 24
DarwinNotificationCenter.shared.addObserver(.exampleDarwinNotification) { _ in
  // Do something
}

DarwinNotificationCenter.shared.post(.exampleDarwinNotification, payload: "Posted")

DarwinNotificationCenter.shared.removeObserver(.exampleDarwinNotification)

Security and Privacy Considerations

When sharing data between your app and its extensions, it’s essential to handle sensitive information carefully to protect user privacy and comply with Apple’s guidelines. Here are some best practices to ensure security and maintain user trust:

  • Limit Shared Data: Only share data that is necessary for the extension’s functionality. Avoid storing sensitive user information in the shared container unless absolutely required.
  • Secure Coding Practices: Implement encryption for any sensitive data stored in the shared container. Utilize iOS security frameworks like Keychain Services to store confidential information securely.
  • Access Control: Ensure that only your app and its associated extensions have access to the shared app group container. Use proper entitlements and avoid exposing the container to other apps.
  • User Consent: Be transparent with users about what data is being shared between the app and its extensions. Update your privacy policy to reflect this sharing and obtain any necessary permissions.
  • Avoid Sensitive Data in UserDefaults: Refrain from storing sensitive information in UserDefaults, even within an app group. If you must store such data, make sure it’s encrypted and securely managed.
  • Handle Darwin Notifications Carefully: Since Darwin notifications are broadcast system-wide and lack sender identity, do not include sensitive data within them. Use notifications as triggers to perform actions, and retrieve any required data securely from the shared container.
  • Adhere to App Store Guidelines: Follow Apple’s privacy and data protection guidelines to prevent rejection during the app review process. Regularly review the App Store Review Guidelines for any updates.
  • Monitor and Audit Access: Keep track of access to the shared container and monitor for any unauthorized attempts to read or modify data. Implement logging mechanisms if appropriate.
  • Data Sanitization: Validate and sanitize all data read from the shared container to prevent injection attacks or corruption.
  • Regular Security Updates: Stay informed about the latest security advisories from Apple. Update your app and extensions promptly to address any vulnerabilities.

By integrating these security practices, you can ensure that data sharing between your app and its extensions is conducted safely. Prioritizing user privacy not only protects your users but also strengthens your app’s reputation and reliability in the market.

Common Issues and Troubleshooting Tips

Incorrect App Group Configurations

When setting up App Groups, misconfigurations can lead to data sharing issues. Ensure that both your app and its extension are added to the same App Group in the Apple Developer portal. Double-check that the App Group identifier (e.g., group.com.yourcompany.yourapp) is correctly entered in the entitlements files for both targets. Also, verify that the App Group capability is enabled under the Signing & Capabilities tab in Xcode for both the app and its extension.

Synchronization Challenges

Data written to shared containers or UserDefaults may not immediately reflect in the other component due to caching. To address this, explicitly synchronize UserDefaults after making changes using sharedUserDefaults?.synchronize(). However, be aware that synchronize() is deprecated and should be used cautiously. For file-based data, ensure that writes are completed before attempting to read from the other component. Implementing file coordination or using more robust synchronization mechanisms can help maintain data consistency.

Issues with Darwin Notifications

If Darwin notifications are not received as expected, confirm that the notification names match exactly between the sender and the receiver. Since these notifications do not carry a userInfo dictionary, all payloads must be managed through the shared container. Ensure that your notification observers are properly registered and that you maintain strong references to them; otherwise, they may be deallocated prematurely. Additionally, remember that Darwin notifications are not delivered when the app is in the background, so plan accordingly for background tasks.

Permission and Entitlement Problems

Accessing the shared container requires correct entitlements. Verify that both your app and its extension have the appropriate entitlements by checking the *.entitlements files. Ensure that your provisioning profiles are updated to include the App Group capability, and that they’re correctly assigned in Xcode. Misaligned entitlements or outdated provisioning profiles can prevent access to the shared resources.

Debugging Tips

  • Logging: Utilize logging to monitor the data flow and identify where the communication breaks down.
  • Shared Container Paths: Print out the paths to the shared container from both the app and the extension to ensure they point to the same location.
  • Thread Management: Be mindful of threading, especially when handling notifications. Dispatch to the main queue if necessary.
  • Testing on Real Devices: Simulators may not accurately replicate entitlement behaviors. Always test on actual devices to validate your implementation.
  • Monitor File Access: Use tools like File Activity tracing in Instruments to monitor file read/write operations in the shared container.

Conclusion

As we stated earlier, this is a basic implementation, and there is much more to do when managing these notifications. We used the same manager as the observer for our notifications to simplify the example. Still, one can try and handle the different observers for a notification, ceasing the observation when deallocated. We leave research work up to you to improve and expand your knowledge and skills. If you want to discuss this article or need help with your project, don’t hesitate to contact us directly or on our Discord server. Our team will be happy to help you.

Did you like it? Spread the word:

✭ If you like Fleksy, give it a star on GitHub ✭