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.
Table of Contents
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:
- If we want to share data: we can use the user defaults to share data between targets by adding them to the same
app group
. - If we want to share code: We have models and libraries that we would like our app and our app's extensions to understand and be able to use. If we keep our code in only one target, we won't be able to use it in the other. To share code, we could move the code to a framework. Then have both, the app and the extension, include the framework and get the same functionality. Using this approach gives us reusability and lowers our maintenance problems (we don't have to deal with duplicated code/functionality).
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.
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
.
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:
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.
Inside the framework's group create the new File, Friend.swift
. The Friend.swift
file will contain the model.
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.
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.
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:
- Apple's extension documentation
- Building a simple widget for the Today Screen, Apple documentation
- Embedding Framework in an App, Apple documentaiton
- How to detect when an app comes to the foreground. https://dev.to/sadra/how-to-detect-when-app-moves-to-background-or-foreground. I used this when I was coming back from the extension to the app. I knew that I had to reload the data because the extension might have changed the data. The code in the article is old, but the idea is the same. I used the following code:
1
2
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
It will be interesting to share messages between the app and the extension. StackOverflow possible solution using Notificaiton Center
UsersDefaults Apple documentation
Sharing Keychain items between multiple apps, Apple documentation. As suggested by mpiwosal on Reddit