Sharing Data Between Applications and Extensions Using App Groups

If you ever needed to share data between your iOS apps and/or your application targets, there's a very easy way to do this using app groups.If you ever needed to share data between your iOS apps and/or your application targets, there’s a very easy way to do this using app groups. In this post we’ll see how to share data between two apps and we’ll even build a small library that will make sharing of data extremely simple. Last week, I wrote about monitoring files on the file system, this library will build on top of DAFileMonitor that I developed for that post. Let’s go…

Sandboxes, Entitlements andΒ Containers

As an iOS developer you already know that iOS apps are sandboxed. Just so we’re all clear, when we say an app is sandboxed, we mean that the access to it is restricted by the system. iOS is built on top of Unix and is using advanced user control on the kernel level to restrict access to your app. For example, if you need hardware resources, like camera, your app needs to ask for it. The same goes for access to the users’ data, like photos. Sandboxing is baked deep into the system and its main reason for existence is that your app can’t go gun-ho crazy and do a lot of damage. Sandboxing is there to protect the system and to protect the user. If your app starts doing damage, for what ever reason, it will only do damage in the sandbox.

You’ve probably seen a file in your project with the extension ‘entitlements’. This is a simple plist file with a list of all the system resources your app requested. We’ll be using this file in our examples.

When I hear a word ‘container’ I immediately think of a word ‘folder’ and that’s exactly what it is. It’s just a folder. Your application container is a special kind of folder created when your app is installed on a device. Your app will get a unique identifier that is different for each device and that unique identifier will be a part of the path to your container (just in case you were thinking you can count on the location of the container to remain fixed across devices). In your app container you have your documents directory, your caches directory, you would save your database there, etc. All the files you save on the file system within your app are all being saved somewhere in the app container.

So what if you want to share data between your apps? Then what?

App Groups

App group is a fancy Apple way of saying ‘shared folder’. If you think of it as a shared folder that all your apps and extensions can access you’d be bang on. You can only access your own app groups. There’s no way for you to access other developers’ app group. And that’s just fine. We’ll be using app groups in our example to pass data around. If you read the article on DAFileMonitor, you can see where I’m going with this πŸ™‚

BBPortal

This is the library we’ll be building here. I’ll cover most of the steps in the post. The library will enable apps and targets to communicate between each other using app groups. Let me show you two demo apps communicating side by side:

All of this might seem like dark magic to you, but it’s actually quite simple. What’s happening in the background is, apps are writing a file in the same app group and they’re using DAFileMonitor to listen for changes in the file. In effect, you create a magical portal where you can push data through and everyone who has the same portal opened will receive the data you’re pushing. Let’s get dirty and fire up Xcode!

The Code

BBPortal is built on top of DAFileMonitor and the code for it is quite simple. It essentially writes a dictionary in the shared container (the app group) and it listens for changes on the same file. Portals are identified by an ID, which is actually a file you write. BBPortalΒ works a lot like a subscribe-notify pattern. When you create your portal you’re implicitly subscribed and are listening to incoming data.

There’s only a handful of instance variables that we need:

public var onDataAvailable: ((Any) -> ())?
    
private let objectID = UUID()

private let groupID: String
private let portalID: String

private var coordinator: NSFileCoordinator?

private var portalQueue: OperationQueue?

When the data is available in the portal, we’ll call the ‘onDataAvailable’ closure. ‘groupID’ and ‘portalID’ are self-explanatory. We have an ‘objectID’ property that we’ll use to filter out the incoming notifications, so we don’t notify the subscriber about the data we pushed. We’ll use the ‘fileCoordinator’ and ‘portalQueue’ to write the dictionary on the file system.

The meat of the class is the ‘send’ method. You can pass Any object into it and it will be sent to other portals:

public func send(data: Any, onCompleted: ((NSError?) -> ())? = nil) {
	if let url = fileURL() {
		
		portalQueue?.addOperation {
			[weak self] in
			var error: NSError?
			self?.coordinator?.coordinate(writingItemAt: url, options: .forReplacing, error: &error, byAccessor: { (url) in
				
				let dictToSave: [String: Any?] = [DictKey.sender.rawValue: self?.objectID, DictKey.payload.rawValue: data]
				
				let dictData = NSKeyedArchiver.archivedData(withRootObject: dictToSave)
				try? dictData.write(to: url)
				
				onCompleted?(error)
			})
		}
	}
}

