iOS App Intents with Ionic

If you’re an iOS developer looking to take advantage of system-level features like Siri and Shortcuts, App Intents offer a simpler and more streamlined approach compared to the older Intents Extensions. While there are some restrictions to keep in mind, implementing App Intents requires less work and is easier than ever before.

In this post, I’ll show you how to create a basic App Intent that can be easily discovered by Siri and Shortcuts. Plus, I’ll provide a step-by-step guide on how to use this intent to navigate to an app built using the Ionic framework. Whether you’re a seasoned iOS developer or just getting started, this post will help you leverage the power of App Intents to create more intuitive and efficient iOS apps. So let’s dive in!

tldr; whole example is available here 

App intent target

Firstly, create a blank Ionic Capacitor project with an iOS framework. You can leave it as it is now and we’ll look at it later. Open iOS project in Xcode and add a new Target (File > New > Target )

As most of the examples are around cooking, I’ve named it Reservation Table Intent.

That will create you an App Intent target with two files: App Intent and App Intent Extension. We don’t need to make any changes to App Intent Extension. Let’s focus on ReserveTableIntent.swift

// ReserveTableIntent.swift

import AppIntents
import SwiftUI

struct ReserveTableIntent: AppIntent {
    static var title: LocalizedStringResource = "Reserve a table"
    static var description = IntentDescription("Reserve a table at restaurant")
    
    @MainActor
    func perform() async throws -> some IntentResult {
        return .result(
            view: ReserveTableIntentView()
        )
    }
}

title is the most important part. This is the phrase that will be used to find your App Intent. It should be quite unique so that Siri can correctly target it. It can also be parameterized.

If you want to feed it with data created in Ionic you can look at this post and check how to use App Groups.

If you want to see how to use parameters you can check the official documentation.

As mentioned in official documentation App Intents can result in may different ways starting from returning another intent, sending a content like CSV file or opening a dialog. We are using the last option with a custom view defined. Let’s implement this view now.

import Foundation
import SwiftUI

struct Table:Identifiable {
    var id: Int
    var name: String
    init(_ id: Int, _ name: String) {
        self.id = id
        self.name = name
    }
}

struct ReserveTableIntentView: View {
    var tables: [Table] = [
        Table(1, "For 2"),
        Table(2, "Family table"),
        Table(3, "Outdoor table")
    ]
    var body: some View {
        
        if #available(iOS 16.2, *) {
            VStack{
                Text("List of available tables")
                HStack {
                    ForEach(tables) { t in
                        HStack {
                            Spacer()
                            Link(destination: URL(string: "intentsexample://open-modal/\(t.id)")!){
                                Text(t.name)
                            }
                            Spacer()
                        }
                        .frame(minWidth: 40, minHeight: 40)
                        .background(.blue)
                        
                        .clipShape(Capsule())
                    }
                }
            }
        }
    }
}

I created a simple struct Table that represents a table in a restaurant. It just has an id and some custom name. Normally you’d get that data from some data provider, but I want to keep this post as simple as possible.

The UI is very straightforward. There are three buttons displayed for each table. When one is clicked, the app opens a Deep Link into your app.

Deep link configuration

To support Deep Linking in your app, go to Url Types definition under Target > App > Info and add some URL Scheme. I simply named it intentsexample.

That’s all. Now install the app and open Shortcuts. Tap new > Add Activity > Applications > name_of_app_you_used > „Reserve a table”.

It should be also available using Siri now. I always test Siri using a written form so I can be sure about the phrase it catches. If you don’t have it enabled Open Settings > Accessibility > Siri. Enable „write to Siri”. Then if you hold the button it should open the prompt.

At this moment buttons will only jump to the app. We need to add Ionic support to handle the deep link properly.

Integration with Ionic

Ionic part is quite simple. We will react on the deep link and open the modal. Let’s start with a modal:

// modal.component.ts
import { Component, Input } from '@angular/core';
import { ModalController } from '@ionic/angular';

@Component({
  template: `
    <ion-header>
      <ion-toolbar>
        <ion-button (click)="dismiss()">Dismiss</ion-button>
      </ion-toolbar>
    </ion-header>
    <ion-content>
      <h1>You selected table: {{ table }}</h1>
    </ion-content>
  `,
})
export class ModalComponent {
  @Input() table: string | null = null;
  constructor(private modalController: ModalController) {}

  async dismiss() {
    await this.modalController.dismiss();
  }
}

Nothing fancy here, just a modal with a passed text. Beside this we want to make an action based on the deep link.

// app.component.ts
import { Component } from '@angular/core';
import { App, URLOpenListenerEvent } from '@capacitor/app';
import { ModalController } from '@ionic/angular';
import { ModalComponent } from './modal/modal.component';

export class Table {
  constructor(public id: number, public name: string) {}
}

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent {
  tables = [
    new Table(1, 'For 2'),
    new Table(2, 'Family table'),
    new Table(3, 'Outdoor table'),
  ];

  constructor(private modalController: ModalController) {
    this.initializeApp();
  }
  initializeApp() {
    App.addListener('appUrlOpen', async (event: URLOpenListenerEvent) => {
      let slug = event.url.split('intentsexample://').pop();
      if (slug && slug.startsWith('open-modal')) {
        slug = slug.split('/').pop();
        const tableId = slug ? +slug : null;
        if (
          tableId &&
          !isNaN(tableId) &&
          this.tables.some((t) => t.id === tableId)
        ) {
          const modal = await this.modalController.create({
            component: ModalComponent,
            componentProps: {
              table: this.tables.find((t) => t.id === tableId)?.name,
            },
          });
          await modal.present();
        }
      }
    });
  }
}

Now when the app is opened using a deep link starting with 'open-modal’ we check if it has a valid table id. If so we show a modal with a selected table. This has also all data hardcoded for simplicity. You can now rebuild, reinstall app and try Siri once again. After selecting the button it will now navigate to the app and open modal with right option selected.

Summary

In my opinion App Intents are very easy and quick to implement. They require less work than Intents Extensions and offer a range of versatile features, including the ability to create custom UIs for user dialogs and communicate with Ionic applications.

While deep linking is a popular option, App Intents offer more flexibility. You can execute actions immediately or store them in shared data to be accessed later in the app. By utilizing App Intents, you can easily add complex features to your app without spending too much time on implementation.


Opublikowano

w

,

przez