DEV Community

irinaivanovaalex
irinaivanovaalex

Posted on

How to launch a React Native app from the Lock Screen on iPhone

Image description
With the release of iOS 18, it became possible to customize the Lock Screen. Naturally, this opens up opportunities to experiment and add interesting features to your React Native app. In this article, I'll explain how to make your app accessible directly from the Lock Screen.

Image description

Requirements

To accomplish this, you'll need:

Steps to Get It Working

1. Create a Widget Template

First, create a widget template using Xcode:

  • Go to Targets -> "+", and select a widget.
  • For this example, name it QScan. Xcode will create two widgets: a regular widget and a Control Widget.
  • The Control Widget is the one you can place on the Lock Screen.

In the Control Widget, you can choose between two types of interactions:

  • Button (ControlWidgetButton).
  • Toggle (ControlWidgetToggle).

For my use case, I chose the first option: ControlWidgetButton.

Here’s the implementation:

struct QScanWidgetControl: ControlWidget {
 static let kind: String = "QScanWidget"

 var body: some ControlWidgetConfiguration {
  StaticControlConfiguration(kind: Self.kind) {
   ControlWidgetButton(action: PerformAction()) {

   // Button with a name and a system icon
   Label("displayName", systemImage: "barcode.viewfinder")
   }
  }
  .displayName("displayName")
  .description("widgetDescription")
 }
}
Enter fullscreen mode Exit fullscreen mode
struct PerformAction: AppIntent {
 static let title: LocalizedStringResource = "displayName"

 // Determines if the app should automatically open when the intent is triggered.
 static var openAppWhenRun = true

 init() {}

 @MainActor
 func perform() async throws -> some IntentResult {
  guard let url = URL(string: "your deeplink") else {
  throw PerformError.invalidURL
  }

  await EnvironmentValues().openURL(url)
  return .result()
 }
}
Enter fullscreen mode Exit fullscreen mode

2. Add App Groups

For the Control Widget to open your app via deep links, you need to enable communication between the widget and the app. This is done using App Groups:

  • Enable App Groups in the Capabilities section for both the app and widget targets.

App Groups allow the widget, which operates in an isolated container, to share data with the app.

3. Configure Target Membership

To ensure the widget can interact with your app:

  • In the QScanWidgetControl.swift file, add the app target to Target Membership.

This step is crucial for proper deep link functionality.

Launching the Control Widget: 20 Minutes of Fun and Frustration

Image description

At first, everything seemed to work: the widget was added to the Lock Screen and launched the app. However, the deep link didn’t work when the app was closed. The app opened but displayed the home screen, as if the deep link wasn’t passed.

After some trial and error, I identified these challenges:

  1. Deep Links and Control Widget Don’t Cooperate If the app is closed, the widget fails to correctly pass the deep link. This happens because the widget operates in an isolated container and can only trigger the app launch. The deep link data is lost during initialization.
  2. Universal Links Are Not a Solution Universal Links seemed like the next logical step, as they are often recommended for Control Widgets. However, they had similar issues:
    • If the app is in the background, Universal Links work as expected and open the correct screen.
    • If the app is closed, only the home screen is displayed.

Besides, I wasn’t keen on using Universal Links, so I continued debugging to find a better approach.

My Solution (A Bit of Magic)

After hours of debugging, I realized the issue lay in the sequence of events:

  1. The widget sends a request to open the deep link.
  2. iOS launches the app first.
  3. Only then does iOS pass the deep link data.

At this point, the app might not be fully initialized, causing the deep link request to fail.

But! I found a workaround (it’s not perfect, but it works). By adding a small delay before triggering the deep link, the app has time to initialize, ensuring the deep link request is processed correctly.


struct PerformAction: AppIntent {
 static let title: LocalizedStringResource = "displayName"
 static var openAppWhenRun = true

 init() {}

 @MainActor
 func perform() async throws -> some IntentResult {
  guard let url = URL(string: "your deeplink") else {
   throw PerformError.invalidURL
  }

  try await Task.sleep(nanoseconds: 1_000_000_000)

  await EnvironmentValues().openURL(url)
  return .result()
 }
}
Enter fullscreen mode Exit fullscreen mode

Image description

Conclusion

This workaround solved the problem for me. If you have ideas for a more elegant solution, share them in the comments—we can brainstorm together!

P.S. You can download and test this feature on iOS here, in the ohmygoods.app.

Top comments (0)