Skip to content

Instantly share code, notes, and snippets.

@nandorojo
Last active October 2, 2024 16:56
Show Gist options
  • Save nandorojo/4d464e8bc9864a13caade86bbdf5ce0d to your computer and use it in GitHub Desktop.
Save nandorojo/4d464e8bc9864a13caade86bbdf5ce0d to your computer and use it in GitHub Desktop.
How to create an iOS Widget with React Native (Expo / EAS)

First, copy the config plugin from this repo: https://github.com/gaishimo/eas-widget-example

You can reference my PRs there too (which, at the time of writing, aren't merged).

After adding the config plugin (see app.json) with your dev team ID, as well as a bundle ID, you can edit the widget folder to edit your code. Then npx expo run:ios (or npx expo run:android).

Workflow

After npx expo run:ios, open the ios folder, and open the file that ends in .xcworkspace in XCode. Make sure you have the latest macOS and XCode versions. If you don't, everything will break.

At the top of XCode, to the right of the play button, click the dropdown (likely to the left of a Simulator or your device) change the target to be widget.

Next, in the left panel, click into your app and open widget/widget.swift. Edit the file, click run, and it'll open on the simulator with the widget.

I watched ten minutes of this YouTube video to learn how widgets work: https://www.youtube.com/results?search_query=swiftui+widget

I also used GPT 4 and read the docs from Apple. Pretty simple.

The data fetching part happens in the TimelineProvider btw.

TLDR: Widgets are like a server-side rendered, static app. All data fetching / image fetching happens in TimelineProvider, which is like getStaticProps from Next.js, except that it runs on a schedule determined by Apple. The result gets passed to your entry view as an entry. You have to create the types for your entry (like TypeScript types, but evaluated at runtime.)

How to share data between React Native and your widget

First, we need to set up app groups in app.json:

{
  "ios": {
      "bundleIdentifier": "com.myapp.app",
      "supportsTablet": true,
      "entitlements": {
        "com.apple.security.application-groups": [
          "group.com.myapp.app.widget"
        ]
      }
    }
}

The entitlements should be an array with IDs. The ID here is important. It must start with group. for iOS. Here, I made it group.${bundleIdentifier}.widget. The text after group. is considered your groupName by Apple.

Next, you'll need a library to call the group functions. You can try this out, hopefully it works: https://t.co/UCgtzSR38g

I'm using this since I only need to target iOS for now: https://github.com/mobiletechvn/react-native-widget-bridge

Should be easy to turn that into an Expo Module if you're feeling creative and want to help out.

import React from "react"
import Constants from "expo-constants"
import { StyleSheet, Text, View } from "react-native"
// @ts-ignore
import Preferences from "react-native-shared-group-preferences"

const appGroupIdentifier = `group.${Constants.expoConfig?.ios?.bundleIdentifier}.widget`

export default function App() {
  return (
    <View style={styles.container}>
      <Text
        onPress={async () => {
          try {
            // apparently android needs to check permissions first
            // but i ommitted that
            await Preferences.setItem(
              "test",
              {
                username: "test",
              },
              appGroupIdentifier,
            )
          } catch (errorCode) {
            // errorCode 0 = There is no suite with that name
            console.log(errorCode)
          }
        }}
      >
        Send data
      </Text>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
})
@nandorojo
Copy link
Author

PS — I haven’t succeeded at reading data in UserDefaults in Swift UI sent from RN yet.

@fobos531
Copy link

PS — I haven’t succeeded at reading data in UserDefaults in Swift UI sent from RN yet.

@nandorojo Most of what you already mentioned is spot on, but the crucial part for being able to read from UserDefaults is to make sure that the Widget itself is also part of the app group. By default, the thing in expo's app config only adds this entitlement to the main app, but not to the other targets in the xcode project. Probably, what needs to be done in this case is for https://github.com/bndkt/react-native-widget-extension to get a PR where you could specify optionally an app group, and then the plugin would be responsible for assigning the targets it creates (Widget target) to the app group that was specified.

After that, you can do something like this to read data in the widget:

  1. In the widget's getTimeline method, fetch the main app's bundle identifier and instantiate a UserDefaults object, like so:
// Source: https://stackoverflow.com/a/27849695
 // Get main app's application bundle
      var bundle = Bundle.main
      if bundle.bundleURL.pathExtension == "appex" {
          let url = bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent()
          if let otherBundle = Bundle(url: url) {
              bundle = otherBundle
          }
      }
      
      
      let userDefaults = UserDefaults(suiteName: "group.\(bundle.bundleIdentifier!).widget")

After that, read whatever you want from the UserDefaults:

    let userDefaults = UserDefaults(suiteName: "group.\(bundle.bundleIdentifier!).widget")
        
    let totalContacts = userDefaults?.integer(forKey: "contactCount") ?? 0

And then, just pass this into the timeline entry and you're good to go. I tested this by rapidly changing the values from within my app (calling UserDefaults.set + reloadAllTimelines for the widget) and it worked perfectly.

@nandorojo
Copy link
Author

oh cool…that’s gotta be the issue. can this be done in the app config? or do i need to add the group to the widget’s info plist?

@fobos531
Copy link

It seems like it, but I think it's best if things like these are handled by the config plugin system. I've dabbled with this and made some solid progress, but I wasn't entirely successful, you can see more info here: bndkt/react-native-widget-extension#7

@Nasseratic
Copy link

Any success with any method?

@aronvsr
Copy link

aronvsr commented Aug 30, 2023

Thank you for this. I forked it and changed your App.tsx for my App.js. Works like a charm!

@pafry7
Copy link

pafry7 commented Sep 12, 2023

Hey everyone, I've created an example repository with widgets for android and iOS, sharing data and refreshing. https://github.com/pafry7/react-native-widget-sync

@GuidoGagliardini
Copy link

Hi everyone. I was able to create the widget and with deep link + expo-linking to create a "communication" between the widget and the app but i can't share whit info from app to widget or widget to app whit "react-native-shared-group-preferences". I think I'm missing a bridge to communicate between them.
Has anyone been able to solve this?
Any success with any method?

react-native: 0.72.6
expo: 49.0.13

@jamfromouterspace
Copy link

jamfromouterspace commented Feb 29, 2024

For anyone trying to figure out how to get App Group or Keychain Group entitlements to work on the widget – the template code is missing something. Note that im using expo-secure-store for keychain stuff, but I dont think it matters.

  1. Create a widget.entitlements file next to widget.swift and Info.plist in the widget folder. Fill it as follows:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.application-groups</key>
	<array>
		<string>group.your.bundle.identifier.data</string>
	</array>
	<key>keychain-access-groups</key>
	<array>
		<string>$(AppIdentifierPrefix)your.bundle.identifier.widget</string>
	</array>
</dict>
</plist>
  1. In withWidgetXCode.ts add this:
const BUILD_CONFIGURATION_SETTINGS = {
...
CODE_SIGN_ENTITLEMENTS: "widget/widget.entitlements",
...
}

and

const TOP_LEVEL_FILES = [
  "Assets.xcassets",
  "Info.plist",
  "widget.swift",
  // add this
  "widget.entitlements",
]

I will PR to the template when I get a chance, but for now this should do the trick.

@SarimGXR
Copy link

Hye, @nandorojo I have used your library

"@bittingz/expo-widgets": "^0.6.19"

But I got this Error

get_default_flags is deprecated. Please remove the keys from the use_react_native! function
[INSTALL_PODS] if you are using the default already and pass the value you need in case you don't want the default
[INSTALL_PODS] [!] Invalid Podfile file: undefined local variable or method `flipper_config' for #Pod::Podfile:0x000000013e8c04a8.
[INSTALL_PODS] # from /private/var/folders/b1/_257_kxx0t3cx8l7jd_64g040000gn/T/eas-build-local-nodejs/3f559959-be84-456d-83b8-533c803bf77e/build/ios/Podfile:56
[INSTALL_PODS] # -------------------------------------------
[INSTALL_PODS] # # Note that if you have use_frameworks! enabled, Flipper will not work if enabled
[INSTALL_PODS] > :flipper_configuration => flipper_config
[INSTALL_PODS] # )
[INSTALL_PODS] # -------------------------------------------
[INSTALL_PODS]
Error: pod install exited with non-zero code: 1
at ChildProcess.completionListener (/Users/sarimahmad/.npm/_npx/93986e4ba4ec11e8/node_modules/@expo/spawn-async/build/spawnAsync.js:42:23)
at Object.onceWrapper (node:events:633:26)
at ChildProcess.emit (node:events:518:28)
at maybeClose (node:internal/child_process:1105:16)
at Socket. (node:internal/child_process:457:11)
at Socket.emit (node:events:518:28)
at Pipe. (node:net:337:12)

@SarimGXR
Copy link

@nandorojo How to share data in Android ? I am able to successfully Launched Widget on IOS and Android using this repositary

https://github.com/gaishimo/eas-widget-example

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment