스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Apple Watch에서 스마트 스택용 위젯 빌드하기
최신 SwiftUI와 WidgetKit API를 사용하여 watchOS 10에서 스마트 스택용 위젯을 빌드하는 과정을 따라해 보세요. Apple Watch에서 관련 정보를 표시하는 위젯을 생성하는 팁과 기술, 모범 사례를 알아봅니다.
챕터
- 0:58 - Get started
- 2:03 - Configure the widget
- 3:51 - Set up the timeline
- 10:16 - Build widget views
- 16:27 - Finish building timeline
- 19:58 - Provide relevant intents
리소스
관련 비디오
WWDC23
-
다운로드
♪ ♪
안녕하세요 캘빈 게이스퍼드입니다 watchOS 팀 소속 엔지니어죠 이번 코딩 세션에서는 새 Apple Watch 스마트 스택용으로 위젯을 빌드해 보겠습니다 AppIntent 구성을 사용해 위젯을 빌드하는 전 프로세스를 자세히 살펴볼 거예요 프로세스에서 최신 WidgetKit와 SwiftUI 업데이트를 사용하게 됩니다
Backyard Birds 앱으로 코딩해 보도록 하죠 Backyard Birds는 새 방문객이 모여드는 뒤뜰을 생성하고 관리하는 앱입니다 뒤뜰의 상태를 위젯에서 보여 줄 거예요 새가 뒤뜰을 방문하면 그 새를 보여 주고 뒤뜰의 상태도 표시합니다 위젯은 스마트 스택에 관련 날짜를 제공하므로 가장 관련성이 높을 때 위젯을 우선으로 처리할 수 있죠 함께 따라 해 보고 싶다면 이번 세션과 관련된 샘플 코드를 다운로드해서 Backyard Birds Xcode 프로젝트를 열어 보세요 프로젝트에 이미 위젯 확장을 추가해서 BackyardVisitorsWidget을 비롯한 파일 몇 개를 생성했습니다 이 파일을 업데이트하는 데 많은 시간을 할애할 거예요 오늘 배울 내용을 알아봅시다 우선 위젯을 정의하는 위젯 구조를 살펴볼 겁니다 위젯 구성 인텐트 개요도 간략히 살펴본 뒤 TimelineEntry 구조를 사용자화해 위젯 뷰 데이터를 보유하고 타임라인을 빌드해 볼게요 프리뷰 활성화에 필요한 데이터가 타임라인에 생기면 위젯 뷰를 빌드할 겁니다 뷰를 빌드하고 나서 타임라인을 마무리하러 돌아갈게요 마지막으로 Relevant Intent Manager를 살피고 watchOS 스마트 스택에서 위젯을 우선으로 처리해야 하는 날짜에 인텐트를 설정할 겁니다 코드의 위젯 구조를 살펴보면서 위젯 구성을 시작해 봅시다
위젯 구조는 위젯 구성을 정의하는 곳입니다 watchOS에 새로 도입된 AppIntentConfiguration을 위젯에서 사용해 보도록 하죠 구성 인텐트와 공급자, 뷰는 위젯 확장을 생성할 때 모두 삭제됐습니다 하나씩 살펴보고 Backyard Birds 위젯에 구현해 볼 겁니다 이 위젯 정의는 양호하네요 WidgetConfigurationIntent를 이어서 살펴봅시다 이 위젯은 App Intent 구성을 사용해서 두 가지 작업이 가능해요 우선 watchOS 위젯 갤러리에서 사전 설정 위젯을 제공합니다 Backyard Birds의 경우 앱의 뒤뜰마다 각각의 구성을 제공하겠죠 또한 WidgetConfigurationIntent는 위젯이 가장 관련성이 높은 날짜를 지정하는 데 사용됩니다 스마트 스택은 해당 정보를 사용해서 위젯을 우선으로 처리하죠
위젯의 구성 App Intent를 살펴보도록 합시다 Backyard Birds의 뒤뜰은 저마다 고유한 ID가 있습니다 미리 추가해 둔 매개변수인 backyardID를 사용해 위젯 인텐트를 생성하겠습니다 각각의 ID로 식별되는 뒤뜰마다 위젯 인텐트가 생기겠죠 이번 시간에 위젯을 빌드하는 데 필요한 매개변수는 이것뿐입니다 App Intent를 자세히 알아보고 WidgetConfigurationIntent를 활용하는 방법을 알고 싶다면 App Intent와 관련된 아래 세션들을 참고해 보세요 위젯 구조가 정의돼 있고 backyardID를 보유하는 WidgetConfigurationIntent도 있습니다 이제 위젯 타임라인으로 가서 TimelineEntry 구조를 살펴봅시다 TimelineEntry 구조는 위젯 뷰가 특정 날짜에 렌더링 시 필요한 데이터를 모두 보유합니다 BackyardVisitorsWidget 파일로 돌아가서 생성된 SimpleEntry 구조를 찾아볼게요
파일이 생성될 때 date와 configuration 프로퍼티가 추가됐습니다 위젯 뷰에 필요한 여타 프로퍼티를 추가로 정의해야 합니다 뒤뜰 상태를 위젯으로 표시할 겁니다 뒤뜰 이름과 함께 음식과 물 상태가 표시되겠죠 방문객이 생기면 방문한 새의 모습과 이름을 알려 주고 뒤뜰에 방문한 새가 없을 때는 누적 방문객 수를 보여 줍니다 뒤뜰 정보를 표시하려면 Backyard Birds 앱에서 특정 시점의 뒤뜰 정보를 모두 보유하는 구조를 사용하게 됩니다 TimelineEntry에 미래 날짜가 있을 수 있으므로 이는 중요합니다 TimelineEntry에 backyard 프로퍼티를 추가해 보죠
이제 방금 추가한 backyard 프로퍼티에 기반해 연산 프로퍼티를 몇 가지 추가해 봅시다 우선 bird 프로퍼티를 추가해 위젯 뷰에서 새가 방문했는지 확인하고 표시할 수 있도록 할게요
뒤뜰에 대한 정보를 더 많이 보여 줄 수 있도록 뷰에서 사용할 프로퍼티를 두 가지 더 추가하겠습니다
waterDuration과 foodDuration을 뷰에서 사용해 물과 음식이 얼마나 지속될지 보여 주도록 할게요 TimelineEntry의 date 프로퍼티에서 해당 내용을 계산합니다
TimelineEntry에는 relevance라는 프로퍼티도 있어요 이 프로퍼티를 구현하면 watchOS 스마트 스택에 가장 중요한 타임라인 엔트리를 알려 줄 수 있죠 TimelineEntry에 relevance 프로퍼티를 추가할게요
내부에서는 TimelineEntry 날짜에 뒤뜰에 들른 방문객이 있는지 확인해 보겠습니다
방문객이 있으면 TimelineEntryRelevance 구조를 반환할 겁니다
TimelineEntryRelevance 구조는 매개변수 두 개를 취합니다 score와 duration인데요 score는 특정 엔트리를 같은 타임라인 내 다른 엔트리보다 우선 처리 할 때 사용됩니다 방문객이 없는 엔트리보다 방문객이 있는 엔트리가 더 높은 순위를 차지하도록 값을 10으로 설정할게요 값은 임의로 잡으면 됩니다 타임라인 엔트리 순위를 매기는 데 필요한 범위라면 어떤 값이든 괜찮아요 duration은 스마트 스택에 relevance 엔트리의 유효 시간을 알려 줍니다 방문객 종료일까지 지속되도록 duration의 값을 설정해 뒀어요 방문객이 없다면 TimelineEntryRelevance 구조를 0점으로 반환하도록 합시다
이렇게 설정하면 스마트 스택에서 어떤 타임라인 엔트리가 가장 중요한지 알 수 있죠 그 외 상황에 따라 watchOS 스마트 스택에서 위젯의 우선순위가 높아질 수 있습니다
TimelineEntry이 근사하네요 위젯 뷰가 렌더링하는 데 필요한 모든 내용이 담겨 있습니다 그럼 이제 TimelineProvider를 빌드해 봅시다 TimelineProvider를 위해 함수 네 가지를 작성해야 합니다 placeholder와 snapshot timeline, recommendations입니다 placeholder 함수는 위젯이 처음 표시되고 빨리 반환해야 할 때 사용됩니다 backyard 프로퍼티를 취하도록 TimelineEntry를 업데이트했으므로 하나를 제공해야겠죠 앱 데이터 모델에서 임의의 뒤뜰을 추가해 수정할게요
좋네요 다음으로 넘어가죠 snapshot 함수는 위젯이 일시적인 상황에 있을 때 사용됩니다 이 함수는 빨리 반환해야 하므로 샘플 데이터를 사용해도 됩니다 페칭하는 데 몇 초 이상 걸리지 않는다면요 placeholder 함수와 동일하게 작업하고 임의의 뒤뜰을 전달하면 됩니다
이쯤 해도 나쁘지는 않지만 좀 아쉽기는 하네요 snapshot 함수는 앞서 추가했던 backyardID 프로퍼티를 갖는 구성 인텐트를 받습니다 데이터는 모두 로컬이기 때문에 임의의 뒤뜰을 사용하는 대신 적절한 뒤뜰을 빠르게 검색하고 반환할 수 있습니다 구성의 backyardID에서 설정된 뒤뜰을 가져와 봅시다
뒤뜰을 살펴보고 visitorEvent를 얻을 수 있을지 확인해 보죠
방문객 날짜로 설정된 엔트리를 반환하고 방문객이 없다면 설정된 뒤뜰을 현재 날짜로 반환할게요
이렇게 하면 설정된 뒤뜰을 보여 주므로 더 나은 프리뷰를 사용자에게 제공하게 됩니다 timeline 함수를 살펴보기 전에 Xcode 캔버스 프리뷰를 열어 봐야겠어요 마지막 SimpleEntry를 수정하고 프리뷰를 볼 수 있도록 뒤뜰을 제공하겠습니다
캔버스를 열어 보죠
위젯 타임라인 프리뷰는 Xcode에 새로 생긴 기능입니다 직사각형 위젯 프리뷰가 캔버스에 나타나고 위젯 타임라인을 구성하는 타임라인 엔트리가 하단에 늘어서 있습니다 캔버스 프리뷰는 위젯을 추가할 때 생성했던 기본 뷰를 사용합니다 타임라인 공급자를 끝마치기 전에 뷰를 빌드하면서 타임라인을 더 근사하게 시각화해 봅시다 BackyardBirdsWidgetEntryView를 찾습니다 WidgetFamily에 대한 환경 프로퍼티를 추가해서 위젯 크기별 뷰를 빌드해 보죠
본문을 switch문으로 바꿔서 accessoryWidget의 크기별로 뷰를 구현해 볼게요
이제 엔트리를 매개변수로 취하는 뷰를 사용해 accessoryRectangular 사례를 생성해 보겠습니다 이 뷰는 아래에 구현할게요
watchOS 스마트 스택에서 위젯의 뷰로 표시된다는 점에서 직사각형 뷰는 독특합니다 직사각형 뷰는 일반적인 패턴을 따라 이미지는 왼편에 텍스트 세 줄은 오른편에 띄울 겁니다 파일 하단으로 이동해서 RectangularBackyardView를 생성하겠습니다
뒤뜰 데이터를 포함하도록 앞서 변경했던 TimelineEntry를 뷰에서 사용할 겁니다 작업을 계속 진행하기 전에 캔버스 뷰를 스마트 스택 직사각형 뷰로 전환하겠습니다
위젯을 빌드하면서 직접 눈으로 확인할 수 있도록요 이제 이미지와 텍스트 세 줄을 뷰의 HStack에 넣어 봅시다
프리뷰를 확인해 보죠 원하는 모습이 아니군요 텍스트를 VStack에 넣을게요
좋습니다, 훨씬 낫네요 실제 데이터를 엔트리에서 뷰에 넣겠습니다 우선 Backyard Birds 앱에서 새를 표시하는 ComposedBird 뷰를 사용합시다
새는 선택 사항이므로 언래핑해야 합니다 ComposedBird 뷰와 VStack을 if-let문에 넣어서 엔트리에 새가 있는지 확인하겠습니다
새가 없다면 뒤뜰의 분수 이미지를 넣고 새가 없다는 문구를 표시하도록 합시다
이제 타임라인을 쭉 살펴보면 새와 함께 텍스트 세 줄이 나오거나 새가 없다고 알려 주는 엔트리를 확인할 수 있죠
새가 있는 경우 상세 내용을 작성해 봅시다 첫 번째 줄은 새의 이름 두 번째 줄은 뒤뜰 이름 세 번째 줄은 뒤뜰의 음식과 물 상태를 나타내도록 할게요
새가 없는 경우 뒤뜰 이름을 알려 주고 음식과 물 상태를 알린 뒤 누적 방문객 수를 표시하도록 하죠
엔트리가 어떻게 보이는지 확인해 봅시다
아주 좋습니다 레이아웃을 좀 수정할게요 우선 ComposedBird를 업데이트합시다 scaledToFit으로 뷰가 화면에 차도록 하고 색상이 있는 시계 페이스에서 같은 색상이 적용되도록 widgetAccentable을 설정해 볼게요 새 이름에도 헤드라인 서체를 추가하며 확장하도록 설정하고 시계 페이스와 색상이 같도록 위젯 수용성을 설정합니다 또한 텍스트 색상은 새의 날개 색상과 같도록 foregroundStyle을 사용할게요
이름이 길어질 경우를 대비해 나머지 두 뷰 모두에 스케일 팩터를 추가합니다
마지막 줄의 foregroundStyle은 secondary로 설정합시다
마지막으로 세 텍스트 뷰가 정렬되도록 스택 행간을 정렬합니다
뷰가 아주 근사하네요 새가 없을 때 else문 뷰에 똑같은 업데이트를 적용합시다
위젯이 더 보기 좋아졌네요
새와 뒤뜰을 표시할 때 자간이 다르군요 새 뷰와 이미지 뷰에 프레임을 추가해서 자간을 똑같이 맞춰 봅시다
VStack에도 프레임을 추가해 적절히 정렬하도록 할게요
watchOS 스마트 스택용 위젯을 마무리하려면 옵션을 하나 더 추가해야 합니다 containerBackground는 새로운 SwiftUI 기능입니다 containerBackground를 뒤뜰의 그레이디언트로 바꿔 보죠 containersBackground 배치를 위젯으로 설정할게요
containerBackground는 시스템에서 선택적으로 사용되며 watchOS 스마트 스택에만 표시되고 시계 페이스에는 나오지 않습니다
watchOS 스마트 스택을 위한 뷰가 이제 준비되었습니다 뷰는 그럴듯해 보이니까 TimelineProvider로 돌아가서 타임라인을 마저 빌드해 볼게요
timeline 함수는 위젯이 뷰를 렌더링하는 데이터가 포함된 타임라인 엔트리 모음을 생성하는 곳입니다 위젯의 주력 함수죠 지금은 임의의 뒤뜰 데이터로 엔트리 다섯 개를 생성 중입니다 새 방문객이 가득한 타임라인으로 교체해 보겠습니다 함수 맨 위에 있는 타임라인 엔트리 배열을 사용해 타임라인을 빌드할 겁니다 우선 생성된 타임라인 코드를 지우겠습니다
이제 backyardID를 사용해 ConfigurationAppIntent에서 설정된 뒤뜰을 가져옵니다
뒤뜰 구조에는 모든 방문객 이벤트를 포함하는 프로퍼티가 있습니다 회수된 뒤뜰에 대한 방문객 이벤트를 반복해 봅시다 이벤트별로 방문객 이벤트 시작일을 포함하는 타임라인 엔트리를 생성하고 설정된 뒤뜰을 전달해 보죠
타임라인 프리뷰가 업데이트됐어요 변경 사항을 살펴봅시다 이제 타임라인 엔트리를 선택하면 새가 나타납니다 예상한 대로 말이죠 하지만 모든 엔트리에 방문객이 있군요 새가 떠났을 때의 엔트리도 추가해야 합니다 두 번째 엔트리를 생성하고 방문객 이벤트 종료일을 사용하죠 동일한 뒤뜰을 사용하고 엔트리 배열에 해당 엔트리를 추가할게요
타임라인을 확인해 봅시다
좋습니다 새가 오고 갈 때 모두 엔트리가 있어요 위젯 타임라인이 훌륭해 보이는군요 새로운 타임라인 프리뷰는 정말 멋져요 위젯과 타임라인을 빌드하기가 훨씬 쉬워졌습니다
마지막으로 타임라인 공급자의 recommendations 함수를 구현해 봅시다 여기에서 backyardID를 보유하는 WidgetConfigurationIntent를 포함할 AppIntentRecommendations 배열을 반환해야 합니다 기본 구현은 삭제할게요
반환할 recommendations 배열을 생성하겠습니다
이어서 앱의 뒤뜰마다 권장 사항을 생성할 수 있도록 모든 뒤뜰에서 반복해 봅시다
뒤뜰마다 ConfigurationAppIntent를 생성해 backyardID를 설정합니다
마지막으로 ConfigurationIntent를 사용해 AppIntentRecommendation을 생성하고 배열에 추가합니다 뒤뜰 이름을 설명으로 사용할게요
이제 recommendations 함수는 Backyard Birds 위젯을 사용자가 선택할 때 위젯 갤러리에서 뒤뜰마다 위젯 구성 목록을 제공합니다 고생하셨어요 이렇게 watchOS에서 위젯을 빌드해 봤습니다 위젯은 시계 페이스 컴플리케이션과 watchOS 스마트 스택으로 표시될 겁니다 앞서 TimelineEntry에서 relevance 프로퍼티를 구현할 때 관련성을 가볍게 다뤘는데 가능한 작업이 더 많습니다 Backyard Birds 앱에서 각 뒤뜰은 새가 먹고 마실 수 있는 음식과 물 상태를 계속 확인합니다 새로운 위젯은 해당 정보도 표시할 겁니다 물과 음식이 바닥날 시기에 관련 있는 인텐트 목록을 시스템에 제공하면 됩니다 해당 기간에는 위젯이 우선으로 처리되고 뒤뜰 관리가 필요하다는 사실을 사용자에게 알립니다
코드로 돌아가서 새로운 함수를 생성해 가능한 위젯 모두에 관련 있는 인텐트를 빌드한 뒤 이 인텐트들을 사용해 RelevantIntentManager를 업데이트하겠습니다 생성할 함수의 이름은 updateBackyard RelevantIntents입니다
이 함수에서는 relevantIntents 배열이 필요해요
RelevantIntentManager를 해당 배열로 업데이트합니다
relevantIntents 배열을 작성하려면 앱의 모든 뒤뜰에 루프를 걸어야 합니다 이어서 뒤뜰에 대한 configurationIntent를 생성하고 현재 뒤뜰에 backyardID를 설정할게요 날짜를 기반으로 RelevantContext를 생성합니다 이번 사례에서는 향후 뒤뜰 음식이 부족한 날짜와 음식이 고갈되는 날짜를 사용하겠습니다
마지막으로 relevantIntent를 생성할게요 위젯의 configurationIntent와 위젯의 종류 그리고 방금 생성한 relevantDateContext를 사용해 배열에 추가하도록 합시다
물 부족 날짜와 물 고갈 날짜도 똑같이 작업할게요
이 정도면 되겠어요 이제 RelevantIntentManager에는 가능한 각 위젯 구성이 더 높은 관련성을 갖는 날짜 레인지가 있습니다 키 컴포넌트에 해당 함수를 추가하면 적절한 시기에 relevantIntent가 업데이트되겠죠 우선 타임라인 공급자의 timeline 함수로 돌아갈게요 타임라인을 반환하기 직전에 함수를 호출해 보죠
그러면 위젯 타임라인을 업데이트할 때마다 relevantIntent가 최신 상태로 유지됩니다 Backyard Birds 앱도 살펴보죠 Backyard Birds 앱은 뒤뜰마다 디테일 뷰가 있고 음식과 물을 리필할 수 있는 페이지를 제공합니다 relevantIntent 업데이트에 최적의 장소이기도 하죠 음식과 물 공급이 바뀔 수 있으니까요 updateBackyard RelevantIntents 함수를 사용해 BackyardContentTab에 Task를 추가할게요 리필 버튼을 탭하면 작업이 진행되도록요 음식과 물이 업데이트됐으므로 WidgetKit로 호출해서 위젯 타임라인을 새로 고침 해야 합니다
이제 물과 음식을 뒤뜰에서 리필하면 관련 인텐트가 업데이트되고 위젯 타임라인이 새로 고침 되겠죠
watchOS 스마트 스택 위젯을 빌드해 봤습니다 RelevantIntentManager를 날짜 인텐트로 업데이트해서 가장 관련성이 높을 때 위젯을 우선으로 처리했고요 오늘 세션에 함께해 주셔서 감사합니다 watchOS 스마트 스택 위젯을 직접 빌드해 보시길 바라요 위젯과 스마트 스택, App Intent를 더 자세히 알고 싶다면 아래 세션을 참고해 보시고요 모험심을 발휘해서 코딩에 계속 도전해 보세요
-
-
4:15 - TimelineEntry
struct SimpleEntry: TimelineEntry { var date: Date var configuration: ConfigurationAppIntent var backyard: Backyard var bird: Bird? { return backyard.visitorEventForDate(date: date)?.bird } var waterDuration: Duration { return Duration.seconds(abs(self.date.distance(to: self.backyard.waterRefillDate))) } var foodDuration: Duration { return Duration.seconds(abs(self.date.distance(to: self.backyard.foodRefillDate))) } var relevance: TimelineEntryRelevance? { if let visitor = backyard.visitorEventForDate(date: date) { return TimelineEntryRelevance(score: 10, duration: visitor.endDate.timeIntervalSince(date)) } return TimelineEntryRelevance(score: 0) } }
-
7:50 - placeholder function
func placeholder(in context: Context) -> SimpleEntry { return SimpleEntry(date: Date(), configuration: ConfigurationAppIntent(), backyard: Backyard.anyBackyard(modelContext: modelContext)) }
-
8:15 - snapshot function
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry { if let backyard = Backyard.backyardForID(modelContext: modelContext, backyardID: configuration.backyardID) { if let event = backyard.visitorEvents.first { return SimpleEntry(date: event.startDate, configuration: configuration, backyard: backyard) } else { return SimpleEntry(date: Date(), configuration: configuration, backyard: backyard) } } let yard = Backyard.anyBackyard(modelContext: modelContext) return SimpleEntry(date: Date(), configuration: ConfigurationAppIntent(), backyard: yard) }
-
10:26 - Widget Entry View
struct BackyardBirdsWidgetEntryView: View { @Environment(\.widgetFamily) private var family var entry: SimpleEntry var body: some View { switch family { case .accessoryRectangular: RectangularBackyardView(entry: entry) default: Text(entry.date, style: .time) } } }
-
11:23 - Backyard Rectangular View
struct RectangularBackyardView: View { var entry: SimpleEntry var body: some View { HStack { if let bird = entry.bird { ComposedBird(bird: bird) .scaledToFit() .widgetAccentable() .frame(width: 50, height: 50) VStack(alignment: .leading) { Text(bird.speciesName) .font(.headline) .foregroundStyle(bird.colors.wing.color) .widgetAccentable() .minimumScaleFactor(0.75) Text(entry.backyard.name) .minimumScaleFactor(0.75) HStack { Image(systemName: "drop.fill") Text(entry.waterDuration, format: remainingHoursFormatter) Image(systemName: "fork.knife") Text(entry.foodDuration, format: remainingHoursFormatter) } .imageScale(.small) .minimumScaleFactor(0.75) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) } else { Image(.fountainFill) .foregroundStyle(entry.backyard.backgroundColor) .imageScale(.large) .scaledToFit() .widgetAccentable() .frame(width: 50, height: 50) VStack(alignment: .leading) { Text(entry.backyard.name) .font(.headline) .foregroundStyle(entry.backyard.backgroundColor) .widgetAccentable() .minimumScaleFactor(0.75) HStack { Image(systemName: "drop.fill") Text(entry.waterDuration, format: remainingHoursFormatter) Image(systemName: "fork.knife") Text(entry.foodDuration, format: remainingHoursFormatter) } .imageScale(.small) .minimumScaleFactor(0.75) Text("\(entry.backyard.historicalEvents.count) visitors") .minimumScaleFactor(0.75) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) } } .containerBackground(entry.backyard.backgroundColor.gradient, for: .widget) } }
-
16:30 - Timeline Function
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> { var entries: [SimpleEntry] = [] if let backyard = Backyard.backyardForID(modelContext: modelContext, backyardID: configuration.backyardID) { for event in backyard.visitorEvents { let entry = SimpleEntry(date: event.startDate, configuration: configuration, backyard: backyard) entries.append(entry) let afterEntry = SimpleEntry(date: event.endDate, configuration: configuration, backyard: backyard) entries.append(afterEntry) } } return Timeline(entries: entries, policy: .atEnd) }
-
18:35 - Recommendations Function
func recommendations() -> [AppIntentRecommendation<ConfigurationAppIntent>] { var recs = [AppIntentRecommendation<ConfigurationAppIntent>]() for backyard in Backyard.allBackyards(modelContext: modelContext) { let configIntent = ConfigurationAppIntent() configIntent.backyardID = backyard.id.uuidString let gardenRecommendation = AppIntentRecommendation(intent: configIntent, description: backyard.name) recs.append(gardenRecommendation) } return recs }
-
20:47 - Relevant Intents Function
func updateBackyardRelevantIntents() async { let modelContext = ModelContext(DataGeneration.container) var relevantIntents = [RelevantIntent]() for backyard in Backyard.allBackyards(modelContext: modelContext) { let configIntent = ConfigurationAppIntent() configIntent.backyardID = backyard.id.uuidString let relevantFoodDateContext = RelevantContext.date(from: backyard.lowSuppliesDate(for: .food), to: backyard.expectedEmptyDate(for: .food)) let relevantFoodIntent = RelevantIntent(configIntent, widgetKind: "BackyardVisitorsWidget", relevance: relevantFoodDateContext) relevantIntents.append(relevantFoodIntent) let relevantWaterDateContext = RelevantContext.date(from: backyard.lowSuppliesDate(for: .water), to: backyard.expectedEmptyDate(for: .water)) let relevantWaterIntent = RelevantIntent(configIntent, widgetKind: "BackyardVisitorsWidget", relevance: relevantWaterDateContext) relevantIntents.append(relevantWaterIntent) } do { try await RelevantIntentManager.shared.updateRelevantIntents(relevantIntents) } catch { } }
-
23:00 - Update Relevant Intents
Task { await updateBackyardRelevantIntents() WidgetCenter.shared.reloadTimelines(ofKind: "BackyardVisitorsWidget") }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.