As you can see, there’s nothing special going on here. We simply add an operation to our operation queue and perform a write operation on it using a file coordinator (hm… come to think of it, there’s probably no need for the operation queue since write operations are coordinated anyway… oh well πŸ™‚ ) We create a dictionary here with the payload we’re supposed to save and the object ID. We’ll be using this object id to filter out our own writes in the file monitor.

When we allocated the class we set up our file monitor:

private var fileMonitor: DAFileMonitor?
private func startObservingFileChanges()
{
	guard let filePath = fileURL()?.path else {
		return
	}
	self.fileMonitor = DAFileMonitor(withFilePath: filePath)
	self.fileMonitor?.onFileEvent = {
		[weak self] in
		let dict = NSKeyedUnarchiver.unarchiveObject(withFile: filePath) as? [String: Any?] ?? [:]
		
		// Don't call the closure if you're the one sending the data through the portal.
		if let senderID = dict[DictKey.sender.rawValue] as? UUID, senderID != self?.objectID {
			guard let payload = dict[DictKey.payload.rawValue], payload != nil else {
				return
			}
			self?.onDataAvailable?(payload!)// Safe to force unwrap because of the guard.
		}
	}
}

As you can see, we’re using the DAFileMonitor library here to monitor the file for changes. When the file get’s changed, we read the dictionary from the file location, check if it’s our class who made the changes, and if it’s not us, get the payload. We, then, pass the payload to the closure.

Apart from a couple of utility functions, this is it. This is all you need to pass data between your apps and extensions. I created a quick demo for you, to show you how to use this library. Let’s go over it now.

How To Use It

We’ll create two simple demo apps. I’ll go over one of them, because, they’re mostly the same. If you want to use BBPortal in your project the first thing you’ll need is to enable App Groups. So go to your projects’ capabilities tab and enable them. If this is your first time creating a group, you will have an empty list, select the plus button at the bottom to add a new group (or container, as it’s called by Xcode):

Give it a good id. Something that will include your domain name, for example: group.agostini.tech.whatEver is a fine ID. When you’re finished adding a group you should see something like this:

Believe it or not, you’re all set for using BBPortal. This library is available through cocoapods, so I’ll assume you installed it. I set some basic UI components that I won’t get into here, I’ll just go over the specifics of using BBPortal in the view controller.

The Code

The little demo apps I created are sending a dictionary through one portal and an int through another one. This way the two apps can control each other, as shown below:

Let’s check the left view controller. We’ll go over the slider portal only:

private func setupSliderPortal() {
	sliderPortal = BBPortal(withGroupIdentifier: Constants.GroupID, andPortalID: Constants.SliderPortalID)
	sliderPortal?.onDataAvailable = {
		(data) in
		
		guard let dict = data as? [String: Any?], let value = dict[Constants.SliderKey] as? Float else {
			return
		}
		
		DispatchQueue.main.async {
			self.sliderLabel.text = "Received slider value: \(value)"
			self.slider.setValue(value, animated: true)
		}
	}
}

Creating the portal is quite simple. We just need the group ID that we created earlier and a portal ID. When anyone sends data through this portal the ‘onDataAvailable’ closure will get called. Here we’ll extract the data that the other app is sending and set the slider value.

Sending the data is quite simple as well:

@IBAction func sliderValueChanged(_ sender: UISlider) {
	sliderPortal?.send(data: [Constants.SliderKey: sender.value], onCompleted: { (error) in
		if let anError = error {
			print("Something went wrong when sending data: ", anError)
		}
	})
}

We simply pass in the dictionary and that’s it. The other app will receive this dictionary and set its slider value. Feels like magic, right πŸ™‚

Conclusion

Who would have thought that sharing data between apps and targets can be this simple πŸ™‚ I really enjoyed writing this article and creating BBPortal, I only hope you enjoyed reading it. I hope you got something useful out of this article. You can find all the examples on my GitHub profile.

