It’s only been a couple weeks since Apple has released iOS 17. I’ve been building a React Native application in the six-figure MRR range for about 6 months now.
We’re at the point where native iOS widgets would be very useful to our users. One thing has been holding me back until iOS 17 though.
Interactive widgets!
After so many years of widgets, Apple has never allowed interactivity. You must open the app, and widgets refresh so slowly… about a max of 1 time per 15 minutes in my experience.
This all changes in iOS 17, we can now trigger interactive updates with refresh intents and more.
Our application lets users view things that update pretty frequently, as much as every few seconds, so having this interactive ability is very useful.
To make this even better, we do plan to add Live Activity support soon.
Key Problems
When you start attempting to add a widget extension to a React Native app, there are really two things you must learn.
The first one is learning Swift + SwiftUI so that you can code widgets to look and feel how you want.
The second issue is syncing data from your application into your Swift code. If you haven’t done this before, you might be wondering why this is necessary. For us, our widgets are dependent upon the logged in user, so we need to sync a cookie across to the native app extension or else it’s impossible to make any widgets.
Let’s get started by setting up the required native extensions, and a special container to sync our auth data from React Native.
For the first few weeks, my tutorials are for my paid subscribers only. You can subscribe for free and view it after that time. I put a lot of effort into these posts, so I hope you understand :)
Native Project Setup
If you are using bare React Native, without Expo, this is already done for you. However, if you are using Expo, you need to create a native project.
It’s very easy, you just need to run
npx expo run:ios
In your project directory. This will create an iOS folder. You can open the .xcworkspace file inside of this folder, using Xcode.
App Groups
Now that we have a native project, the very first thing we must do is create an App Group in Xcode.
Navigate to your main app target, and go to “Signing & Capabilities”
Click the “+Capability” button on the left side of this image. Search the drop down for “App Groups” and hit enter.
You will see a new section like this. Hit the plus button and create a new group name, you can see mine in the photo below.
This is the name of your group container.
Access your App Group from React Native
Normally, we would need to bridge some code for RN and make a package to do this. Thankfully someone else has already made a package that will allow us to store data in an App Group from React Native. This react-native-user-defaults package.
There is a little trick needed to ensure your Android RN build doesn’t break if you import this package. I believe the developer didn’t have an Android app and didn’t handle it internally, thankfully there is a work around for this.
To set it up we need to install it first:
npx expo install @alevy97/react-native-userdefaults
Then, since running this in Android will break our app, I made a new file called userDefaults.ts:
import { Platform } from "react-native";
export let groupDefaults: any = {};
// module breaks on android
if (Platform.OS === "ios") {
const UserDefaults = require("@alevy97/react-native-userdefaults");
groupDefaults = new UserDefaults.default("group.blah.blah");
}
This platform check and require call won’t run on Android.
Be sure to replace “group.blah.blah” with the name you gave your group in xcode.
At this point, you can store some stuff in this group. This is how I do it:
groupDefaults?.set?.("cookie", cookie);
We have the optional chain so that nothing will happen on Android. The moment users login, I grab the `Set-Cookie` header from the fetch call, and pass it to the app group library.
Add a Widget Extension
At this point, we can hit the “+” button at the bottom of “Targets” to add a new one:
Be sure to choose “Widget Extension.” Once you create it, we need to select the target and then add the App Group capability just like we did with the React Native app target.
Now your widget will be able to access the shared data.
Inside of a network request, or even SwiftUI directly, you can do an `if let` statement like this:
if let cookie = UserDefaults(suiteName: "group.blah.blah")?.string(forKey: "cookie") {
// Do stuff
}
In my application, I am calling a network request function inside of the getTimeline widget TimelineProvider implementation. That network request returns data, or an error enum, and that error enum has a value for .notLoggedIn.
This allows me to render a different SwiftUI view when the user’s cookie is empty in the App Group.
If you’d like me to dive deeper into this with more code samples, please let me know.
App Groups for WatchOS?
One caveat to these groups: they do not share to WatchOS, which is unfortunate. So you cannot share your widget code to a watch target and reuse it perfectly. However, there is a way to sync your auth data, using the watch connectivity framework. Depending on feedback I may do a tutorial on that soon.