Sharing information between iOS app and an extension Jul 16 2019

If you decided to create an Extension for your app (maybe a Today Extension), you will quickly find out that you need to access data from your main app. And soon after, you will find out that you can’t access files in your main app directly from the extension. The process is not as easy as you thought when you started. The good news is that there are a few solutions, and they are not that complicated. In this post, I’ll show you how to use App Groups and embedded frameworks to create the bridge between your app and the extension.

Let’s start by understanding some of the parts involved in the extension/app problem.

A target’s scope

We know we can’t access the app’s files directly from our extension, that is because the app and the extension are different targets. Each target is a different product. A target could be an app, a framework, an extension, a test suite, etcetera. Because they are different targets they have different scopes, they can only access what is inside of their scope.

Having this separation has security benefits, a target can not interfere with another target’s data. We want to keep those benefits but also be able to share data when we want too. There are different ways to solve the information sharing between targets. It also depends on what do we want to share, data or code. I’ll show you one solution for each case, but remember there are more ways to solve this. The following is a summary of possible solutions on how to share data and code between targets:

That should give you more clarity on what we are going to do. Now let’s have a more detailed look at each, we’ll start with sharing data:

Sharing data

We can share data in many ways. We could, for example, share it by storing it on the UserDefaults, or by saving files to disk or using CoreData. Each target gets its own container, so everything is isolated, but there is also a way to create a shared container. A shared container is created using App Groups.

App groups

App Groups were created to share containers between apps(and targets). The data stored inside a shared container is accessible to any target that belongs to that group.

Let’s imagine we are building a Today Extension. We have an Array of Friends, and we want both our app and our Today Extension to have access to that data. In our case, we are going to store a JSON representation of our structure in a file inside the group’s container. And with that, both the app and the extension will have access to the same data.

First, we would need to activate the App Groups capability for both the app and the extension. You can activate App groups in the Capabilities tab inside your Project settings.

Activate App Groups capability

Now we need to create the group, use the convention of the reverse domain plus the group’s container name. In my example: group.com.rderik.sharefriends.

Create App Groups

Now go to the extension and do the same, activate the App Groups capability and add the group.

Once you’ve done that, the container will be accessible for both targets. Now you can access it using the FileManager. You can create a function to write the JSON representation to disk, like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
 func saveFriends(friends: [Friend]) {
        let documentsDirectory = FileManager().containerURL(forSecurityApplicationGroupIdentifier: "group.com.rderik.sharefriends")
       let archiveURL = documentsDirectory?.appendingPathComponent("rderikFriends.json")
        let encoder = JSONEncoder()
        if let dataToSave = try? encoder.encode(friends) {
            do {
                try dataToSave.write(to: archiveURL!)
            } catch {
                // TODO: ("Error: Can't save Counters")
                return;
            }
        }
    }

And you could use a function like the following to read form the JSON file and decode the data:

1
2
3
4
5
6
7
8
9
10
11
12
  func loadFriends() -> [Friend] {
        let documentsDirectory = FileManager().containerURL(forSecurityApplicationGroupIdentifier: "group.com.rderik.sharefriends")
        guard let archiveURL = documentsDirectory?.appendingPathComponent("rderikFriends.json") else { return [Friends]() }

        guard let codeData = try? Data(contentsOf: archiveURL) else { return [] }

        let decoder = JSONDecoder()

        let loadedFriends = (try? decoder.decode([Friend].self, from: codeData)) ?? [Friend]()

        return loadedFriends
    }

That solution is fine when you want to store data in files for persistance. If you want to use the UserDefaults to store key/value pairs, use init(suiteName:). For example:

1
let userDefaultsValue = UserDefaults(suiteName: "group.com.rderik.sharefriends")?.object(forKey: "someKey")

If you want to use CoreData to share data between targets, you could use the something similar to what we did before:

1
2
let documentsDirectory = FileManager().containerURL(forSecurityApplicationGroupIdentifier: "group.com.rderik.sharefriends")    
let archiveURL = documentsDirectory?.appendingPathComponent("rderikFriends.sqlite")

And then do all your regular CoreData setup.

Another case is if you wish to share Keychain items (as suggested by mpiwosal in the Reddit thread for this post). You need to activate the Keychain access groups entitlements, create a group and have the app and the extension be part of the same group. Now you can write and read to that Keychain access group.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//Adding to the Keychain group
var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
                            kSecAttrServer as String: server,
                            kSecAttrAccount as String: user.username,
                            kSecAttrAccessGroup as String: accessGroup, //this is the important part
                            kSecValueData as String: user.password.data(using: String.Encoding.utf8)!]
var status = SecItemAdd(query as CFDictionary, nil)
if status != errSecSuccess {
    let errmsg = SecCopyErrorMessageString(status, nil)
    print("Error: \(errmsg)")
} else {

  print("Succesful save")
}

//Reading from the keychain group
// Get the value from keychain
query = [kSecClass as String: kSecClassInternetPassword,
         kSecAttrServer as String: server,
         kSecAttrAccessGroup as String: accessGroup, //this is the important part
         kSecMatchLimit as String: kSecMatchLimitOne,
         kSecReturnAttributes as String: true,
         kSecReturnData as String: true]
var item: CFTypeRef?
status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != errSecItemNotFound else { throw KeychainError.noPassword }
// If reached here the data should be in `item`

Ok, now you have a few ways to share data between your apps. But how would you share the model Friends?

You need it for both your app and extension, to encode/decode the content of the file in the shared container. Le'ts figure out how to share code.

Sharing code

We could duplicate the Friend class, copy it to the extension, and then both targets will know how the model works. But this is inefficient, and it will cause trouble in the future, imagine maintaining the code in both places. You will have to remember that any new change should appear in both files.

There are a few possible solutions, let explore some of them.

Target Membership

We could keep the file in your app target and then set the Target Membership to also include the extension. You do this by going to the File Inspector (Option+Command+1) and selecting the extension in the Target Membership section:

Select Target Membership

The problem with this approach is that the compiled file exists in both targets. You keep your code in one place, but if you start doing this for many files, the size of your targets will increase, and we want to keep things slim. Another issue is discoverability. If you keep all your shared code in the same place, it will be easier for a new programmer to know it is “shared” code. If you keep files with different target memberships in the same place, a new programmer will have to check the Target Membership of every file to be sure.

Another elegant solution is to use a framework. We will put all our shared code inside the framework and have both targets include it.

Using a framework

A framework is a bundle that contains libraries, and other resources (i.e. images, sounds files, videos, sprites, etcetera) that can be included in any of your targets. In our case, we are going to create a framework that will be shared by the app and the extension.

First, create the framework by going to File > New > Target … and select framework, add a name and click finish. When we create a framework this way, the framework exists located inside your current project.

Create Framework

Inside the framework’s group create the new File, Friend.swift. The Friend.swift file will contain the model.

Create file inside framework

Make sure to create it inside your newly created framework group. Now you can create your Friend struct to hold your data model.

1
2
3
4
5
6
7
8
9
10
11
//  Friend.swift

import Foundation

public struct Friend: Codable {
    private(set) public var name: String;

    public init(name: String = "Anonymous"){
        self.name = name
    }
}

Now you have the framework created, and it contains our Friend model. We need to add it to our application and our extension so they can make use of it.

Go to your project’s settings, select your application target and scroll to the “Embedded Binaries”, there you can add our framework. Adding the framework to “Embedded Binaries” will automatically add the framework to the Linked Frameworks and Libraries section. We embed the binaries to the app target. Adding the binaries this way, the user doesn’t need to have the framework already installed on the computer, it’ll be embedded in our app.

Add framework to application target

Now we have to do the same to our Extension. There is no “Embedding binaries” option for extensions because the extension depends on the app and you can embed the binaries there.

Link framework to extension

With that in place, we can now import the framework in our target’s classes.

That is how you would share code between application and extension. In the example, we had the framework on the same project, but it could be a framework in its own project. It could even be a framework that you reuse in many other projects.

Ok, that was all I wanted to show you about how to share code.

Final Thoughts

When you build extensions, you’ll need a way to handle data sharing and code sharing between targets. In the Apple ecosystem, there are many ways to do the same thing. But the general idea is the same. You need a bucket that the targets can access and share data through it. Now you know how to accomplish this.

An important detail to keep in mind is that when we are sharing data using the filesystem, the data can be modified by a different process. If you are interested in this topic, search for concurrency, there is a lot of material to cover there.

Ok, that is it for this post, I hope this post is useful the next time you have to communicate between targets. Let me know if you have any questions or feedback.

Related topics/notes of interest:

1
2
let notificationCenter = NotificationCenter.default
        notificationCenter.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)

** There is no comment system yet, but you can send me a message on twitter @rderik or send me an email: derik[at]rderik[dot]com.