Home iOS & Swift Tutorials

App Clips for iOS: Getting Started

In this tutorial, you’ll learn how to design and implement App Clips.

4.8/5 4 Ratings

Version

  • Swift 5, iOS 14, Xcode 12

At Apple’s Worldwide Developers Conference (WWDC) 2020, Apple announced App Clips: smaller, on-demand versions of apps that allow users to perform specific tasks.

App Clips are quite powerful, because they allow users who do not have your app to still use its functionality. From ordering a coffee to parking your car, App Clips have many great uses. Better yet, App Clips offer fantastic ways to discover new apps to try!

In this tutorial, you’ll create an App Clip experience for SwiftyLemonade, a simple app that allows you to buy lemonade using an App Clip. Along the way, you’ll learn:

  • What an App Clip is.
  • How to add an App Clip target.
  • How to share assets and code.
  • About App Clip experiences and how to make one.
  • How to confirm a user’s location using the Location Confirmation API.
  • Working with App Clip notifications.
Note: This tutorial assumes you know the basics of SwiftUI. If you’re new to SwiftUI, check out the SwiftUI: Getting Started tutorial first.
Note: You must have Xcode 12 installed to follow this tutorial. You can use the simulator for the majority of this tutorial, but testing the Location Confirmation API requires a device running iOS 14. To do this, you’ll need to update the bundle ID in the starter app before you begin following the tutorial’s instructions.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial. In the starter project, you’ll find SwiftyLemonade, an app that displays a list of lemonade stands at various Major League Soccer (MLS) stadiums. Build and run to check out the app:

List of lemonade stands

The app shows a list of Swifty’s Lemonade Stands located at various MLS stadiums. You can also mark a stand as a favorite and view a list of your favorites in a separate tab:

List of favorite stands

To favorite a lemonade stand, long tap an item in the list:

Marking as favorite

From here, you can select a lemonade stand and order one of Swifty’s famous lemonades. It’s a real hit with soccer fans:

Ordering lemonade

In Xcode, take a look at the main files you will be working on:

  • LemonadeStand.swift contains a struct representing a lemonade stand and an array of stands to display in the app.
  • Lemonade.swift contains a struct representing lemonade and two menu arrays.
  • MenuList.swift displays the lemonade menu for the selected lemonade stand.
  • DetailView.swift displays the details of the selected lemonade.
  • StandList.swift displays a list of lemonade stands for selection. Here, you can long-press to favorite or unfavorite a stand.
  • StandTabView.swift is a TabView for displaying a full list of lemonade stands or lemonade stands marked as a favorite.
  • LemonadeStandLocations is a Swift package containing the locations of Swifty Lemonade stands.

In this tutorial, you’ll build an App Clip that brings you to the LA Galaxy menu to buy lemonade.

What Exactly Is an App Clip?

An App Clip is a lightweight version of an app that enables users to perform a specific task without installing the full version of the app. This allows the user to access the right parts of your app right when they need them. To launch an App Clip, you scan an NFC tag, QR code or App Clip code. This flow is called an App Clip experience.

If a user has your app installed, an App Clip experience will act as entry point into the app. For example, a Coffee franchise app might have an App Clip experience that, when scanned, goes to the menu of the coffee shop you are in. Or if the app is not installed, the associated App Clip Card gets downloaded from the App Store. The App Clip Card is then presented to the user launching this flow. As a developer, you can configure App Clip Cards using App Store Connect, but remember: They require a main app.

Note: If you would like to learn more about configuring your App Clip’s launch experience, check out Apple’s documentation on Configuring Your App Clip’s Launch Experience.

Adding an App Clip Target

First, add the App Clip target to the project and name it SwiftyLemonadeClip:

Adding App Clip target

Be sure to set Interface to SwiftUI and Life Cycle to SwiftUI App. Then, click Activate when prompted. A new group named SwiftyLemonadeClip gets added to the Project navigator:

SwiftyLemonadeClip group

Additionally, Xcode sets the name and bundle identifier for your App Clip. You might notice that the bundle identifier has .Clip as an extension:

SwiftyLemonade bundle identifier

Now that you have added the App Clip target, it’s time to test it out. Build and run:

Hello, world!

Wow! There’s not much going on here. In the next section, you’ll learn how to share code and assets between both the app and App Clip target.

Sharing Assets and Code Between Targets