Have a nice day πŸ™‚

Dejan.

More resources

22 thoughts on “Sharing Data Between Applications and Extensions Using App Groups

  1. Markus

    Hey Dejan, excellent article. I was just wondering if there are any limitations on the amount of data that can be passed through the portal to the other app?

    Reply
  2. shahad

    Hi dejan, great article :).
    I have one question though, Can I pass the location of apple watch to an iPhone app by using this method?

    Reply
  3. Sven

    Hi Dejan

    Nice and straight forward article.

    I have following requirement and wondering if this can be the solution.
    I would like to have one app downloading data in a container which can be accessed by other apps for update their repository and share data without iCloud. In order that the other apps do not have to connect to the server for updates one by one.

    Can you give me a hint?
    Thanks.

    Reply
    1. Dejan Agostini Post author

      Hi Sven,

      Thank you very much, I appreciate it πŸ™‚

      If you own all the apps that need to share the data (they have the same team id), then App Groups would be the simplest way of sharing data between your apps. If you don’t own all the apps that need to share data (or they don’t have the same team id), then you would have to use iCloud or some other cloud file storage. iOS doesn’t allow apps to write outside of the sandbox, as you know.

      Hope it helps and have a nice day πŸ™‚
      ~D;

      Reply
      1. Sven

        Thank you very much for your fast answer!

        Even though I do not own the apps, I have access to the developer who develop and sign the apps,
        In my understanding, it should be no problem to give all of them the same team id, right?
        It is not dependent of the signing process, right?

        S.

        Reply
  4. Shweta

    Hi,

    Can this be used with Objective C code. Actually I am trying to use this with my project which is in Objective – C but when I try to use BBPortal it says undefined.

    Reply
  5. Daniel

    Hi Dejan,
    I was trying out this code in a project and I ran into some errors. Some of the errors read “Use of unresolved identifier ‘Constants'” and “Use of unresolved identifier ‘sliderPortal’; did you mean ‘setupSliderPortal’?”. I then cloned your two demo projects from GitHub and I tried to use those, but I came across a “no such module ‘BBPortal’ error on both projects. I thought both would be easy fixes, but the podfiles are not working as they should be, and the dependencies can’t be accessed, so I have no way to test this code. Any suggestions?

    Reply
  6. Steven

    Hello Dejan,
    I have tested your two apps on one simulator and it worked. My question for you is, can I use the same approach and run the two apps on two different simulator instances and still have them connect? I am creating two apps and I want them to connect on different simulator instances.

    Thank you

    Reply
    1. Dejan Agostini Post author

      Hi Steven,

      Thanks for reading the blog, I appreciate it πŸ™‚

      Short answer, no. You can only pass the data locally on the same device. If you need to pass data across devices, consider using firestore/firebase, or something similar.

      You’re welcome and have a nice day πŸ™‚

      Reply
  7. Naly

    Hi Dejan,

    Should we concern the I/O issue? I think the approach will frequently read and write the shared file,

    Reply
  8. Piyush Bharadwaj

    Hi Dejan,

    Very good and straight to the point article.

    I have a very similar kind of requirement but between the apps with different Team IDs.

    I have to share a Token between apps. This Token is a Login token which is generated in my app (say ABC app) on successful login.
    Now I want that if some XYZ app (Not of my team, but we have a deal of sharing content) launches, it can access the Login token of ABC app.
    On successful validation of this Login token over the server, user will be able to access some premium content of XYZ app.

    Reply
  9. Slavcho

    Hi Dejan.
    Great article :))

    I want to make some little modification in my project. Because if I use CoreData instead of shared file, notifying all of the apps are quite tricky and difficult. So I was thinking instead of CoreData to use a single file, which will have a json structure according to my needs. But my concern is, instead of overwriting the file, I want to edit/update it every-time I receive new data. In that case, correct me if I’m wrong, I have to:
    1. Open the file
    2. Convert it to NSDictionary/Dictionary
    3. Add the item to the correct place, in the correct array
    4. Save the file.

    What do you think of this? Any opinion?

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.