Añadir un App Clip en iOS 14

1
1806



Índice


1. Introducción

Presentados en la WWDC 2020 para iOS 14, los App Clips son la respuesta de Apple a las PWA. Se tratan de Apps ultraligeras, de no más de 10 MB, 100% nativas y que se descargan mediante la lectura de un QR o mediante NFC. Su objetivo es encapsular una mínima funcionalidad de una aplicación, de forma que no sea necesario descargar una aplicación entera para ello (por ejemplo, comprar una bebida o pagar rápidamente por el alquiler de algún servicio).

App Clip Code

Al contrario que una aplicación normal, los App Clips son eliminados por el sistema automáticamente tras detectar que no se usan, borrando todos los datos. En cambio, si estos se usan periódicamente puede mantener los datos del usuario (por ejemplo, imaginemos el caso de un App Clip para comprar un refresco, que se usa casi todos los días para comprar el mismo tipo de refresco, en este caso el App Clip no expirará y además recordará la última elección).

Puedes encontrar el código de ejemplo aquí https://github.com/DaniOtero/DodoAirlines-AppClipDemo


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15″ (2.5 GHz Intel Core i7, 16 GB 1600 MHz DDR3)
  • Sistema Operativo: macOS 11 Big Sur Beta 2
  • Xcode 12 Beta 2
  • iOS 14
  • Swift 5.3


3. Añadiendo un App Clip a nuestra aplicación

La aerolínea Dodo Airlines, una vez vista la WWDC 2020, decide que sería una genial idea crear un App Clip para que todos aquellos pasajeros que no tengan la aplicación puedan hacer Check-In rápidamente. Su aplicación actual tiene tres pantallas, metidas en un TabBar, una para buscar y reservar vuelos, otra para hacer Check-In y la típica pantalla de perfil en la que el usuario puede ver sus datos, su nivel, sus millas Nook, etc (nota: todo el código es un ejemplo sencillo, es todo «cartón-piedra», no hace absolutamente nada salvo como mucho navegar entre pantallas).

Pantallas

La pantalla de Check-In, que es la que nos interesa para este ejemplo, pide el apellido del pasajero y el localizador del vuelo. Una vez introducidos nos presenta una pantalla de detalle con los datos del vuelo, y nos ofrece la posibilidad de comprar algún extra y/o para el viaje o hacer Check-In (si no se había hecho aun) y obtener la tarjeta de embarque.

Detalle Check-In

Lo primero vamos a añadir una extensión App Clip a nuestro proyecto, para ello en Xcode seleccionamos el proyecto y pulsando en el icono «+» abajo a la izquierda. La llamaré DodoAirlinesClip por poner un ejemplo.

Añadir App Clip

Me creará en el proyecto un directorio «DodoAirplinesClip». Este contendrá «DodoAirlinesClipApp.swift» y «ContentView.swift» así como el «Info.plist» y el «DodoAirlinesClip.entitlements». Voy a refactorizar «ContentView» para renombrarlo a «MainView», que de momento contiene un «Hello World».

import SwiftUI

struct MainView: View {
    var body: some View {
        Text("Hello World")
    }
}

struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
    }
}


4. Modificando la App

Ya tenemos nuestro App Clip, ahora vamos a darle forma. Queremos que el App Clip muestre la pantalla actual de Check-In, pero queremos cambiar su comportamiento. En vez de mostrar la pantalla intermedia de la que hemos hablado antes, queremos que sea rápido y liviano y que directamente nos obtenga la tarjeta de embarque. Para conseguir esta diferencia de comportamiento podemos hacer dos cosas, o bien creamos una vista completamente distinta o bien introducimos un flag de compilación. Para este ejemplo se va a optar por la segunda. Seleccionamos nuestro proyecto, a continuación el target del App Clip, y en la sección «Build Settings» -> «Swift compiler – Custom flags», creamos un nuevo flag, por ejemplo «APPCLIP».
Custom flag

Con esto ya podemos cambiar el comportamiento de la vista en función de si es la aplicación principal o el App Clip con la directiva «#if – #else – #endif

#if APPCLIP
// Do something only if it is the App Clip
#endif

