Sharing Object Data Between an iOS App and Its Widget

Use Codable and App Groups to get your iOS 14 widget all the data it needs

Michael Kiley
6 min readFeb 2, 2021
Photo by David Martin on Unsplash

So you have your widget up and running, and you’re ready to provide some cool new functionality for your users using the latest iOS features. But how do you connect your widget with your app, so the widget can get at all the nicely modeled object data your app creates?

That’s what we’re here to make happen! Your main application can store object data by taking advantage of the Codable protocol, and you can give your widget and app shared access to this stored data by adding them to the same App Group. Let’s get started.

Make your data Codable

First, we need to make sure your app’s object data is ready to be stored. An easy way to accomplish this is by implementing Codable on the structs and classes that represent your data. Codable is a type alias for the Encodable and Decodable protocols. Conforming to it allows you to encode and decode your data into external representations like JSON using implementations of Decoder and Encoder, such as JSONEncoder and JSONDecoder.

Implementing Codable conformance on a type you have defined is as simple as adding the Codable type alias to its inheritance list and making sure all its properties are Codable! Standard library types like String and Int and Foundation types like Date and Data already implement Codable out of the box. For example, let’s say you have defined the following data type:

// A simple data type representing a Personstruct Person {
var name
var age
}

Since all of the properties of Person already implement Codable by default, declaring Codable conformance on Person is all that’s necessary:

// Person becomes Codable just by declaring conformancestruct Person : Codable {
var name : String
var age : Int
}

And what happens if you have a more complex data type, with properties of types that you have defined yourself, such as this Car struct?

// More complex data type with non-simple propertystruct Car {
var make : String
var model : String
var owner : Person
}

The same rules apply here — you can implement Codable on Car by making sure all its properties implement Codable.

// In order to make Car Codable, we just need to implement Codable on our Person type and declare Car as Codablestruct Car : Codable {
var make : String
var model : String
var owner : Person
}

When your data model conforms to Codable, you will be able to serialize it into JSON using JSONEncoder and write that JSON to storage. Before that, however, you will need access to storage shared between your app and widget, and that’s where app groups come in…

Create an App Group

Now that your data is ready to be written and read externally, we need access to shared storage for that data. We will accomplish this by creating an App Group and adding both the app and the widget to it. Normally, iOS isolates each app into its own container; App Groups are the operating system’s way of letting apps access shared containers and communicate through interprocess communication, which is exactly what you will need for your app and widget.

To create an App Group and add your app to it, first open your project settings by clicking on the .xcodeproj file for your app located at the root of your project in Xcode:

Select your project’s .xcodeproj file to open its settings

Then, select your app’s target and navigate to the Signing and Capabilities tab.

Navigate to Signing and Capabilites for your app’s target

Click the + Capability button in the upper left to add a capability, and double click on App Groups from the list that appears to add this capability to your app. You should now see the following added to your capabilities section:

Your added App Group capability

Clicking the + below Add containers here will allow you to add a new App Group container and give your App access to it. App Group containers should be named group.your_app’s_bundle_id. Once you have added it, it will appear on your list of App Groups — it may be in red font at first, but this will go away by hitting the refresh button next to the + below your list of App Groups. If you have done everything correctly, your Capabilities section will now show something like this:

After successfully adding an App Group container

Your app now has access to an App Group container accessible through the group identifier we assigned it, group.your_app’s_bundle_id. Next, we need to give your widget access to this same App Group container. This can be done by selecting your widget’s target from the left hand menu, going to its Signing and Capabilities tab, and adding App Groups to its capabilities list:

After giving your widget access to your new App Group container

The App Group container you added for your main application should show up automatically on your list of containers, and you can give your widget access to it by checking the check-box next to it.

Store and access shared data

Now, all we have to do is read and write our data to the shared storage made available to us by the App Group. Writing your data is as simple as converting your Codable data into JSON using JSONEncoder.encode() and writing it to UserDefaults using your App group:

/* Creating some data of our Codable type */
var person = Person(name: "John Deere", age: 24)
var car = Car(make: "Chevy", model: "Volt", owner: person)
/* Since it's Codable, we can convert it to JSON using JSONEncoder */
let carData = try! JSONEncoder().encode(car)
/* ...and store it in your shared UserDefaults container */
UserDefaults(suiteName:
"group.your_app_bundle_id")!.set(carData, forKey: "car")

The key data sharing portion of this code comes when we call UserDefaults(suiteName : String), which causes us to write to storage located in our shared App Group container instead of in the default UserDefaults storage only accessible by the process that owns it.

To read your data, you just need to reverse this process, first reading it from your App Group container’s UserDefaults storage and then deserializing it from JSON using JSONDecoder.decode():

/* Reading the encoded data from your shared App Group container storage */
let encodedData = UserDefaults(suiteName: "group.your_app_bundle_id")!.object(forKey: "car") as? Data
/* Decoding it using JSONDecoder*/
if let carEncoded= encodedData {
let carDecoded = try? JSONDecoder().decode(Car.self, from: carEncoded) if let car = carDecoded{
// You successfully retrieved your car object!
}
}

You can now use this same code in both your app and your widget, and they will be able to read and write your object data into shared storage.

Choosing to store data in UserDefaults is definitely an important decision in its own right, and I found this article taking a closer look at UserDefaults and this article discussing data storage options helpful in understanding the consequences. If you end up deciding to go with Core Data, you might find this guide to sharing a Core Data database using an App Group container useful; if you instead opt for FileManager, this Stack Overflow thread is a good start.

Let me know in the comments if you have any questions, or have developed any better practices for iOS extension data sharing — thanks!

Resources

Apple docs on Encoding and Decoding custom data types
Apple docs on adding App Groups and App Group identifiers
More detailed guide on using working with JSON and Codable

--

--

Michael Kiley

Software developer. Mobile, serverless, voice. Knows some things, curious about all things.