With the project set up, you can start sharing assets and code from the App to the App Clip — after all sharing is caring!

Note: An App Clip and app can share quite a lot, but shouldn’t share sensitive information. If you’d like to learn about making data available to the App Clip’s corresponding app, check out Apple’s Documentation.

Sharing Code and Assets

Because App Clips are lightweight versions of main apps, there will be dependencies. Back in Xcode, click the SwiftyLemonadeClip target and add the LemonadeStandLocations Swift package as a dependency in the Frameworks, Libraries, and Embedded Content section. Your App Clip now has access to the locations of the lemonade stands:

Adding LemonadeStandLocations dependency

Next, share some Swift files. The App Clip will need to know information about lemonade stands. Click LemonadeStand.swift in the Project navigator and update the target membership in the File inspector to include SwiftyLemonadeClip:

Adding Lemonade.swift to App Clip target

After doing this, you should see a bunch of errors:

Cannot find type 'LemonadeStand' in scope error

Worry not! These errors will disappear once you add the remaining dependencies.

Update the target membership of the following files as you did for LemonadeStand.swift:

  • Lemonade.swift
  • MenuList.swift
  • DetailView.swift
  • OrderPlacedView.swift
  • StandList.swift
  • Assets.xcassets in the SwiftyLemonade group

Score! No more errors!

Designing the App Clip Experience

An App Clip experience is an entry point into your app that gets invoked using a URL. An app could have many App Clip experience URLs that lead to specific tasks. In this tutorial, you’ll launch an App Clip experience URL at one of Swifty’s lemonade stands, showing the menu to place an order.

To kick off, create a new Swift File under SwiftyLemonadeClip and name it SwiftyLemonadeClipModel.swift. Disable the SwiftyLemonade target because the new file only needs to be available to your App Clip:

Adding App Clip model

Then, inside SwiftyLemonadeClipModel.swift, add the following code under import Foundation:

class SwiftyLemonadeClipModel: ObservableObject {
  @Published var selectedStand: LemonadeStand?
}

Here you created a SwiftyLemonadeClipModel class that conforms to ObservableObject. You have also added a @Published property to notify your App Clip of a selected stand.

Next, in SwiftyLemonadeClipApp.swift, you must instantiate your model. Add the following property to the struct:

  @StateObject private var model = SwiftyLemonadeClipModel()

You now must supply this property to the child views of the App Clip. Still in SwiftyLemonadeClipApp.swift, replace body with the following:

var body: some Scene {
  WindowGroup {
    //1
    ContentView()
      .environmentObject(model)
  }
}

In the code above, you set model to be an environment object, making it available to the ContentView view sub-hierarchy. Next, you’ll work out which is the right stand for the clip to select.

Getting App Clip Experience Data

Data gets passed to an App Clip via a registered URL in App Store Connect. Registering a URL is outside the scope of this tutorial, but that doesn’t mean you’re on the losing team! To get data from a URL, you must configure your App Clip to do so.

To start, click the SwiftyLemonadeClip target, under the Signing & Capabilities tab, then add a new Associated Domain named appClips:swiftyLemonade.example:

Adding App Clips associated domain

Next, the App Clip must interpret the data. Back in SwiftyLemonadeClipApp.swift, replace body with the following to get the query items from the URL:

var body: some Scene {
  WindowGroup {
    ContentView()
      .environmentObject(model)
      .onContinueUserActivity(
        NSUserActivityTypeBrowsingWeb,
        perform: handleUserActivity) //1
   }
}

// 2
func handleUserActivity(_ userActivity: NSUserActivity) {
  //3
  guard 
    let incomingURL = userActivity.webpageURL,
    let components = URLComponents(
      url: incomingURL, 
      resolvingAgainstBaseURL: true),
    let queryItems = components.queryItems 
  else {
    return
  }

  //4
  guard 
    let latValue = queryItems.first(where: { $0.name == "lat" })?.value,
    let lonValue = queryItems.first(where: { $0.name == "lon" })?.value,
    let lat = Double(latValue),
    let lon = Double(lonValue) 
  else {
    return
  }

  //5
  print("Latitude: \(lat), Longitude: \(lon)")
}

