스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
앱 인텐트 자세히 알아보기
앱 인텐트 프레임워크를 사용하여 앱의 검색 가능성과 앱 사용률을 높일 수 있는 방법을 알아보세요. 이 Swift 프레임워크의 강력한 기능에 대해 안내하고, 앱 인텐트와 SiriKit 인텐트의 차이점을 살펴보며, 앱의 기능을 시스템에 노출하는 방법을 보여드립니다. 또한 엔티티 및 쿼리를 빌드하여 풍부한 앱 단축어 경험을 만드는 방법을 소개합니다. 앱 인텐트에 대해 자세히 알아보려면 WWDC22의 ‘Implement App Shortcuts with App Intents(앱 인텐트를 통해 앱 단축어 구현)' 및 ‘Design App Shortcuts(앱 단축어 디자인)'을 시청하시기 바랍니다.
리소스
관련 비디오
Tech Talks
WWDC23
WWDC22
WWDC21
-
다운로드
♪ 잔잔한 힙합 연주 음악 ♪ ♪ 안녕하세요 저는 Shortcuts Engineering의 Michael Gorbach입니다 앱 Intents에 대한 심층 분석을 시청해주셔서 감사합니다 그건 여러분 앱의 기능을 시스템에 노출시켜주는 우리의 새 프레임워크입니다 분석 계획은 다음과 같습니다 짧은 소개 후에 intents와 그 파라미터와 개체 정의 방법을 설명하겠습니다 강력한 조사 결과와 여러분이 구축할 수 있는 필터링 기능과 함께 여러분의 intents가 사용자와 상호 작용하는 방법도 알아보죠 마지막으로는 앱 Intents의 아키텍처와 수명 주기를 다룹니다 그럼 시작해보겠습니다 iOS 10에서 우린 SiriKit Intents 프레임워크를 소개했는데 그건 여러분이 앱의 기능을 메시징, 운동 지불 같은 Siri 도메인에 연결하게 해줍니다 이제 우린 앱 Intents라는 새 프레임워크를 도입합니다 거기엔 핵심 요소들이 세 가지 있습니다 intents는 여러분의 앱에 내장된 동작들로 시스템 전체에 사용될 수 있습니다 intents는 개체들을 써서 여러분 앱의 개념들을 나타냅니다 앱 Shortcuts는 intents를 둘러싸서 그게 자동이 되게 하고 쉽게 찾을 수 있게 합니다 앱 Intents가 앱의 기능을 더 많은 곳에서 이용할 수 있게 해서 고객들에게 혜택을 주는 두 가지 방법을 설명하겠습니다 앱 Shortcuts로 누구나 Siri를 통해 자기 목소리로 앱의 기능을 쓸 수 있습니다 아무것도 미리 설정할 필요 없이요 동일한 채택은 여러분의 intents가 Spotlight에서 나타나게 합니다 사람들이 여러분의 앱을 검색하고 그게 제안될 때요 그건 여러분의 작업을 가장 중요한 위치에 둡니다 앱 Intents를 써서 여러분은 Focus Filters도 구축해서 고객들이 특정 Focus를 위해 앱을 맞춤화하게 해줍니다 예를 들어 그들은 자신의 Calendar 앱을 구성해서 그들이 실제로 작업 중일 때만 일정을 보여주게 할 수 있습니다 Focus Filter 채택 방법에 관해 자세히 알고 싶으면 이 세션을 확인해보세요 앱 Shortcuts로 여러분의 intents는 Shortcuts 앱에 자동으로 뜹니다 수동으로 추가할 필요 없이요 여러분의 동작을 Shortcuts로 통합시키는 건 고객들에게 가치가 굉장히 큽니다 그건 단축어를 돌아가게 하고 여러분 앱의 기능을 활용할 수 있기 때문입니다 시스템 전체의 아주 많은 곳에서요 그건 Home Screen에서 한 번의 탭으로 단축어를 돌릴 수 있죠, macOS의 메뉴 바와 다양한 방법으로요 그건 자동화로 단축어가 자동으로 돌아가게 구성할 수도 있습니다 단축어 지원은 여러분 앱의 힘과 역량을 배가해줍니다 그걸 전체 Shortcuts 환경에 연결해주는 방식으로요 그건 Apple과 다른 개발 업체들의 앱들로 이뤄진 집합의 힘을 활용합니다 그 이유는 단축어가 여러 앱의 동작들을 결합할 수 있어서 사용자들이 기능들과 역량들을 새롭게 발명할 수 있게 하는데 여러분은 어떤 작업도 할 필요가 없기 때문입니다 여러분의 동작이 다른 것들과 함께 잘 작동되고 이 환경에 매끄럽게 맞출 방법을 알고 싶으면 우리의 디자인 발표를 확인해보세요 앱 Intents 구축에 있어서 우리의 목표는 그걸 개발하는 작업을 즐겁게 하는 거였습니다 앱 Intents는 간결합니다 간단한 intent를 작성하려면 코드 몇 줄만 있으면 되지만 그 API는 더 심오하고 더욱 맞춤화할 수 있는 동작에 스케일링도 하기 때문입니다 앱 Intents는 현대적입니다 우린 Swift에 전념하면서 결과 빌더와 속성 래퍼와 프로토콜 지향 프로그래밍과 제네릭스를 활용했습니다 이 API들은 최첨단 언어 기능이 없다면 존재하지 못할 겁니다 앱 Intents는 채택도 쉽습니다 거기엔 제품과 목표의 재구성이나 프레임워크 제작이 필요 없기 때문입니다 확장 프로그램도 필요 없고 그건 여러분의 앱에서 바로 채택될 수 있습니다 그리고 앱 Intents 코드는 유지 보수가 가능합니다 SwiftUI처럼 앱 Intents는 여러분의 코드를 핵심적인 진리의 근원으로 사용해서 별도의 편집기나 정의 파일의 필요성을 방지합니다 이는 여러분이 채택한 걸 빠르게 구축하고 반복하게 하며 유지 보수를 단순하게 합니다 모든 게 한 곳에 있기 때문입니다 그럼 이제 이 새 API들을 탐색해보죠 intent부터 시작하겠습니다 우리의 새 프레임워크의 중심 구성 요소입니다 앱 Intent, 줄여서 intent는 여러분이 앱이 시스템에 노출하는 분리된 단일한 기능 단위입니다 예를 들어 intent는 새로운 달력 이벤트를 만들거나 특정 화면을 열거나 주문을 할 수 있습니다 intent는 사용자의 요청으로 실행될 수 있습니다 가령 단축어를 돌리거나 Siri에게 부탁하는 방식으로요 또는 자동 방식으로요 가령 Focus filter나 Shortcuts 자동화 사용처럼요 intent가 실행되면 그건 결과를 되돌려주거나 오류를 내보냅니다 intent에는 세 개의 핵심 요소가 포함됩니다 메타데이터입니다 즉, intent에 관한 정보죠 여기엔 로컬화된 제목도 포함됩니다 파라미터입니다, 이는 intent가 실행될 때 사용할 수 있는 입력입니다 그리고 수행 기법입니다 그건 intent가 실행될 때 실제 작업을 하는 겁니다 오늘 우리의 출발점은 이 Library 앱입니다 저는 열렬한 책벌레이기 때문에 그 주된 기능은 제가 읽었던 책들과 읽고 싶은 책들이나 현재 읽는 책을 추적하는 겁니다 각 범주는 제가 책장으로 부르는 앱의 별도 탭에 나타납니다 제 사용자들이 Currently Reading 책장에 늘 방문하기에 저는 앱 Intent를 노출해서 그걸 여는 걸 더욱 빠르고 편리하게 하겠습니다 저는 여기에 OpenCurrentlyReading intent를 만들겠습니다 AppIntent 프로토콜을 따르는 Swift 구조를 정의해서요 저는 수행이라는 한 가지 기법만 실행해야 합니다 제 앱에는 탭을 열어줄 수 있는 Navigator가 이미 있어서 저한테 intent를 실행하는 일은 코드 몇 줄에 불과합니다 저는 수행 기법에 @MainActor를 주석으로 달겠습니다 제 Navigator가 메인 스레드를 예상하기 때문입니다 제 intent에는 제목도 필요합니다 오늘 제가 보여드릴 다른 모든 문자열과 마찬가지로 제가 문자열 파일에 키를 추가하면 이건 자동으로 로컬화됩니다 기본 앱 Intent를 작동시키는 데 필요한 건 그게 다입니다 그건 제 코드에서 정의되어 있으니 Shortcuts 편집기에 자동으로 나타납니다 거기에서 제 사용자는 그걸 단축어에 추가할 수 있습니다 이 intent를 노출시키는 것만으로 엄청난 영향력이 제공됩니다 일단 고객들이 이 intent를 단축어로 바꾸면 시스템의 정말 많은 곳에서 사용될 수 있기 때문입니다 거기엔 이런 데가 다 포함되죠 저의 새 intent를 사용하고 발견하기 쉽게 하기 위해 앱 Shortcuts에 대한 지원도 추가하겠습니다 소량의 코드로 제 intent가 Spotlight와 Shortcuts 앱에서 자동으로 나타나게 할 수 있습니다 그리고 사람들이 Siri에게 말할 수 있는 구절을 정의해서 이 intent를 그들의 목소리로 쓰게 할 수 있습니다 'Implement App Shortcuts with App Intents' 세션을 확인해서 자세한 내용을 알아보세요 지금까지 저는 intent를 노출시켜서 Currently Reading 책장을 열었습니다 다음으론 그걸 일반화하죠 파라미터를 추가해서 그게 어느 책장이든 열 수 있게요 저한텐 책장을 나타내는 이넘이 있습니다 그게 intent 파라미터로 사용되려면 저는 그게 AppEnum 프로토콜을 따르게 해야 합니다 AppEnum에는 String 원시 값이 필요하니 저는 그걸 먼저 추가하겠습니다 그건 또한 이넘 사례 각각에 대해 로컬화 가능한 인간이 읽을 수 있는 제목을 제공할 것을 요구합니다 그것들은 직역 사전으로 제공되어야 합니다 컴파일러가 이 코드를 구축 시간에 읽기 때문입니다 마지막으로 저는 typeDisplayName을 추가합니다 이 이넘 타입 전체를 위한 사용자가 볼 수 있는 로컬화 가능한 이름이죠 저는 'Shelf'를 쓰겠습니다 intent에서 각각의 파라미터는 @Parameter 속성 래퍼를 써서 선언됩니다 그건 제목처럼 파라미터에 관한 정보로 초기화됩니다 여기에서 저는 새 책장 파라미터를 정의하는데 저는 그걸 제 수행 기법에서 읽습니다 파라미터는 이 타입들 모두를 지원합니다 숫자, 문자열, 파일 등과 여러분 앱의 개체들과 이넘들까지 포함해서요 Shortcuts 편집기에서 intent의 모습은 이렇습니다 책장 파라미터가 표의 열로 나타나는 데 주목하세요 저는 UI를 더욱 간결하게 만들 수 있고 그게 Shortcuts에 더 잘 맞게 할 수 있습니다 ParameterSummary API를 사용해서요 Parameter Summary는 한 문장으로 편집기에 있는 여러분의 intent와 그 파라미터를 나타냅니다 가령 'Open '처럼요 Shortcuts에서 최고의 결과를 위해 여러분은 자신이 만드는 모든 intent에 대해 Parameter Summary를 반드시 제공해야 합니다 폴드 아래에 어떤 파라미터가 나타날지와 어떤 게 숨어 있는지도 정의할 수 있습니다 이 API들은 아주 멋진 것들을 할 수 있습니다 가령 intent의 모든 파라미터의 실제 값을 바탕으로 요약을 다르게 하는 것처럼요 When과 Otherwise API나 Switch, Case Default API를 사용해서요 파라미터 요약 추가를 위해 저는 이 정적 속성을 실행합니다 여기에서 저는 문자열 'Open'을 되돌려주고 책장 파라미터를 채워줍니다 Open Shelf가 작동되게 하기 위해 마지막으로 제가 해야 할 일은 intent가 돌아갈 때 Library 앱을 열게 하는 겁니다, 이렇게요 앱을 여는 건 정적 속성인 openAppWhenRun으로 제어됩니다 그건 기본 설정인 거짓이 되는데 대부분의 intent에 좋은 일이죠 그런데 이것처럼 UI에서 뭔가를 열어주는 intent의 경우에 저는 그걸 참으로 설정해야 합니다 저는 방금 책장을 열어주는 intent를 만들었습니다 이건 엄청나게 간단합니다 책장 세트는 고정되어 있으니까요 그런데 제가 Books를 열어주는 intent를 만들고 싶다면 어떨까요? 그것의 세트는 고정되지 않고 역동적입니다 그걸 위해 저한텐 개체가 필요합니다 개체란 여러분의 앱이 앱 Intents에 노출하는 개념입니다 값들이 역동적이거나 사용자 정의형일 때는 이넘 대신 개체를 사용해야 합니다 Notes에 있는 주석이나 Photos의 사진이나 앨범처럼요 개체들의 인스턴스를 제공하기 위해 여러분의 앱은 쿼리를 실행하고 intents의 결과로 개체를 되돌려줄 수 있습니다 먼저 intent를 만들어서 앱에 있는 책을 열겠습니다 Shortcuts 편집기에서 그건 이런 모습일 겁니다 사람들이 Book 파라미터를 두드리면 책을 고를 피커를 얻습니다 제 앱이 제공한 제안된 개체들의 집합을 포함해서요 피커 상단에 있는 이 검색 필드로 그들은 자신의 라이브러리에서 어떤 책이든 찾을 수도 있습니다 제가 intent 자체를 만들기 전에 책 개체와 그에 상응하는 쿼리를 만들어야 합니다 개체에는 최소한 세 가지가 포함됩니다 식별자, 디스플레이 표현 개체 타입명입니다 개체를 추가하려면 먼저 구조가 AppEntity 프로토콜을 따르게 합니다 여기에서 저는 BookEntity에 대한 새 구조를 정의하겠습니다 그런데 제 모델의 기존 타입을 따르게 할 수도 있습니다 여러분은 자신의 개체가 Identifiable 프로토콜을 따르게 해서 식별자를 제공합니다 앱 Intents는 이 식별자를 써서 여러분의 개체를 참고합니다 그게 여러분의 앱과 시스템의 다른 부분들 사이를 이동할 때요 식별자는 안정적이고 지속적이어야 합니다 그건 고객들이 만든 단축어에 저장될 수 있기 때문입니다 디스플레이 표현은 사용자에게 이 개체를 보여주는 데 사용됩니다 그건 텍스트로 된 문자열만큼 단순할 수 있습니다 책 제목처럼요 여러분은 부제목과 이미지를 제공할 수도 있습니다 typeDisplayName은 개체의 타입을 나타내는 인간이 읽을 수 있는 문자열입니다 이 사례에서 그건 'Book'입니다 이제 책 개체를 완성하기 위해 저는 쿼리를 추가해야 합니다 쿼리는 여러분의 앱에서 개체를 회수하는 인터페이스를 시스템에 부여합니다 쿼리는 몇 가지 방법으로 개체를 찾아볼 수 있습니다 모든 쿼리는 식별자를 기반으로 개체를 찾아볼 수 있어야 합니다 문자열 쿼리는 검색을 지원합니다 나중에 여러분은 속성 쿼리를 접하게 될 텐데 그건 더욱 유연합니다 모든 쿼리가 제안된 개체를 제공할 수 있는데 그건 사용자들이 목록에서 고를 수 있게 해줍니다 모든 개체가 쿼리와 연관지어져야 합니다 시스템이 그 개체의 인스턴스를 찾아볼 수 있게요 여러분은 EntityQuery 프로토콜을 따르는 Swift 구조를 만들어서 쿼리를 제공합니다 기본 쿼리에 요구되는 기법은 하나만 있습니다 그건 식별자 배열이 주어졌을 때 여러분이 개체를 모두 찾는 데 실행합니다 저는 모델 데이터베이스에 가서 그 식별자들과 일치하는 모든 책을 찾아서 그걸 실행했습니다 이제 저는 그 쿼리를 개체에 연결해야 합니다 그렇게 하는 방법은 제가 BookEntity 타입에 defaultQuery 정적 속성을 실행하고 제 BookQuery의 인스턴스를 되돌려주는 겁니다 사용자가 책을 고르면 그 식별자는 단축어에 저장됩니다 단축어가 실행되면 앱 Intents는 그 식별자를 제 쿼리에 전달해서 BookEntity 인스턴스를 회수합니다 이제 BookEntity 타입이 AppEntity 프로토콜을 따르니 저는 제 OpenBook intent에서 그걸 파라미터로 쓸 수 있습니다 수행 기법은 제 Navigator를 써서 그 책으로 찾아갑니다
책 피커를 지원하기 위해 제 쿼리는 제안된 결과도 제공해야 합니다 그러기 위해 저는 쿼리에 기법을 하나 더 실행해서 제 Library 앱에 추가된 모든 책을 되돌려줘야 합니다 Shortcuts는 그 결과들로 피커를 채울 겁니다 Shortcuts UI의 상단에 검색 필드가 있는 데 주목하세요 제 앱에는 수많은 책 개체가 있을 수 있으니 제 데이터베이스를 바탕으로 제 앱 프로세스에서 검색을 직접 돌려야 합니다 StringQuery API는 제가 그렇게 하게 해줍니다 StringQuery 서브프로토콜을 채택하면 저는 실행할 기법을 하나 더 얻는데 그건 entities (matching string:)란 것으로 문자열이 주어질 때 결과를 되돌려줍니다 여기에서 전 그걸 단순 대소문자 미구분 일치로 책 제목에 대해 실행했습니다만 더 멋진 걸 할 수도 있었습니다 예를 들어 저자나 시리즈명을 검색하는 것처럼요 제 책 목록이 엄청나게 길고 좋아하는 책 목록이 짧다면 저는 suggestedEntities에서 좋아하는 것들만 되돌려주고 entities (matching string:)에 의존해서 사용자들이 긴 목록의 내용을 검색하게 할 수 있습니다 이제 저는 제 앱에서 책을 여는 방법을 노출했고 그 과정에서 책 개체와 책 쿼리를 구축했습니다 저는 동일한 개체와 쿼리를 써서 intents를 더 만들 수 있습니다 제 다음 작업은 라이브러리에 책을 추가하는 intent를 구축하는 겁니다 고객들은 온라인을 돌아보는 동안 공유 시트 단축어를 써서 빠르게 책을 추가하거나 HomePod에 있는 Siri에게 책을 추가하라고 말할 수 있습니다 화면을 보지도 않은 채로요 여러분의 UI를 보여주지 않고 여러분의 모델을 직접 조종하는 intent를 이런 식으로 구축하는 건 사용자들에게 권한을 줄 수 있죠 이건 제 AddBook intent의 실행입니다 책 제목과 선택적으로 저자명을 파라미터로 취합니다 거기에는 선택적인 주석도 포함되는데 어떤 친구가 그 책을 추천했는지 기록하는 주석입니다 수행 기법은 그 책을 라이브러리에 추가해줍니다 비동기/대기를 써서 API 호출로 그걸 찾아보는 방식으로요 그게 일치를 못 찾아내면 오류를 내보냅니다 이 오류를 로컬화하기 위해 오류 타입을 다음에 맞게 합니다 CustomLocalizedString ResourceConvertible 프로토콜이죠 저는 이 속성에서 로컬화된 문자열 키를 되돌려주고 그 키를 제 문자열 파일에 추가하겠습니다 이 Add Book intent는 있는 그대로 대단히 유용합니다 Siri, 위젯 등에서요 하지만 그게 다른 intent들과 결합될 수 있다면 그건 훨씬 더 유연해집니다 약간의 노력으로 저는 제 Add Book intent를 앞에서 구축한 Open Book intent에 결합할 수 있게 하고 그 결과를 한 쪽에서 다른 쪽으로 전달합니다 그러기 위해 저는 Add Book intent가 그 결과의 일환으로 값을 되돌려주게 하겠습니다 주목할 점은 제 수행 기법의 리턴 타입이 새 프로토콜을 취해서 제가 되돌려주는 값을 나타낸다는 겁니다 이제 사용자들은 이 intent의 결과치를 책 개체를 파라미터로 받아들이는 다른 intent에 연결할 수 있습니다 Add Book intent와 Open Book intent는 꽤 자연스럽게 함께 짝을 이루니 여러분은 책을 추가하고 그걸 라이브러리에서 곧바로 열어주는 단축어를 만들 수 있습니다 intent에서 결과를 되돌려주고 그걸 앱에서 열어주는 건 흔한 패턴입니다 앱 Intents에는 이걸 표현하는 내장 방식이 있는데 그걸 openIntent로 부릅니다 제가 openIntent를 추가하면 고객들은 Shortcuts에 새 스위치를 얻을 텐데 그걸 'Open When Run'이라 합니다 그들이 그 스위치를 꺼버리면 배경에서 단축어의 일환으로 방해받지 않고 이 intent를 쓸 수 있습니다 그들이 스위치를 켜 놓으면 새롭게 추가된 책이 제 Library 앱에서 즉시 열립니다 openIntent의 채택은 Open Book intent의 인스턴스를 만들고 결과의 일부로 그걸 돌려주는 것만큼 쉽습니다 이 intent가 돌아갈 때 Open When Run 스위치가 켜져 있으면 Open Book intent는 Add Book intent가 끝난 후 자동으로 수행됩니다 개체들과 쿼리들로 여러분이 할 수 있는 일은 더 많습니다 다음 API들의 집합으로 AppIntents는 SiriKit Intents 프레임워크에선 여러분이 갖춰본 적 없는 강력한 능력을 가능하게 합니다 여러분이 개체들에서 더 많은 정보를 노출할 수 있는 방법을 살펴보겠습니다 그리고 고객들이 그걸 바탕으로 찾고 필터링할 수 있는 방법도요 지금까지 저는 제 책 개체에 모든 기본 요건을 추가했습니다 하지만 사람들이 자신의 단축어에 책을 더 깊이 통합하게 하려면 저는 제 책에 관한 걸 조금 더 많이 노출해야 합니다 개체들은 속성들을 지원하는데 거기엔 여러분이 사용자들에게 노출하길 원하는 개체에 관한 추가 정보가 들어 있습니다 이 경우에 저는 책의 저자와 출간 일자와 읽은 날짜와 추천한 사람을 넣어서 사람들이 자신의 단축어에 그 속성들을 쓸 수 있게 하죠 저는 @Property라는 속성 래퍼를 사용해서 제 BookEntity에 속성을 추가합니다 속성들은 파라미터가 지원하는 모든 타입을 지원하고 각각은 로컬화된 제목을 취합니다 이 새로운 속성들로 제 고객들은 이제 Shortcuts에서 마법의 변수들을 사용하여 책 개체를 갖고 작업할 때 각각의 새 정보를 빼낼 수 있습니다 앞에 나왔던 Add Book intent를 사용할 때 그들은 자신의 단축어에 새로 추가된 책의 저자나 출간 일자를 사용할 수 있습니다
여러분이 속성을 쿼리와 결합하면 여러분의 앱은 Shortcuts의 대단히 강력한 Find 및 Filter 동작을 자동으로 얻습니다 이 유연한 프레디케이트 편집기 UI로요 이제 제 고객들은 읽은 날짜, 제목 저자 등을 바탕으로 책을 찾고 필터링할 수 있습니다 예를 들어 Delia Owens가 쓴 모든 책을 찾는 건 정말 쉽습니다 Sort by와 Limit 옵션을 사용해서 여러분은 더 많은 고급 쿼리를 지원할 수 있습니다 Delia Owens가 쓴 가장 최근에 출간된 책 세 권을 찾는 것처럼요 고객은 이런 구성 요소들을 사용해서 아주 멋진 것들을 할 수 있습니다 자신의 컬렉션에서 가장 흔한 세 명의 저자를 찾는 것처럼요 이 많은 걸 가능하게 하려면 저는 또 다른 종류의 쿼리인 속성 쿼리란 걸 채택해야 합니다 속성 쿼리는 문자열이나 식별자를 바탕으로가 아니라 개체 내 속성을 바탕으로 개체를 찾습니다 속성 쿼리를 실행하는 데는 3단계가 있습니다 첫째, 여러분이 쿼리 속성을 선언합니다 그건 속성들을 써서 여러분의 개체가 검색될 방법을 명시합니다 그런 다음 정렬 옵션을 추가하는데 그건 쿼리 결과가 정렬될 수 있는 방법을 정의합니다 그리고 마지막으로 entities(matching:)를 실행해서 검색을 돌립니다 쿼리 속성들은 AppIntents가 이 쿼리와 연관된 개체에서 검색할 수 있는 모든 방법을 선언합니다 각각은 제 개체의 속성을 나열합니다 비교 연산자들의 속성도요 그것에서 이용 가능한 포함한다, 같다, 보다 작다처럼요 여기에서 저는 '보다 작다' '보다 크다' 연산자를 제 날짜 속성에 나열하고 '포함한다', '같다'를 제 제목 속성에 나열합니다 쿼리 속성은 속성과 비교기로 이루어진 각각의 조합을 여러분이 선택하는 타입으로 매핑하는데 그걸 비교기 매핑 타입이라 합니다 여기에서 저는 CoreData를 쓰니 NSPredicate를 사용하겠습니다 제가 맞춤형 데이터베이스나 REST API를 사용한다면 저는 저만의 비교기 타입을 디자인해서 그걸 대신 써도 됩니다 이건 제 책을 위한 쿼리 속성들을 구성하기 위한 코드입니다 저는 BooksQuery가 EntityProperty Query 프로토콜을 따르게 합니다 그리고 정적 변수 속성을 실행합니다 QueryProperties 결과 빌더를 사용해서요 각각의 입력이 명시하는 건 질의받을 수 있는 Property의 keyPath와 그것 내에서 그 속성에 적용될 수 있는 각각의 비교기입니다 저는 각각의 비교기에 대해 NSPredicate를 제공합니다 NSPredicate를 제 비교기 매핑 타입으로 선택했기 때문이죠 시스템이 제 앱에 쿼리에 대한 결과를 돌려달라고 요청할 때 그건 제가 여기에서 구축하는 NSPredicates를 다시 제공해줍니다 정렬에도 유사한 정의가 있습니다 이건 제 모델이 책을 정렬할 수 있는 모든 속성의 목록입니다 이 경우에 저는 제목, 읽은 날짜 출간 일자별로 정렬를 허용합니다 마지막으로 저는 entities(matching:)를 실행합니다 그건 제 데이터베이스에 질의하고 일치하는 개체들을 되돌려줍니다 이 기법은 이전에 정의된 쿼리 파라미터에서 제가 썼던 비교기 매핑 타입의 배열을 가져갑니다 이 경우에는 NSPredicate입니다 이 프레디케이트들은 제가 질의하고 싶은 제 개체의 속성들에 관한 기준을 설명해줍니다 그건 모드를 취하기도 해서 표시하는 건 프레디케이트를 'and'와 'or' 중 뭘로 결합해야 할지와 졍렬 기준인 핵심 경로와 결과의 수에 대한 선택적인 제한입니다 제 실행은 이런 파라미터를 써서 제 CoreData 데이터베이스에 대한 쿼리를 수행합니다 고객들은 이 속성 쿼리로 뭘 할 수 있을까요? 라이브러리에서 임의의 책을 골라 읽을 수 있습니다 자신의 책 중 20세기 초반에 출간된 모든 걸 찾을 수 있습니다 그들은 Shortcuts 환경을 활용해서 제 앱을 다른 사람들에게 연결해서 더욱 유용하게 만들 수 있습니다 예를 들어 그들은 스프레드시트 앱을 써서 그들이 올해 읽은 모든 책을 CSV file로 내보낼 수 있습니다 아니면 그래프 작성 앱을 써서 지난 10년 동안 매년 읽은 책의 수에 관한 차트를 만들 수 있습니다 그건 시작에 불과합니다 이런 종류의 심도 있는 앱 Intents 채택은 고객들이 여러분의 앱을 써서 앱에서 하길 원하는 걸 하게 해주며 그걸 그들의 작업 흐름에 결정적인 부분이 되게 합니다 이 통합들 각각은... 예를 들어 그래프 작성 같은 건 여러분이 구축하지 않아도 되는 기능입니다 여러분의 intent가 수행되면 여러분의 앱은 사용자와 상호 작용을 해서 결과를 보여주거나 말해주거나 모호함을 해결해줘야 할 수 있어요 그게 Siri 요청이든 단축어든요 앱 Intents는 다음과 같은 수많은 상호 작용을 지원합니다 intent가 완료됐을 때 사용자에게 텍스트 및 음성 피드백을 전하기 위한 대화와 시각적 피드백을 전하기 위한 스니펫들 값 요청과 사용자에게 intent 파라미터의 값을 분명하게 해달라고 요청하는 명확화와 파라미터 값 입증에 대한 확인이나 업무와 관련됐거나 지장을 주는 intent들에 관해 사용자와 확인하는 겁니다 대화는 intent를 돌리는 사람에게 구두 또는 텍스트 반응을 제공합니다 음성 체험에서 intent의 효과가 좋으려면 대화를 제공하는 게 정말 중요합니다 앞서 나왔던 제 Add Book intent에 needsValueDialog를 추가하겠습니다 그건 책 제목을 요청할 때 말로 하는 거죠 그리고 제 수행 기법에서 돌려받은 결과 대화도 추가하겠습니다 그것들은 Shortcuts나 Siri가 수많은 우리의 플랫폼에서 읽거나 보여주는 겁니다 스니펫은 대화를 시각화한 것으로 생각하면 되는데 intent의 결과에 시각적 표현을 추가할 수 있게 해줍니다 스니펫을 사용하려면 여러분의 intent 결과에 뒤따르는 종료로 여러분이 선택한 SwiftUI 뷰를 추가하세요 위젯의 경우처럼 SwiftUI 뷰도 파일로 보관되어 Shortcuts나 Siri로 전송됩니다 앱 Intents는 requestValue를 내보내서 사용자에게 값을 요청하는 걸 지원합니다 예를 들어 그건 때로 선택적인 파라미터에 대한 값이 필요할 때 쓸모가 있습니다 여기에서 requestValue는 제 문자열 검색이 책 한 권을 초과하는 결과를 돌려줄 때 저한테 도움이 됩니다 이 경우에 전 창을 뜨게 해서 저자를 물어봅니다 책 검색 범위를 줄이기 위해서요 requestValue는 제가 내보낼 수 있는 오류를 주는데 그건 사용자에게 창을 뜨게 하고 업데이트된 저자명으로 그 동작을 다시 돌립니다 한편 명확화는 파라미터에 대한 값들의 집합에서 사용자가 선택하게 해야 할 때 아주 좋습니다 그건 제 Add Book 동작에서 다수의 가능한 결과를 처리할 더욱 좋은 방법을 저한테 줍니다 여기에선 생성된 책들에서 저는 저자명의 목록을 얻고 그 가능한 값들에 대한 명확화를 요청합니다 사용자는 그것들 사이에서 선택할 것을 요청받고 저는 그 결과를 되돌려받습니다 마지막으로 앱 Intents는 두 종류의 확인을 지원합니다 첫 번째 종류는 파라미터 값의 확인입니다 여러분이 이걸 쓸 때는 어떤 값에 대한 당연한 추측치가 있는데 그걸 확인하고 싶을 때입니다 그냥 확실하게 하려고요 책을 추가할 때 제목별로 책을 찾아보기 위해 제가 호출하는 웹 서비스가 두 개의 일치 결과를 돌려주는데 그중 하나가 월등하게 인기가 많을 때가 있습니다 그런 경우 저는 가정합니다 사용자가 그 인기 있는 책을 추가하려고 했다고요 하지만 확인을 추가해서 제 생각이 맞는지 확실하게 하죠 그러기 위해 저는 제목 파라미터에 requestConfirmation을 호출합니다 두 번째 종류는 intent의 결과에 대한 확인입니다 이건 예를 들어 주문을 하는 데 참 좋습니다 제가 제 Library 앱을 유료화하고 서점을 통해 주문하기를 추가하고 싶다면 주문을 제대로 받았는지 확실히 하는 게 좋겠죠 이를 위해 저는 제 intent에 requestConfirmation을 호출하고 해야 할 주문을 전달할 수 있어요 저는 여기에 주문의 미리 보기를 보여주는 스니펫도 명시하겠습니다 저는 호출 앞에 'try'를 붙입니다 requestConfirmation은 사용자가 확인하는 대신 취소하면 오류를 내보내거든요 제 발표를 마치기 전에 앱 Intents 아키텍처의 두 가지 측면을 다루고 싶습니다 여러분은 그 프레임워크를 채택할 때 그걸 알아야 합니다 앱 Intents 구축에는 두 가지 방법이 있습니다 앱 내에서 아니면 별도의 확장 프로그램에서입니다 이들 중 intents를 앱 내에서 직접 실행하는 게 가장 단순합니다 이게 참 좋은 이유는 프레임워크가 필요 없고 코드 복제를 할 필요도 없기 때문입니다 그리고 프로세스들을 조정할 필요도 없습니다 여러분의 앱을 사용하면 메모리 한계가 높아지고 확장 프로그램에선 하기가 더 힘든 종류의 일을 여러분이 할 수 있게 해줍니다 오디오 재생처럼요 여러분이 intent에서 openAppWhenRun을 실행해서 참을 돌려주게 한다면 앱은 전경에서 실행될 수 있습니다 그 외의 경우에는 배경에서 돌아갑니다 배경에서 돌아갈 때 여러분의 앱은 특수 모드에서 시작됩니다 성능 극대화를 위해 장면들이 나오지 않는 채로요 실제로 여러분이 배경 앱 intents를 여러분의 앱에서 실행하면 우린 여러분이 장면 지원도 실행하길 강력하게 권합니다 또는 여러분은 확장 프로그램에서 앱 intents를 구축할 수 있습니다 여기에는 두 가지 장점이 있습니다 무게가 더 가볍습니다 확장 프로그램 프로세스가 앱 intents만 처리하고 여러분의 앱을 스피닝하는 걸 요구하지 않기 때문입니다 여러분이 Focus intents를 처리한다면 확장 프로그램을 쓰는 건 Focus가 바뀔 때 intents가 확장 프로그램에서 즉시 수행되게 할 거란 뜻이기도 합니다, 여러분의 앱이 먼저 전경에서 돌아간다는 요건 없이도요 확장 프로그램에선 일을 좀 더 해야 합니다 새로운 목표물을 추가하고 프레임워크로 코드를 이동시키고 여러분의 앱과 확장 프로그램 사이의 조정을 처리해야 하니까요 앱 Intents 확장 프로그램을 만들려면 Xcode에서 File > New Target으로 간 다음 App Intents Extension을 선택하세요 앱 Intents로 여러분의 코드가 유일한 진리의 근원입니다 앱 Intents는 이런 품격 있는 개발자 체험을 이룹니다 여러분의 intent와 개체와 쿼리와 파라미터를 구축 시간에 정적으로 추출하는 방식으로요 Xcode는 구축 프로세스 동안 여러분의 앱이나 확장 프로그램 번들 내에 메타데이터를 생성하는데 거기에는 Swift 컴파일러에서 수신한 정보가 담겨 있습니다 그게 여러분의 코드에서 돌아갈 때요 이 모든 걸 작동하게 하려면 여러분의 앱 Intents 타입을 프레임워크가 아닌 목표물이나 확장 프로그램 내에 보관하세요 마찬가지로 로컬화된 문자열도 여러분의 앱 Intents 타입이 있는 동일한 번들 내의 문자열 파일에서 발견돼야 합니다 업그레이드하면 좋은 SiriKit Intents가 있는 기존 앱을 갖고 있는 분들의 경우 intents를 채택해서 위젯이나 메시징 혹은 미디어 같은 도메인을 통합시키려면 SiriKit Intents 프레임워크를 계속 사용해야 합니다 하지만 Siri와 Shortcuts를 위한 맞춤형 intents를 추가하면 앱 Intents로 업그레이드해야 합니다 SiriKit Intents 정의 파일에 있는 Convert to App Intent 버튼을 눌러서 업그레이드 과정을 시작하면 됩니다 앱 Intents로 Shortcuts에 여러분의 앱을 통합시키는 건 개발자로서 자신의 영항력을 극대화하기에 참 좋은 방법입니다 앱 Intents를 채택하기 위한 노력을 조금만 함으로써 여러분은 고객들을 위한 가치를 많이 창출하기 때문입니다 함께해주셔서 감사합니다 여러분이 지금 앱 Intents를 써보고 피드백을 주시면 좋겠습니다 이 새로운 프레임워크가 여러분의 앱을 쓰는 사람들을 놀라게 하고 기쁘게 하고 그들에게 권한을 줄 수 있다는 게 기쁩니다 독서 즐겁게 하시고 이번 WWDC가 굉장하길 바랍니다 ♪
-
-
5:33 - Open Currently Reading
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 }
-
6:42 - App Shortcuts
struct LibraryAppShortcuts: AppShortcutsProvider { static var appShortcuts: [AppShortcut] { AppShortcut( intent: OpenCurrentlyReading(), phrases: ["Open Currently Reading in \(.applicationName)"], systemImageName: "books.vertical.fill" ) } }
-
7:11 - Shelf Enum
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", ] }
-
7:49 - Open Shelf
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 }
-
10:56 - Book Entity
struct BookEntity: AppEntity, Identifiable { var id: UUID var displayRepresentation: DisplayRepresentation { "\(title)" } static var typeDisplayRepresentation: TypeDisplayRepresentation = "Book" static var defaultQuery = BookQuery() }
-
12:29 - Book Query
struct BookQuery: EntityQuery { func entities(for identifiers: [UUID]) async throws -> [BookEntity] { identifiers.compactMap { identifier in Database.shared.book(for: identifier) } } }
-
13:16 - Open Book
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 } }
-
13:40 - Suggested Entities and String Book Query
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()) } } }
-
15:11 - Add Book 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" } } } }
-
18:21 - Book Entity with Properties
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 } }
-
20:59 - Books 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(for identifiers: [UUID]) async throws -> [BookEntity] { identifiers.compactMap { identifier in Database.shared.book(for: identifier) } } func suggestedEntities() async throws -> [BookEntity] { Model.shared.library.books.map { BookEntity(id: $0.id, title: $0.title) } } func entities(matching string: String) async throws -> [BookEntity] { Database.shared.books.filter { book in book.title.lowercased().contains(string.lowercased()) } } 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) }) } }
-
24:10 - Dialog
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> & ProvidesDialog { 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, dialog:"Added \(book) to Library!" ) } enum Error: Swift.Error, CustomLocalizedStringResourceConvertible { case notFound var localizedStringResource: LocalizedStringResource { switch self { case .notFound: return "Book Not Found" } } } }
-
24:25 - Snippet
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 & ShowsSnippetView { 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) { CoverView(book: book) } } enum Error: Swift.Error, CustomLocalizedStringResourceConvertible { case notFound var localizedStringResource: LocalizedStringResource { switch self { case .notFound: return "Book Not Found" } } } }
-
24:50 - Request Value
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 { let books = await BooksAPI.shared.findBooks(named: title, author: authorName) guard !books.isEmpty else { throw Error.notFound } if books.count > 1 && authorName == nil { throw $authorName.requestValue("Who wrote the book?") } return .result() } enum Error: Swift.Error, CustomLocalizedStringResourceConvertible { case notFound var localizedStringResource: LocalizedStringResource { switch self { case .notFound: return "Book Not Found" } } } }
-
25:22 - Request Disambiguation
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 { let books = await BooksAPI.shared.findBooks(named: title, author: authorName) guard !books.isEmpty else { throw Error.notFound } if books.count > 1 { let chosenAuthor = try await $authorName.requestDisambiguation(among: books.map { $0.authorName }, dialog: "Which author?") } return .result() } enum Error: Swift.Error, CustomLocalizedStringResourceConvertible { case notFound var localizedStringResource: LocalizedStringResource { switch self { case .notFound: return "Book Not Found" } } } }
-
25:48 - Request Parameter Confirmation
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> { guard var book = await BooksAPI.shared.findBooks(named: title, author: authorName).first else { throw Error.notFound } let confirmed = try await $title.requestConfirmation(for: book.title, dialog: "Did you mean \(book)?") book.recommendedBy = recommendedBy Database.shared.add(book: book) return .result(value: book) } enum Error: Swift.Error, CustomLocalizedStringResourceConvertible { case notFound var localizedStringResource: LocalizedStringResource { switch self { case .notFound: return "Book Not Found" } } } }
-
26:26 - Request Result Confirmation
struct BuyBook: AppIntent { @Parameter(title: "Book") var book: BookEntity @Parameter(title: "Count") var count: Int static var title: LocalizedStringResource = "Buy Book" func perform() async throws -> some IntentResult & ShowsSnippetView & ProvidesDialog { let order = OrderEntity(book: book, count: count) try await requestConfirmation(output: .result(value: order, dialog: "Are you ready to order?") { OrderPreview(order: order) }) return .result(value: order, dialog: "Thank you for your order!") { OrderConfirmation(order: order) } } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.