Ahora, añadimos el fichero «CheckinView.swift» y todas sus dependencias al target «DodoAirlinesClip». Seleccionamos los archivos oportunos, y en el inspector los marcamos para dicho target.

Añadir al target

Si nos dejamos alguna dependencia, el compilador se quejará y nos dirá que le falta algo para compilar el target, como es el caso. ¿Qué sucede? Vamos a echar un vistazo a nuestro CheckinView.swift:

//
//  CheckInView.swift
//  DodoAirlines
//
//  Created by Daniel Otero on 08/07/2020.
//

import SwiftUI

#if APPCLIP
import StoreKit
#endif

struct CheckInView: View {
    @State private var surname: String = ""
    @State private var locator: String = ""
    @State private var keyboardOffset: CGFloat = 0
    @State private var showDetail: Bool = false

    var body: some View {
        NavigationView {
            GeometryReader { geometry in
                container(geometry: geometry)
            }
            .colorScheme(.light)
        }
        .accentColor(.white)
        .colorScheme(.dark)
    }

    private func container(geometry: GeometryProxy) -> some View {
        VStack {
            Image("logo")
            VStack(spacing: 8) {
                VStack {
                    TextField("Surname", text: $surname)
                        .padding(.all, 8)
                    TextField("Locator", text: $locator)
                        .padding(.all, 8)
                }
                .background(Color.white)
                .cornerRadius(8)

                NavigationLink(
                    destination: checkinDetailView,
                    isActive: $showDetail,
                    label: {
                        CustomButton(action: checkIn) {
                            Text("Check In")
                        }
                    })
            }
            .padding(.all, 16)
            .background(Color("Box"))
            .cornerRadius(16.0)
            Spacer()
        }
        .frame(height: geometry.size.height - keyboardOffset)
        .animation(.spring())
        .padding(.all, 16)
        .background(Color("Background").edgesIgnoringSafeArea(.all))
        .onAppear {
            NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notification in
                let rect = notification.userInfo![UIResponder.keyboardFrameBeginUserInfoKey] as! CGRect
                self.keyboardOffset = rect.height
            }

            NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in
                self.keyboardOffset = 0
            }
        }
    }

    private func checkIn() {
        showDetail.toggle()
    }

    var checkinDetailView: some View {
        let flight = Flight(origin: "MAD",
                            dest: "NRT",
                            date: Date(),
                            checked: false,
                            duration: 3600 * 14)
        return CheckInDetailView(flight: flight)
    }
}

Vale, la vista actual contiene un NavigationLink que hace uso de CheckInDetailView y este no está incluido en nuestro target. Esto no es lo que queremos, sino que queremos que directamente devuelva las tarjetas de embarque. Vamos a empezar por declarar dos variables de estado para el App Clip:

#if APPCLIP
    @State private var showCheckInSuccess: Bool = false
    @State private var presentingAppStoreOverlay: Bool = false
#endif

La primera la utilizaremos para simular que hemos obtenido la tarjeta de embarque (simplemente vamos a mostrar un alert a modo de ejemplo). La segunda la vamos a utilizar para mostrar un overlay que invite al usuario a descargar la aplicación principal si le ha gustado la experiencia. Para hacer esto, iOS nos lo da casi hecho. Modificamos ligeramente el cuerpo de la vista:

var body: some View {
    NavigationView {
        GeometryReader { geometry in
                #if APPCLIP
                container(geometry: geometry)
                    .appStoreOverlay(isPresented: $presentingAppStoreOverlay) {
                        SKOverlay.AppClipConfiguration(position: .bottom)
                    }
                #else
                container(geometry: geometry)
                #endif
        }
        .colorScheme(.light)
    }
    .accentColor(.white)
    .colorScheme(.dark)
}

E incluimos el StoreKit en la parte superior:

import SwiftUI

#if APPCLIP
import StoreKit
#endif

Ahora viene lo que realmente nos interesa, localizamos nuestro botón de «Check-In», que ahora mismo es una NavigationLink. Vamos a duplicarlo para que haga cosas distintas en función de si es la aplicación principal o el App Clip:

