• Introducing AppIntents

    • iOS 10에 처음 소개된 SiriKit Intents
      • 앱의 기능을 Siri 도메인(메시징, 운동, 지불 등)에 연결시켜 주기 위함
    • 이를 대체하는 새로운 프레임워크인 App Intents의 등장
      • Intents: 시스템 전체에서 사용될 수 있는 앱의 동작
      • Entities: intent를 통해서 표현되는 앱의 컨셉
      • App Shortcuts: intent를 래핑해서 자동으로 실행하고, 쉽게 발견되기 하고 싶을 때 사용
    • 어떻게 시스템이 사용하는가?
      • App Shortcut은 Siri를 통해서 추가 설정 없이 음성으로 실행될 수 있다.
        • 동시에 spotlight로 앱을 검색할 때도 shortcut을 찾을 수 있다.
        • 단축어 앱에도 자동으로 뜬다.
      • AppIntents를 써서 Focus Filter를 구축할 수 있다.
    • 핵심 속성
      • Concise
        • 간결하지만, 깊이 있게 커스텀할 수 있다.
      • Modern
        • Swift기반
        • Result Builder와 Property Wrappers, POP, 제네릭을 적극적으로 활용
      • Easy
        • 쉽게 사용 가능
        • 아키텍쳐 재구축이나 프레임워크 생성 등이 필요 없다.
        • 별도 extension이 아니라 앱에 채택할 수 있다.
      • Maintainable
        • SwiftUI처럼 코드가 곧 Source of Truth
        • 이는 빠르게 구축하고 반복할 수 있게 하고, 유지보수를 단순하게 한다.
  • Intents and paramaters

    • 특징
      • 앱이 가진 기능 중 한 조각
      • 수동 혹은 자동 실행 모두 가능
      • 결과 값 혹은 에러를 반환함
    • 구성요소
      • 메타데이터
        • localized title
      • 매개변수
      • 실행 메소드
    • 예시
      • 특정 탭 열기 Intent

        • Intent만 만들어도 단축어 앱에서 사용자가 추가 할 수 있는 형태로 나온다.
        struct OpenCurrentlyReading: AppIntent {
            static var title: LocalizedStringResource = "Open Currently Reading"
        
            @MainActor // 메인스레드를 강제하기 위해서
            func perform() async throws -> some IntentResult {
                Navigator.shared.openShelf(.currentlyReading)
                return .result()
            }
          
            static var openAppWhenRun: Bool = true
        }
        
      • spotlight와 단축어 앱에서 자동으로 나타나게 하기 → 디테일은 다른 세션에서

        struct LibraryAppShortcuts: AppShortcutsProvider {
            static var appShortcuts: [AppShortcut] {
                AppShortcut(
                    intent: OpenCurrentlyReading(),
                    phrases: ["Open Currently Reading in \\(.applicationName)"],
                    systemImageName: "books.vertical.fill"
                )
            }
        }
        
      • intent에 넘길 매개변수 선언

        • AppEnum 프로토콜을 따라야 한다.

          enum Shelf: String {
              case currentlyReading
              case wantToRead
              case read
          }
          
          extension Shelf: AppEnum {
              static var typeDisplayRepresentation: TypeDisplayRepresentation = "Shelf"
          
          		// 컴파일 타임에 결정되어야 하기 때문에 리터컬로 써야 한다.
              static var caseDisplayRepresentations: [Shelf: DisplayRepresentation] = [
                  .currentlyReading: "Currently Reading",
                  .wantToRead: "Want to Read",
                  .read: "Read",
              ]
          }
          
      • 매개변수 제공하기

        • parameter Summary를 제공해서, 좀 더 깔끔하게 보이도록 할 것
        • 앱을 열어야 하는 경우는 openAppWhenRun을 true로 설정할 것.
          • UI로 보여줘야 하는 경우
        struct OpenShelf: AppIntent {
            static var title: LocalizedStringResource = "Open Shelf"
        
            @Parameter(title: "Shelf")
            var shelf: Shelf
        
            @MainActor
            func perform() async throws -> some IntentResult {
                Navigator.shared.openShelf(shelf)
                return .result()
            }
        
            static var parameterSummary: some ParameterSummary {
                Summary("Open \\(\\.$shelf)")
            }
        
            static var openAppWhenRun: Bool = true
        }
        

        스크린샷 2022-08-11 오전 12.12.31.png

  • Entities, queries, and results

    • AppEnum은 고정된 경우의 수만 다룰 수 있다.

      • 동적인 것을 다루기 위해서는 Entity가 필요하다.
    • Entity의 구성 요소

      • Identifier
      • Display Representation
      • Entity type name
    • Entity예시

      • AppEntity를 채택해야 한다.
      • 식별자는 안정적이고, 영속적으로 사용할 수 있는 값이여야 한다.
      • displayRepresentation은 단순 텍스트일 수도 있지만, 부제목과 이미지를 제공할 수도 있다.
      struct BookEntity: AppEntity, Identifiable {
          var id: UUID
      
          var displayRepresentation: DisplayRepresentation { "\\(title)" }
      
          static var typeDisplayRepresentation: TypeDisplayRepresentation = "Book"
      
      		// ...
      }
      
    • Query

      • 앱에서 Entity를 찾아내기 위한 인터페이스
      • 종류
        • ID 기반
        • String 기반
        • Property기반
      • 특정 엔티티를 제안할 수 있다.
      • Query에 의해서 나오는 Entity만 유저가 선택할 수 있다.
    • Query 예시

      struct BookQuery: EntityQuery {
          func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
              identifiers.compactMap { identifier in
                  Database.shared.book(for: identifier)
              }
          }
      }
      
      struct BookEntity: AppEntity, Identifiable {
      		// ...
      
      		static var defaultQuery = BookQuery()
      }
      
    • Query로 Entity를 가져와서 Intent에서 쓰기

      • 선택은 단축어 앱에서 이미 했다.

        struct OpenBook: AppIntent {
            @Parameter(title: "Book")
            var book: BookEntity
        
            static var title: LocalizedStringResource = "Open Book"
        
            static var openAppWhenRun = true
        
            @MainActor
            func perform() async throws -> some IntentResult {
                guard try await $book.requestConfirmation(for: book, dialog: "Are you sure you want to clear read state for \\(book)?") else {
                    return .result()
                }
                Navigator.shared.openBook(book)
                return .result()
            }
        
            static var parameterSummary: some ParameterSummary {
                Summary("Open \\(\\.$book)")
            }
          
            init() {}
        
            init(book: BookEntity) {
                self.book = book
            }
        }
        
    • Query에 Suggestion과 Search 지원하기

      struct BookQuery: EntityStringQuery {
          func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
              identifiers.compactMap { identifier in
                  Database.shared.book(for: identifier)
              }
          }
      
          func suggestedEntities() async throws -> [BookEntity] {
              Database.shared.books
          }
      
          func entities(matching string: String) async throws -> [BookEntity] {
              Database.shared.books.filter { book in
                  book.title.lowercased().contains(string.lowercased())
              }
          }
      }
      
    • 좀 더 복잡한 Intent

      • 결과 값을 반환하거나, 다른 Intent를 이어서 실행하거나
      struct AddBook: AppIntent {
          static var title: LocalizedStringResource = "Add Book"
      
          @Parameter(title: "Title")
          var title: String
      
          @Parameter(title: "Author Name")
          var authorName: String?
      
          @Parameter(title: "Recommended By")
          var recommendedBy: String?
      
          func perform() async throws -> some IntentResult & ReturnsValue<BookEntity> & OpensIntent {
              guard var book = await BooksAPI.shared.findBooks(named: title, author: authorName).first else {
                  throw Error.notFound
              }
              book.recommendedBy = recommendedBy
              Database.shared.add(book: book)
      
              return .result(
                  value: book,
                  openIntent: OpenBook(book: book)
              )
          }
      
          enum Error: Swift.Error, CustomLocalizedStringResourceConvertible {
              case notFound
      
              var localizedStringResource: LocalizedStringResource {
                  switch self {
                      case .notFound: return "Book Not Found"
                  }
              }
          }
      }
      
  • Properties, finding, and filtering

    • Enitity에 Property를 추가해서 더 많은 정보를 제공하고, 이를 Finding과 filtering에 쓸 수 있다.

      struct BookEntity: AppEntity, Identifiable {
          var id: UUID
      
          @Property(title: "Title")
          var title: String
      
          @Property(title: "Publishing Date")
          var datePublished: Date
      
          @Property(title: "Read Date")
          var dateRead: Date?
      
          var recommendedBy: String?
      
          var displayRepresentation: DisplayRepresentation { "\\(title)" }
      
          static var typeDisplayRepresentation: TypeDisplayRepresentation = "Book"
      
          static var defaultQuery = BookQuery()
      
          init(id: UUID) {
              self.id = id
          }
      
          init(id: UUID, title: String) {
              self.id = id
              self.title = title
          }
      }
      
    • Property를 이용한 Query

      struct BookQuery: EntityPropertyQuery {
          static var sortingOptions = SortingOptions {
              SortableBy(\\BookEntity.$title)
              SortableBy(\\BookEntity.$dateRead)
              SortableBy(\\BookEntity.$datePublished)
          }
      
          static var properties = QueryProperties {
              Property(\\BookEntity.$title) {
                  EqualToComparator { NSPredicate(format: "title = %@", $0) }
                  ContainsComparator { NSPredicate(format: "title CONTAINS %@", $0) }
              }
              Property(\\BookEntity.$datePublished) {
                  LessThanComparator { NSPredicate(format: "datePublished < %@", $0 as NSDate) }
                  GreaterThanComparator { NSPredicate(format: "datePublished > %@", $0 as NSDate) }
              }
              Property(\\BookEntity.$dateRead) {
                  LessThanComparator { NSPredicate(format: "dateRead < %@", $0 as NSDate) }
                  GreaterThanComparator { NSPredicate(format: "dateRead > %@", $0 as NSDate) }
              }
          }
      
      		// ...
      
          func entities(
              matching comparators: [NSPredicate],
              mode: ComparatorMode,
              sortedBy: [Sort<BookEntity>],
              limit: Int?
          ) async throws -> [BookEntity] {
              Database.shared.findBooks(matching: comparators, matchAll: mode == .and, sorts: sortedBy.map { (keyPath: $0.by, ascending: $0.order == .ascending) })
          }
      }
      
  • User Interaction

    • App Intent가 지원하는 Interaction
      • Dialog: Intent 사용자에게 텍스트나 음성으로 피드백을 주는 것
      • Snippets: Dialog를 시각화 한 것
      • Request Value
      • Disambiguation
      • Confirmation
  • Architecture and lifecycle