Here’s what that does:

  1. Register a handler for NSUserActivityTypeBrowsingWeb. iOS invokes this handler when it encounters an App Clip experience URL.
  2. Process the URL data.
  3. Continue execution only if there is a URL containing queryItems
  4. Check whether there are queryItems named lat and lon and assigns these to latValue and lonValue. These items represent the latitude and longitude of the associated lemonade stand. If these values don’t exist, then it’s not valid for this App Clip experience. These values are of type String and you convert them to type Double.
  5. Print the lat and lon values to the console.

Simulating a Clip Launch

To test this, create a launch URL. This allows you to simulate launching the App Clip from an App Clip experience URL. To create one, set the active scheme to SwiftyLemonadeClip. Then, edit the scheme and enable the _XCAppClipURL Environment Variable by clicking the checkbox. Finally, set its value to https://swiftyLemonade.example.com/order?lat=33.8644&lon=-118.2611 as illustrated below:

Adding launch URL

The query parameters of the URL you added are lat and lon, and their values represent the latitude and longitude of the lemonade stand.

Now, build and run. You’ll see the latitude and longitude values get printed to console:

Latitude and longitude in console

What Lemonade Stand Is This?!

It’s time to find your nearest lemonade stand. Back in SwiftyLemonadeClipApp, under import SwiftUI, add the following:

import CoreLocation

Next, in handleUserActivity(_:), replace the print() you added earlier with the following code:

//1
let location = CLLocationCoordinate2D(
  latitude: CLLocationDegrees(lat),
  longitude: CLLocationDegrees(lon))

//2
if let stand = standData.first(where: { $0.coordinate == location }) {
  model.selectedStand = stand
  //3
  print("Welcome to \(stand.title)! :]")
}

This code:

  1. Creates a CLLocationCoordinate2D variable using the lat and lon values you got from the URL
  2. Queries standData to find the first value with a matching location. If you find a stand, set it as the selectedStand in SwiftyLemonadeClipModel.
  3. Prints the name of the stand to console

To check this out, build and run and to see a welcome message printed to the console:

Welcome to LA Galaxy

You’ve configured your App Clip to get data from a URL!

Happy!

Ordering Some Lemonade

It’s time to create the App Clip experience for SwiftyLemonade. In this section, you’ll take the selected stand and display its associated menu. A user will then be able to order some lemonade.

First, under SwiftyLemonadeClip, open ContentView.swift. Adding the following property in ContentView :

@EnvironmentObject private var model: SwiftyLemonadeClipModel

Here, you’ve added the model you created earlier as an environment object.

Next, replace body with the following:

var body: some View {
  //1
  if let selectedStand = model.selectedStand {
    //2
    NavigationView {
      //3
      MenuList(stand: selectedStand)
    }
  }   
}

This code:

  1. Checks whether the model has a selectedStand.
  2. Adds a navigation hierarchy with MenuList as the root view.
  3. Instantiates a MenuList for the selectedStand to display a list of menu items.

To check this out, build and run:

LA Galaxy lemonade menu

Now you’ve linked the model to the content view, the user can order a lemonade by scanning a URL code at Swifty’s LA Galaxy Lemonade Stand. App Clip Experiences should focus on a specific task such as ordering lemonade. As a result, notice the lack of a tab bar at the bottom of the screen and that there is no list of stands to choose from:

Ordering lemonade using app clip

Can’t Find a Lemonade Stand?

What if the app can’t find a lemonade stand for a location? It would be nice to show a message to the user. Open SwiftyLemonadeClipModel.swift and add the following property:

@Published var locationFound = true

This property tracks whether the app finds a lemonade stand location. It’s true by default, because it’s likely to find a lemonade stand.

Next, open SwiftyLemonadeClipApp and, in handleUserActivity(_:), add an else clause after the optional-binding code to find the lemonade stand:

else {
  model.locationFound = false
}

If there’s no lemonade stand at the location, set locationFound to false. You can also remove the print statement if you would like; this was only for debugging.

Now, back in ContentView.swift under SwiftyLemonadeClip, add the following to the end of the body:

if model.locationFound == false {
  Text("Error finding stand.")
}

If there’s no stand nearby, you display a nice message.

To test this, update the _XCAppClipURL to include an invalid latitude. Set the value to https://swiftyLemonade.example.com/order?lat=33.8644&lon=0.

Build and run to see the error message:

Lemonade stand not found

Great stuff! Now you’ve updated your App Clip to handle invalid lemonade stand locations. Before you continue, change _XCAPPClipURL back to a valid URL: https://swiftyLemonade.example.com/order?lat=33.8644&lon=-118.2611.

