The App Opened Before It Was Installed: Building an iOS App Clip
The invite link had a problem I could not unsee. A user taps the share link you sent them. They do not have the app. Safari opens a web…
The App Opened Before It Was Installed: Building an iOS App Clip

The invite link had a problem I could not unsee. A user taps the share link you sent them. They do not have the app. Safari opens a web page.
~10 mins
They tap “Download on the App Store.” They wait for the download. They open the app. They go through onboarding.
By that point, six steps have passed since they were just trying to join a family on IngrediCheck.
An App Clip cuts that to one step. Tap the link, and a tiny version of the app opens instantly, no download required. The invite is already filled in.
They see the family name, they tap one button, and they are in. The full app install happens in the background, optionally, after.
I want to walk through how we built ours: what an App Clip actually is, what Apple restricts you from doing inside one, how to wire the invite code from the clip into the main app after install, and every screenshot you will need to submit it to App Store Connect.

A tiny app that lives inside your app
An App Clip is not a separate app. It is a second target inside your existing Xcode project, built from a subset of your code, and bundled inside your main app’s IPA when you submit to App Store Connect. Apple extracts it, hosts it on their CDN, and delivers it on demand when someone taps a qualifying link.
The clip has its own bundle ID. If your main app is llc.fungee.ingredicheck, your clip is llc.fungee.ingredicheck.Clip. It shares your Apple Developer account, your associated domain, and your App Store Connect listing, but it is a separate build target.
Apple limits a clip to 15 MB on disk. That limit forces good discipline: you ship only the one thing this entry point is for. In our case, that is the invite confirmation screen and a CTA to install the full app.

Setting up the Xcode target
In Xcode, go to File > New > Target, scroll down to App Clip, and click Next. Give it a name. Ours is IngrediCheckClip.

This creates a new folder in your project navigator with its own @main entry point, its own Info.plist, its own Assets.xcassets, and its own .entitlements file. None of your main app's SwiftUI views are included by default. You add only what you need.

Three things need to be wired before the clip does anything useful.
1. The entitlements on both targets
The clip needs two entitlements. First, the parent app identifier so Apple knows which full app it belongs to. Second, the associated domain for the URL that will trigger it.
<!-- IngrediCheckClip.entitlements -->
<key>com.apple.developer.parent-application-identifiers</key>
<array>
<string>$(AppIdentifierPrefix)llc.fungee.ingredicheck</string>
</array>
<key>com.apple.developer.associated-domains</key>
<array>
<string>appclips:www.ingredicheck.app</string>
</array>
The main app also needs the appclips: domain alongside its existing applinks: entry.
<!-- IngrediCheck.entitlements -->
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:www.ingredicheck.app</string>
<string>appclips:www.ingredicheck.app</string>
</array>
Notice the difference: applinks: is for Universal Links that open the installed app. appclips: is for triggering the clip when the app is not installed. Both can coexist on the same domain.

2. The App Clip experience in App Store Connect
Apple does not auto-trigger your clip on every URL. You have to register specific URL prefixes in App Store Connect under Features > App Clip Experiences. The URL you register is the prefix that will activate your clip.
For us that is https://www.ingredicheck.app/invite/, and any URL that starts with that prefix will launch IngrediCheckClip instead of opening Safari.

App Store Connect Console
At this point the clip can launch. The next question is how the invite code gets from Safari into the clip.
3. The Smart App Banner on the web page
On iOS, when a user lands on your invite page in Safari, the clip does not open automatically. Apple shows a Smart App Banner at the top of the page with your clip’s icon and a one-tap button. You opt into this by adding one meta tag to the page’s <head>.
// InvitePage.jsx: fires when platform is iOS
const meta = document.createElement('meta')
meta.name = 'apple-itunes-app'
meta.content = 'app-clip-bundle-id=llc.fungee.ingredicheck.Clip, app-id=6477521615'
document.head.appendChild(meta)
The app-clip-bundle-id tells Safari which clip to surface. The app-id tells it which full app to offer as the fallback. When the user taps the banner, iOS downloads the clip and launches it with the full URL as the user activity.

Receiving the URL inside the clip
When the clip launches, iOS passes the triggering URL as a NSUserActivityTypeBrowsingWeb activity. You catch it in your @main struct with .onContinueUserActivity.
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
guard let url = activity.webpageURL,
let code = Self.parseInviteCode(from: url)
else { return }
SharedInviteStorage.save(inviteCode: code)
inviteCode = code
}
parseInviteCode pulls the six-character code from the URL path, uppercases it, and validates it against a simple regex. The clip gets the invite code, validates it, and saves it.
But saves it where? The clip cannot write to the main app’s sandbox, because at this point the main app may not even be installed. That is what SharedInviteStorage solves.

The shared bridge between clip and app
An App Clip and the main app cannot talk directly. They run in separate sandboxes. But they can share a common App Group, which is a shared UserDefaults suite that both processes can read and write.
Both targets declare the same group in their entitlements.
<key>com.apple.security.application-groups</key>
<array>
<string>group.llc.fungee.ingredicheck</string>
</array>
Then a single enum, included in both targets, wraps the reads and writes. The interesting half is consumeInviteCode: it reads the stored value and immediately deletes it in one call, so the code can only be claimed once.
static func consumeInviteCode() -> String? {
let defaults = UserDefaults(suiteName: suiteName)
guard let code = defaults?.string(forKey: codeKey) else { return nil }
defaults?.removeObject(forKey: codeKey)
return code
}
If the main app launches and finds a code here, it knows the user came from a clip session and routes them directly into the invite flow.

IngrediCheck (main app)

IngrediCheckClip (clip app)
What the clip actually shows
Our clip is one screen. There is no navigation, no auth, no tab bar. It shows the family name the user was invited to join, a short list of what the app does, and an App Store overlay at the bottom that lets them install the full app without leaving the clip.

.appStoreOverlay(isPresented: $showOverlay) {
SKOverlay.AppClipConfiguration(position: .bottom)
}
SKOverlay is the standard Apple component for this. It slides up from the bottom of the clip and presents the install button. The user does not go to the App Store. They tap once, the download starts silently in the background, and they can keep reading the clip screen until the app is ready.
The family name comes from a quick async fetch against our invite preview endpoint. While it loads we show a shimmer placeholder built with a LinearGradient overlay that sweeps left to right. When the response comes back the name animates in with a combined opacity and scale transition. One edge case worth handling: if the user's URL arrives after the view has already appeared, onChange(of: inviteCode) triggers a fresh fetch and resets the shimmer so the old name never flashes before the new one loads.
When the full app installs and picks up the handoff
The clip saved the invite code to the shared App Group the moment the URL arrived. When the main app launches for the first time, it checks that storage before anything else.
// AppDelegate.swift — application(_:didFinishLaunchingWithOptions:)
if let code = SharedInviteStorage.consumeInviteCode() {
NSLog("[AppClip] Handoff invite code found: %@", code)
MainActor.assumeIsolated {
DeepLinkManager.shared.storePending(.joinFamily(inviteCode: code))
}
NotificationCenter.default.post(name: .deepLinkPending, object: nil)
} else {
DeferredDeepLink.checkOnFirstLaunch()
}
If a clip code is waiting, the main app routes it through DeepLinkManager exactly like a tapped Universal Link, and the invite code arrives pre-filled in the onboarding flow. If there is no clip code, it falls back to the deferred fingerprint check. The two paths are completely independent and do not interfere.
Screenshot to take here: The main app’s onboarding screen with the invite code already filled in, immediately after the user installs from the clip.

The things that silently broke
None of this worked the first time. Three issues cost us the most time.
The Smart App Banner did not appear for the first two days. The clip was configured, the URL was registered in App Store Connect, the meta tag was in the page. Safari still showed nothing.
The problem was propagation delay. App Store Connect pushes the App Clip Experience configuration to Apple’s CDN, and that can take up to 24 hours to show up in Safari on a real device. A simulator will never show the banner. You need a real iPhone and patience.
The handoff did not work in our first build. The clip saved the code to the App Group, the main app launched, and consumeInviteCode() returned nil.
The reason was a typo: group.llc.fungee.ingredicheck in the clip versus group.llc.fungee.IngrediCheck with a capital I in the main app. Both targets must declare the exact same string, case for case, or they write to different storage buckets that will never see each other's data.
The _XCAppClipURL environment variable did not fire onContinueUserActivity when we first set it. It turns out the variable must be set on the App Clip scheme, not the main app scheme. It sounds obvious but when you are new to the second target, it is easy to edit the wrong scheme and spend an afternoon wondering why the handler never triggers.
Testing without the App Store
You cannot test the clip through TestFlight easily, because the App Clip experience has to be live in App Store Connect before Safari will surface the Smart App Banner. But you can run the clip target directly from Xcode to a device.
Select the IngrediCheckClip scheme, choose your device, and hit Run. The clip launches but without a triggering URL, so the invite code field is empty.
To simulate a real URL, use the Xcode scheme editor. Under Run > Arguments, add an environment variable: _XCAppClipURL set to https://www.ingredicheck.app/invite/ABCDEF. Xcode passes this to the clip on launch and your onContinueUserActivity handler fires with that URL.
There is one more wrinkle. When testing on a physical device from Xcode (DEBUG build) or on TestFlight (sandbox receipt), we do not want to test with an empty invite code every time. So the clip seeds a default test code on non-production builds.
init() {
if Self.isTestingBuild {
SharedInviteStorage.save(inviteCode: "TST123")
_inviteCode = State(initialValue: "TST123")
} else {
_inviteCode = State(initialValue: "")
}
}
static var isTestingBuild: Bool {
#if DEBUG
return true
#else
return Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
#endif
}

The full picture
The clip is the lightest layer of the invite flow, and it is also the most visible one to new users. It is the first thing they see before they have ever opened the app. That is a strange responsibility for 15 MB of code.
What made it work is treating the clip as a pure display surface with exactly one job: show the family name, present the install button, and save the invite code to the shared bridge. No auth, no analytics, no navigation. The moment the full app installs, it picks up the handoff from that bridge and the user never notices where one ended and the other began.
The invite path that used to require six interactions now requires one. In our early internal testing, every tester who went through the clip completed the invite flow without dropping off at the App Store step, which had been the single biggest exit point before. The user who previously had to download, open, onboard, and then type a code now sees the family name before the app even exists on their phone. That is the transformation, and it is exactly what App Clips were designed for.
Before the App Clip, joining a family meant installing the app, navigating through onboarding, and finally entering an invite code you had to remember or paste. After the App Clip, the first screen a new user sees already knows which family invited them.
The code travels from a Safari link, through the clip, into the installed app without the user touching it once. That gap between “tapped a link” and “inside the app” is where most invite flows lose people. The clip closes it.
Eightinity Engineering We share real-world AI and mobile engineering insights. Before you go:
👏 Show your support by clapping and following the author 🧠 Discover more AI, OpenAI, and SAM model articles 📱 Explore iOS, Android, and UI engineering blogs 🚀 Learn how we build AI-powered products at Eightinity 🔔 Follow us: **LinkedIn | [X (Twitter)](https://x.com/8inityStudio) | [Website](https://www.eightinity.in/)**
메타데이터
- post_id
- 3ce4d2d897c4
- slug
- the-app-opened-before-it-was-installed-building-an-ios-app-clip-3ce4d2d897c4
- url
- https://medium.com/eightinity/the-app-opened-before-it-was-installed-building-an-ios-app-clip-3ce4d2d897c4
- canonical_url
- https://medium.com/eightinity/the-app-opened-before-it-was-installed-building-an-ios-app-clip-3ce4d2d897c4
- author_url
- https://medium.com/@gunjanw114
- status
- ok
- fetched_at
- 2026-07-09 09:01:30