App Intents with Siri

Sep 19 2024 · Swift 6.0, iOS 18, Xcode 16

Lesson 03: Optimizing User Experience with Siri Integration

Demo

Episode complete

Play next episode

Next

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Intro

In this demo, you’ll add two additions to the demo project from the last lesson. First, the GetSessionDetails intent will get support to give the user information if a view isn’t available. Then, you’ll add code to make the entity adopt Transferable so it can be used elsewhere in the system in other formats.

Enhancing the Intent

Updating GetSessionDetails

The perform function for intents is pretty flexible regarding what it can return. Originally, it only returned the session. Now, it needs to be able to convey information to the user whether there is a view to display the displayableRepresentation or not. Start by adding additional protocol adoption to the return type of perform:

func perform() async throws -> some IntentResult & ReturnsValue<SessionEntity> & ProvidesDialog & ShowsSnippetView {
let snippet = SessionSiriDetailView(session: sessionData)
let dialog = IntentDialog(
  full: """
  The runtime reported for \(sessionToGet.name) is \(sessionToGet.sessionLength ?? "no runtime reported")
  and has the following description: \(sessionToGet.sessionDescription ?? "no description provided").
  """,
  supporting: "Here's the information on the requested session."
)
return .result(value: sessionToGet, dialog: dialog, view: snippet)

Demoing the Intent View

Let’s see how all this looks in the simulator. Run the app once, just in case this is the first time. This allows Spotlight to index all of the session information. Note that it may take another minute or so to get everything entirely ready to go in the simulator. Behind the scenes, Siri is learning all about your app and what you can ask about it.

Adding Transferable Support

Getting your entities ready for Transferable requires two code additions. Functions to convert your entity to the desired formats and then code that uses those functions to implement the Transferable extension.

Updating the Entity

Start by adding some required imports and an extension block to SessionEntity, where code related to Transferable will go, but don’t adopt that protocol just yet:

import CoreTransferable
import UIKit
extension SessionEntity {
func toString() -> String {
  return "\(self.name) (\(self.sessionLength ?? "No Length"): \(self.sessionDescription ?? "No Description"))"
}
func sessionAsJSON() async -> Data? {
  var json: [String: String] = [:]
  json["id"] = "\(self.id)"
  json["name"] = self.name
  json["imageName"] = self.imageName
  json["sessionDescription"] = self.sessionDescription
  json["sessionLength"] = self.sessionLength
  json["url"] = self.url?.absoluteString
  do {
    return try JSONSerialization.data(withJSONObject: json, options: JSONSerialization.WritingOptions.prettyPrinted)
  } catch let myJSONError {
    print(myJSONError)
  }
  return nil
}
func sessionAsJPEG() async -> SentTransferredFile? {
  var transferredFile: SentTransferredFile?
  let size = CGSize(width: 300, height: 500)
  let frame = CGRect(origin: .zero, size: size)
  let image = UIGraphicsImageRenderer(size: size).image { rendererContext in
    UIColor.systemGray.setFill()
    rendererContext.fill(CGRect(origin: .zero, size: size))
    toString().draw(in: frame)
  }
  ...
  ...
  if let data = image.jpegData(compressionQuality: 0.4) {
    let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    let file = path.appendingPathComponent("session.jpg")
    try? data.write(to: file)
    transferredFile = SentTransferredFile(file)
  }
  return transferredFile
}

Adding the Transferable Extension

Now that the helper methods are defined, the extension can adopt Transferable and add the necessary protocol property.

extension SessionEntity: Transferable {
static var transferRepresentation: some TransferRepresentation {
  DataRepresentation(exportedContentType: .json) { sessionEntity in
    guard let sessionData = await sessionEntity.sessionAsJSON() else { throw TransferableError.invalidJSON
  }
  return sessionData
}
enum TransferableError: Error {
  case noFileFound
  case invalidJSON
}
      FileRepresentation(exportedContentType: .jpeg) { sessionEntity in
        guard let sessionJPG = await sessionEntity.sessionAsJPEG() else { throw TransferableError.noFileFound
      }
      return sessionJPG
    }
  }
}

Demoing Sending the Entity to Notes via the Shortcuts App

Build and run the app so the updated code is sent to the simulator. To check the entity’s conversion to JSON and JPEG, make some shortcuts. Start by opening shortcuts and starting a new shortcut.

See forum comments
Cinema mode Download course materials from Github
Previous: Instruction Next: Conclusion