To wrap up this section, you have added an App Clip experience that gets launched by using a URL as a launch argument. This URL gives the location of the lemonade stand you are in and shows its menu. From here, you can place an order.

But what if there is a mix-up at the distribution center and the wrong tags get sent to the wrong lemonade stands? You could be ordering a lemonade at a different stand! Or worse, what if someone has placed an invalid tag at a store to commit fraud? To prevent this, Apple has introduced a new, lightweight Location Confirmation API, which you’ll learn about in the next section.

Setting up Location Confirmation

Along with App Clips, Apple introduced the Location Confirmation API. This framework provides enough information to verify that the invoked App Clip is at the expected location. The Location Confirmation API works by comparing the App Clip’s activation payload with the user’s location.

Note: You need a device running at least iOS 14 to test the Location Confirmation API. On the simulator you’ll just see an error message in the console.

In SwiftyLemonade, you’ll disable the option to place an order if you are not at the correct location. To prepare for this, open SwiftyLemonadeClipModel.swift and add the following property:

@Published var paymentAllowed = true

This property determines whether the user can order lemonade.

To set up location confirmation, open the Info.plist of the App Clip. Click the disclosure triangle next to the App Clip key to reveal the Requests location confirmation key. Change its value to YES:

Request location confirmation

Next, open SwiftyLemonadeClipApp.swift and, under import CoreLocation, add the following code:

import AppClip

This gives you access to the App Clip payload information to verify the user’s location.

Now, add the following at the bottom of handleUserActivity(_:) in SwiftyLemonadeClipApp.swift:

//1
guard let payload = userActivity.appClipActivationPayload else {
  return
}

//2
let region = CLCircularRegion(
  center: location, 
  radius: 500,
  identifier: "stand_location"
)

//3
payload.confirmAcquired(in: region) { inRegion, error in
  //4
  guard error == nil else {
    print(String(describing: error?.localizedDescription))
    return
  }

  //5
  DispatchQueue.main.async {
    model.paymentAllowed = inRegion
  }
}

This code:

  1. Gets the payload information from launching the App Clip. If it doesn’t exist, stop execution.
  2. Creates a region using the location found in the URL with a circular boundary of 500 meters. Core Location uses SI units for distance. If you’re not used to those units, a meter is just more than a yard.
  3. Checks whether the App Clip is activated within region
  4. If there’s an error, log it to the console for debugging.
  5. inRegion indicates whether the App Clip was invoked in the correct location. Use this value to enable or disable payments.
Note: If you’d like to learn about the App Clips Activation payload, check out Apple’s Documentation on APActivationPayload.

Using Custom Flags

Next, disable the ability to place an order if the user isn’t at the expected location. Open DetailView.swift, found in the Views group under SwiftyLemonade. This view allows you to order lemonade. Add the following code:

@EnvironmentObject private var model: SwiftyLemonadeClipModel

This code gives DetailView.swift access to the App Clip’s model. But this model is not available in the SwiftyLemonade target. If you select this scheme and try to build your app, you see the following error:

Cannot find type 'SwiftyLemonadeClipModel' in scope

To solve this, wrap this code in a conditional, using a Swift Compilation Flag. Open the Build Settings of your App Clip target and add the custom flag APPCLIP to both the Debug and Release schemes:

Adding custom flags

Back in DetailView.swift, wrap model in a conditional using the custom flag:

#if APPCLIP
@EnvironmentObject private var model: SwiftyLemonadeClipModel
#endif

Now, the code within the conditional will compile only when the App Clip scheme is selected. Build the app and see the errors disappear!

Next, the App Clip should display an alert that payment is disabled. To do this, add a property outside of the conditional:

@State private var showWarningAlert = false

This property determines whether to show the warning alert.

Disabling Ordering

Next, inside placeOrder(), add the following code before orderPlaced = true:

//1
#if APPCLIP
//2
guard model.paymentAllowed else {
  //3
  showWarningAlert = true
  return
}
#endif

This code:

  1. Executes the code inside the conditional only if the custom flag exists in the scheme.
  2. Checks if paymentAllowed is true. This means the App Clip can place an order.
  3. Sets showWarningAlert to true if payment is not allowed, and the rest of the code in placeOrder() is not executed.

Showing an Alert

To show the alert when placeOrder() gets called, add the following code inside body after the closing brace of the sheet view modifier:

//1
.alert(isPresented: $showWarningAlert) {
//2
  Alert(
    title: Text("Payment Disabled"),
    message: Text("The QR was scanned at an invalid location."),
    dismissButton: .default(Text("OK"))
  )
}

This code:

  1. Presents an alert if showWarningAlert is set to true
  2. Configures the alert to have a title, message and dismiss button. This alert warns the user that payment is not allowed.

One more step before you can test this out. Unless you live within 500 meters of Dignity Health Sports Park, the home of the LA Galaxy, you’ll need to simulate your location on a device. This allows you to place an order for lemonade without being anywhere near Swifty’s Lemonade Stand.

Simulating Location

To simulate being at the LA Galaxy stadium, create a GPX File in the SwiftLemonadeClip group and name it LaGalaxy.gpx. Be sure to select the SwiftLemonadeClip target:

Adding a GPX file

Open LaGalaxy.gpx and replace its contents with the following:

<?xml version="1.0"?>
<gpx version="1.1" creator="Xcode">
    <wpt lat="33.8644" lon="-118.2611">
        <name>Dignity Health Sports Park</name>
        <time>2014-09-24T14:55:37Z</time>
    </wpt>
</gpx>

The code you added represents the GPS coordinates of Dignity Health Sports Park.

Putting it all Together

Finally, to test each scenario, you must set a default location when building and running the app. To do this, you must edit the scheme for the App Clip and set the default location.

First, test the error flow where you are not at the expected location. To set a default location, select the SwiftyLemonadeClip scheme and then Edit Scheme:

Selecting default location as London

It’s time to check out this beautiful error alert you added. Build and run the app and try to place an order:

Payment disabled

Next it’s time to simulate being in L.A. Go back and edit the scheme and set the default location to the GPX file you created earlier:

Setting location as L.A.

Now, with the stadium’s GPS coordinates set in your device, you should be able to order some lemonade. Build and run:

Successful order

Congratulations! You’ve used the Location Confirmation API to verify the user’s location and prevent any misplaced orders.

Wouldn’t it be great if you could get notified when lemonade is ready? That way, you could go watch the game without missing any of the action. Never fear when ephemeral notifications are here!

Using Ephemeral Notifications

Like a main app, App Clips can receive notifications. These can add great value to App Clips by notifying you when your order is ready. App Clips can receive notifications only for a short time after getting launched — up to eight hours.

Open the Info.plist of your App Clip and enable ephemeral notifications — it’s in the same place as the location confirmation permission:

Enabling notification

This enables notifications for the App Clip, but the user can opt in on the App Clip Card. As a result, in SwiftyLemonadeClipApp.swift, add the following code below handleUserActivity(_:):

func requestNotificationAuthorization() {
  //1
  let notifCenter = UNUserNotificationCenter.current()
  notifCenter.getNotificationSettings { setting in
    //2
    if setting.authorizationStatus == .ephemeral {
      return
    }
    //3
    notifCenter.requestAuthorization(options: .alert) { result, error  in
      print("""
        Authorization Request result: \(result) \
        - \(String(describing: error))
        """)
    }
  }
}

This code:

  1. Retrieves the notification settings for the app
  2. Checks to see the if app is authorized to receive ephemeral notifications. If access is already granted, then there is no need to continue. But if access is not granted, then request it again.

Finally, while still in SwiftyLemonadeClipApp.swift, add the following code to the end of body. Be sure it appears inside the closing brace for WindowGroup:

//1
.onAppear {
  requestNotificationAuthorization()
}

This code calls requestNotificationAuthorization() when this view appears.

There you go! You don’t need to miss all the soccer action now, with notifications enabled.

Note: If you would like to learn more about App Clip notifications, check out Apple’s Documentation, or our Push Notifications Tutorial.

Where to Go From Here?

You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

In this tutorial, you learned how to:

  1. Add an App Clip target
  2. Share assets and code
  3. Use the Location Confirmation API to verify you are at the correct location
  4. Set up App Clip notifications

If you enjoyed this tutorial, check out SwiftUI by Tutorials. You’ll take a deep dive into how to define what your app’s UI should do with concise, declarative language, and say goodbye to tons of confusing UIKit code.

If you are interested in learning more about App Clips, check out Apple’s Documentation.

If you have any questions or comments, join the forum discussion below.

Average Rating

4.8/5

Add a rating for this content

4 ratings

More like this

Contributors

Comments