#if APPCLIP
CustomButton(action: checkIn) {
    Text("Check In")
}
.alert(isPresented: $showCheckInSuccess) {
    Alert(title: Text("Success"),
            message: Text("Your flight is checked in. If this was a real App this should download your boarding passes 🙂"))

}
#else
NavigationLink(
    destination: checkinDetailView(),
    isActive: $showDetail,
    label: {
        CustomButton(action: checkIn) {
            Text("Check In")
        }
    })
#endif

También tenemos que modificar la función «checkIn()» para que haga cosas distintas:

#if APPCLIP
showCheckInSuccess.toggle()
presentingAppStoreOverlay.toggle()
#else
showDetail.toggle()
#endif

Y en este punto aún se nos quejará, eso es porque aún está intentado usar CheckInDetailView. Simplemente lo excluimos de la compilación del App Clip:

#if !APPCLIP
var checkinDetailView: some View {
    let flight = Flight(origin: "MAD",
                        dest: "NRT",
                        date: Date(),
                        checked: false,
                        duration: 3600 * 14)
    return CheckInDetailView(flight: flight)
}
#endif

Y con esto ya sería suficiente, si ahora ejecutamos el target del App Clip, podemos ver que, ademas de no aparecer el TabBar, cuando pulsamos el botón «Check In» nos muestra un alert en vez de navegar a otra pantalla.

Ejecutar App Clip

App Clip


5. Opciones avanzadas

Todos los App Clips llevan asociada una URL que hay que registrar en App Store Connect, que es la que leen los dispositivos por NFC o mediante código QR. Esta URL se puede parsear para cambiar el comportamiento de la App, en función de sus query params. Imaginemos que en el ejemplo anterior, a parte del hacer Check-In, Dodo Airlines quiere que con el App Clip se pueda hacer Check In y comprar una maleta a la vez, pero solo para ciertas ubicaciones por temas legales. En ese caso, tan solo tendría que hacer que en el «DodoAirlinesClipApp», añadir algo como:

import SwiftUI

@main
struct DodoAirlinesClipApp: App {
    @StateObject private var model = DodoAirlinesModel()


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

    func handleUserActivity(_ userActivity: NSUserActivity) {
        guard let incomingURL = userActivity.webpageURL,
              let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
              let queryItems = components.queryItems,
              queryItems.contains(where: { $0.name == "purchaseBaggage" }) else {
            return
        }

        guard let payload = userActivity.appClipActivationPayload,
              let latitudeValue = queryItems.first(where: { $0.name == "latitude" })?.value,
              let longitudeValue = queryItems.first(where: { $0.name == "longitude" })?.value,
              let latitude = Double(latitudeValue), let longitude = Double(longitudeValue) else {
            return
        }

        let region = CLCircularRegion(center: CLLocationCoordinate2D(latitude: latitude,
                            longitude: longitude), radius: 100, identifier: "dodo_location")

        payload.confirmAcquired(in: region) { inRegion, error in
            if let error = error {
                print(error.localizedDescription)
                return
            }
            DispatchQueue.main.async {
                model.purchaseBaggageAllowed = inRegion
            }
        }
    }
}

El método confirmAcquired nos permite saber si el App Clip se ha ejecutado en la ubicación leída de los query params (aunque lo suyo sería que esta ubicación o ubicaciones se le devuelva de algún servicio o configuración, pero para propósitos de prueba nos vale).


6. Conclusiones

Aunque es muy fácil crear un App Clip, la principal barrera de entrada es que es obligatorio que esté escrita en Swift UI, con lo cual es difícil que aplicaciones existentes, sobre todo aquellas que aún estén dando soporte a versiones anteriores a iOS 13.

En estos casos crear un App Clip supondría reescribir dicha funcionalidad de una App en Swift UI, y si aun después de la salida de iOS 14 continúa soportando versiones anteriores a iOS 13 tendrá que mantener tanto la parte en UIKit como la parte en Swift UI de forma independiente.

Por contra, para aplicaciones nuevas resulta muy fácil. Tendiendo vistas con alta cohesión y bajo acoplamiento (SRP de SOLID), debería ser bastante fácil poder reutilizar esa vista para el propósito que se desee.

Si además la App cuenta con integración “Sign-In with Apple” y Apple Pay, la cosa aun se vuelve más interesante, porque los usuarios pueden identificarse y pagar fácilmente con solo un par de taps.

1 COMENTARIO

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad