View in English

  • Apple Developer
    • 시작하기

    시작하기 탐색

    • 개요
    • 알아보기
    • Apple Developer Program

    알림 받기

    • 최신 뉴스
    • Hello Developer
    • 플랫폼

    플랫폼 탐색

    • Apple 플랫폼
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store

    피처링

    • 디자인
    • 배포
    • 게임
    • 액세서리
    • 웹
    • 홈
    • CarPlay
    • 기술

    기술 탐색

    • 개요
    • Xcode
    • Swift
    • SwiftUI

    피처링

    • 손쉬운 사용
    • 앱 인텐트
    • Apple Intelligence
    • 게임
    • 머신 러닝 및 AI
    • 보안
    • Xcode Cloud
    • 커뮤니티

    커뮤니티 탐색

    • 개요
    • Apple과의 만남 이벤트
    • 커뮤니티 주도 이벤트
    • 개발자 포럼
    • 오픈 소스

    피처링

    • WWDC
    • Swift Student Challenge
    • 개발자 이야기
    • App Store 어워드
    • Apple 디자인 어워드
    • 문서

    문서 탐색

    • 문서 라이브러리
    • 기술 개요
    • 샘플 코드
    • 휴먼 인터페이스 가이드라인
    • 비디오

    릴리즈 노트

    • 피처링 업데이트
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • tvOS
    • Xcode
    • 다운로드

    다운로드 탐색

    • 모든 다운로드
    • 운영 체제
    • 애플리케이션
    • 디자인 리소스

    피처링

    • Xcode
    • TestFlight
    • 서체
    • SF Symbols
    • Icon Composer
    • 지원

    지원 탐색

    • 개요
    • 도움말
    • 개발자 포럼
    • 피드백 지원
    • 문의하기

    피처링

    • 계정 도움말
    • 앱 심사 지침
    • App Store Connect 도움말
    • 새로 추가될 요구 사항
    • 계약 및 지침
    • 시스템 상태
  • 빠른 링크

    • 이벤트
    • 뉴스
    • 포럼
    • 샘플 코드
    • 비디오
 

비디오

메뉴 열기 메뉴 닫기
  • 컬렉션
  • 전체 비디오
  • 소개

더 많은 비디오

  • 소개
  • 요약
  • 자막 전문
  • 코드
  • 코딩 실습: SwiftData로 영속성 추가하기

    실제로 SwiftData에서 기존 앱에 영속성을 추가하는 과정을 경험해 보세요. 데이터 모델을 정의하고 영구 데이터를 SwiftUI와 원활하게 통합하는 방법을 안내합니다. 또한 이 표현적이고 선언적인 API를 사용하여 앱 상태를 관리하는 기본 기술을 알아봅니다.

    챕터

    • 0:00 - Introduction
    • 1:05 - Identify relevant state
    • 3:17 - Define your schemas
    • 9:41 - Define model relationships
    • 13:33 - Update the view layer
    • 21:47 - Next steps

    리소스

    • Wishlist: Planning travel in a SwiftUI app
    • SwiftData
      • HD 비디오
      • SD 비디오

    관련 비디오

    WWDC26

    • SwiftData의 새로운 기능

    WWDC25

    • SwiftData: 상속 및 스키마 마이그레이션 자세히 알아보기
  • 비디오 검색…

    안녕하세요, 저는 Matthew Turk입니다 SwiftData 팀의 엔지니어예요 오늘은 기존 SwiftUI 앱의 동적 데이터를 가져와서 Apple의 모든 플랫폼에서 작동하는 최신 퍼시스턴스 레이어에 SwiftData로 연결하는 방법을 보여드릴게요 Wishlist라는 목록 기반 앱의 소스 코드로 시작하겠습니다 Wishlist는 아이디어를 기록하여 여행 계획을 정리하고 여행을 계절별 컬렉션으로 그룹화하는 앱이에요 developer.apple.com에서 샘플 앱을 다운로드해서 따라 해보세요 이 영상에서는 프로젝트 파일을 단계별로 살펴보며 데이터 유형과 SwiftData 모델 및 스키마에 사용할 변수를 파악하겠습니다 그리고 이 모델들이 데이터베이스에서 어떻게 서로 연관되는지 SwiftUI 뷰를 어떻게 업데이트하는지 새 모델을 성능을 고려하여 어떻게 표시하는지 더 복잡한 사용 사례를 위한 상호운용성과 확장성도 살펴봅니다

    실제로 그 데이터 흐름이 어떻게 작동하는지 미리 보겠습니다

    Wishlist 탭에는 최근 여행이 상단에 있고 여러 테마 목록의 더 많은 여행이 아래로 스크롤됩니다

    Goals 탭에서는 다양한 목표의 배지를 볼 수 있어요 그 여행들에서 완료하고 싶은 목표들이에요 Search 탭에서는 여행과 활동을 필터링할 수 있어요 해안 트레일을 검색해 볼게요 상위 결과네요

    이 여행에는 남은 활동이 다섯 가지 있어요 전에 Point Reyes를 방문해 그곳에서 엘크 11마리를 봤으니 이건 완료로 체크할게요

    Wishlist 탭으로 돌아가서 플러스 버튼을 탭해 새 여행을 추가할게요 언젠가 오로라를 다시 보고 싶어요 여행 이름을 "Northern Lights"로 짓고 함께 할 사진을 선택할게요

    완료를 누르면 새 여행이 생겼어요

    방금 보신 데이터 흐름이 가능한 건 Wishlist의 뷰가 SwiftUI 환경을 통해 DataSource 변수를 가져오기 때문이에요 DataSource 클래스는 미리 설치된 모든 여행 데이터를 관리하고 제공합니다 모든 여행, 목표, 검색 결과는 요청 시 메모리에서 필터링되고 정렬돼요 작은 예제에서는 잘 작동합니다 하지만 실제로는 최적화가 필요할 거예요 앱의 프런트 부분을 간결하게 유지하려면요 또한 Wishlist는 이 데이터를 처리하고 저장하는 데 RAM에 의존하는데 이 데이터를 처리하고 저장하는 데 RAM을 사용합니다 이는 새 여행이나 활동을 나중에 저장할 수 있는 안정적인 장소가 아니에요

    앱을 닫고 다시 실행하면

    새 여행의 모든 정보가 사라지고 미리 설치된 콘텐츠로 초기화됩니다

    하지만 꼭 그럴 필요는 없어요 SwiftData가 해결하는 문제가 바로 이거예요 지금까지 관련 상태를 파악했어요 여행 컬렉션, 목표 상태, 검색 결과가 바로 그것들이에요 SwiftData는 그 상태를 모델 컨텍스트를 통해 영구 저장소에 연결할 수 있어요 그게 바로 이 영상이 끝날 때 우리가 도달할 지점입니다 다음 단계는 이 데이터 구조가 코드 어디에 있는지 찾고 SwiftData 스키마로 모델로 리팩터링하는 거예요 그러면 메모리 내 DataSource를 대체할 준비가 됩니다 영구 ModelContext로 말이에요 이를 통해 효율적인 데이터베이스 쿼리를 작성해 뷰를 구동할 수 있어요 Activity 유형 같은 모델 하나를 좀 더 자세히 살펴봅시다

    Activities를 유지하려면 먼저 SwiftData를 가져오고 이 Observable 매크로를 Model 매크로로 교체합니다 SwiftData가 자동으로 Observable 준수를 생성해줘요 좋네요! 참고로, name과 isComplete에 있는 이 didSet 옵저버는 두 프로퍼티 중 하나가 변경될 때마다 활동의 dateEdited 값을 설정합니다

    패러글라이딩 활동이 있다고 가정해 봅시다 아직 완료하지 않았고 오전 9시 41분에 4월 1일에 마지막으로 편집했어요 이벤트 타임라인입니다 그런 다음 활동을 패러글라이딩에서 수영으로 변경하기로 했다고 가정해요 name의 프로퍼티 옵저버가 실행되어 dateEdited를 업데이트합니다 나중에 해변으로 여행을 가서 활동을 완료로 체크해요 isComplete의 프로퍼티 옵저버가 실행되어 dateEdited를 업데이트합니다 자동으로 업데이트되는 dateEdited 프로퍼티는 목록에서 정렬하거나 필터링하기 좋은 방법이에요 하지만 프로퍼티 옵저버와 계산된 프로퍼티는 항상 호환되지 않아요 그래서 dateEdited를 최신 상태로 유지하는 다른 기법을 사용할게요 그 전에 현재 프로젝트가 다시 빌드되도록 할게요 이 다이어그램으로 돌아올게요 지금은 이 didSet 블록들을 제거하고

    dateEdited는 그대로 두세요 자, 다음은 Trip 클래스입니다 동일하게 SwiftData를 가져오고

    매크로를 교체합니다

    이제 이 빌드 오류들을 살펴보며 무엇이 누락됐는지 파악해 봅시다 여기서는 creationDate가 변경 가능해야 한다고 합니다 let 대신 var를 사용하도록 선언을 수정할게요

    이제 SwiftData가 런타임에 이 프로퍼티를 데이터베이스에서 불러온 값으로 채울 수 있어요 다음 오류는 TripCollection 프로퍼티와 모든 모델 프로퍼티가 Codable이어야 한다고 합니다 이 요구 사항이 있는 건 SwiftData가 프로퍼티를 데이터베이스 컬럼으로 직렬화할 수 있어야 하기 때문이에요 여행 컬렉션은 여행의 계절 테마를 저장합니다 스키마에 포함되어 유지되어야 해요 Command 클릭으로 선언으로 이동할게요 그리고 Codable 준수를 명시적으로 추가할게요 빌드 오류가 몇 가지 남아 있어요 스키마를 계속 구성하면서 수정해 나갈게요 다음으로 Wishlist에서 목표 추적이 어떻게 작동하는지 살펴봅시다 이 Goal 유형의 경우 변경이 Observable을 Model로 변환하는 것처럼 단순하지 않아요 Goal은 열거형으로 선언되어 있어요 각 목표에는 이름, 종류 같은 프로퍼티가 있어요 활동 완료 또는 여행 완료 여부를 추적하는 것처럼요 활동 또는 여행 완료 여부를 추적하고 목표를 달성하기 위한 목표 여행 수나 활동 수도 있어요 정확히 18개의 목표가 있고 모두 미리 정의되어 있어요 열거형은 닫힌 값 집합을 정의하기 때문이에요 원래 인터페이스 데모에는 괜찮지만 영구 모델에 필요한 건 클래스입니다 클래스는 프로퍼티를 저장하고 원하는 만큼 인스턴스화할 수 있어요 올바른 인터페이스를 갖추려면 Goal의 설계를 재고해야 합니다 목표가 처리되고 표시되는 로직과 조화를 이루도록요 높은 수준에서 각 목표는 배지에 해당합니다 특정 조건이 충족되는지 여부에 따라 Wishlist에 표시돼요 이 목표들의 상태를 충실하게 유지하려면 특정 목표의 조건이 충족됐는지 최소한의 표현을 캡처하고 저장할 방법이 필요합니다 SwiftData에서 클래스는 그 최소한의 표현을 나타내기 위해 선택하는 기반입니다 Goal을 클래스로 변환하면서 동일한 세 가지 프로퍼티부터 시작할게요 이름, 종류, 목표 수입니다

    원래 Wishlist 앱에서는 완료된 항목 수가 별도로 저장됐어요 열거형에는 저장된 프로퍼티가 없기 때문이에요 이제 영구 클래스가 생겼으니 진행 값을 목표 자체에도 저장해 봅시다 completedCount라고 불러요

    그리고 isComplete라는 Boolean 프로퍼티도요 completedCount가 target보다 크거나 같을 때 true 값을 저장할 거예요 target 이상일 때 true를 저장합니다 저장된 프로퍼티이기 때문에 뷰 레이어에서 쿼리를 시작할 때 완료된 목표와 다가오는 목표를 분리하는 데 유용할 거예요

    다음으로 Kind 프로퍼티를 다뤄야 합니다 Wishlist는 목표가 여행 완료와 활동 완료 중 어느 것과 관련됐는지에 따라 여행 완료인지 활동 완료인지에 따라 다른 세부 정보를 표시하는 데 사용해요 SwiftData의 모델 상속 지원을 사용하여 이 세부적인 차이점들을 새 하위 클래스로 분리할 수 있어요 상속은 잘 정의된 클래스 계층 구조가 있을 때 효과를 발휘하는 소프트웨어 설계 패턴이에요 각 하위 클래스가 수퍼클래스와 동일한 개념을 나타내지만 수퍼클래스와 같은 아이디어를 나타내되 공통 프로퍼티 집합의 더 구체적인 표현을 갖는 경우예요 예를 들어, 나선 은하와 렌즈형 은하는 은하의 하위 클래스로 어떤 종류의 은하에서도 찾을 수 있는 별이나 먼지 같은 여러 특성을 상속하면서 별과 먼지처럼요 가시적 형태에서 범주적인 차이가 있습니다 마찬가지로, 여행 목표와 활동 목표가 있을 수 있어요 둘 다 목표 수퍼클래스에서 공통 프로퍼티를 상속받습니다 Goal에서 Kind 프로퍼티를 제거하여 상속으로 여러 종류의 목표를 모델링할게요 Goal에서 Kind 프로퍼티를 제거하면서요 그리고 TripGoals와 ActivityGoals에 대한 하위 클래스를 도입합니다

    모델 상속을 사용하는 것이 언제 적합한지 자세히 알아보려면 WWDC 2025의 "SwiftData: 상속과 스키마 마이그레이션 심층 분석" 세션을 확인하세요 이제 이 모든 모델 간의 관계를 정의하기 시작하는 데 필요한 모든 것이 갖춰졌어요 Wishlist에서 각 여행은 일련의 활동과 연관됩니다 이것이 일대다 관계예요 각 영구 Trip 모델은 잠재적으로 많은 영구 Activity 모델을 갖습니다 Wishlist의 여러 부분이 일대다 관계를 근사화하여 딕셔너리와 배열을 순회하는 함수를 사용해왔어요 예를 들어, 활동 ID를 기반으로 Trips에서 Activities로 매핑하는 함수가 있어요 곧 이 딕셔너리들을 유형 간의 적절한 일대다 관계로 변환할게요 각 Trip이 0개 이상의 Activities를 가질 수 있도록요 배열을 선언하는 것이 한 종류의 모델이 SwiftData에게 다른 종류의 모델을 필요에 따라 모델 컨텍스트에서 참조할 수 있음을 알려주는 방식이에요 Trip에서 activities의 배열을 선언할게요 여기서 relationship 매크로도 추가하여 Trip의 activities 배열을 관계로 명시적으로 표시합니다 데이터베이스에서 여행을 삭제하면 일정에 있던 활동 모델도 함께 삭제됩니다 마지막으로, 이 photoURL 프로퍼티를 조정해야 합니다 현재는 파일 경로에 불과해서 파일이 이름이 바뀌거나 다른 디렉토리로 이동하면 의미를 잃게 돼요 그리고 전체 해상도 이미지는 뷰가 TripDetailView처럼 전체로 표시해야 할 때만 불러와야 해요 여행 캐러셀을 스크롤할 때는 썸네일만 표시할게요 그래서 thumbnailData라는 새 프로퍼티를 추가합니다 선택한 사진의 낮은 해상도 버전을 캐시하고 원시 바이트를 데이터베이스에 인라인으로 저장해요 그리고 별도로 URL 대신 전체 해상도 이미지를 영구 외부 파일 참조를 사용하여 저장할 거예요 SwiftData에서는 이미지만을 위한 새 모델을 만들어서 이를 수행합니다 이미 TripImage 유형으로 추가해뒀어요 여기서 구현 세부 정보는 다루지 않겠지만 나중에 샘플 코드에서 더 읽어보시길 권합니다 photoURL이 더 이상 URL이 아니니 마우스 오른쪽 버튼을 클릭하여 photo로 이름을 바꾸고 선언을 선택하고 이 리팩터 메뉴에서 Rename을 선택합니다 멀티 커서 편집을 사용하고 있어서 여러 파일이 한 번에 업데이트됩니다 그리고 이 주석을 클릭하여 새 변수명으로 업데이트할게요 Return을 누르세요 그런 다음 초기화 메서드의 레이블을 리팩터링하고

    activities를 직접 설정하도록 본문을 조정합니다

    이 관계를 설정함으로써 이전의 기능을 복제했어요 활동 중심 뷰가 이름, 시즌, 상위 여행의 다른 세부 정보를 참조하고 표시하는 기능이요 이제 SwiftData 기능들을 정말로 통합하기 시작했으니 프로젝트에 불필요한 파일들이 있어요 TripEditModel은 더 이상 필요 없어요 SwiftUI 뷰가 SwiftData 모델에 직접 바인딩하여 실시간으로 편집 사항을 전달할 수 있기 때문이에요 DataSource의 모든 책임은 ModelContext와 쿼리, 관계에 의해 자동으로 처리됩니다 삭제하세요

    마지막 부분을 되돌아볼 가치가 있어요 프로젝트에서 수백 줄의 코드를 방금 제거했습니다 상태 관리, 저장 로직, 필터링, 정렬, 관계 순회, 검색의 상당 부분이 자동으로 작동합니다 WindowGroup에 마지막 손질을 하면 모델 레이어가 완성됩니다 여기 이 modelContainer 씬 수식어는 SwiftUI에게 query 매크로와 함께 새 스키마를 사용하도록 지시합니다 다음으로 뷰 레이어를 업데이트하겠습니다 스키마와 모델 컨테이너가 준비됐으니 자동 저장이 기본적으로 활성화됩니다 실제로 확인하기 전에 이 영구 모델들을 통합할 거예요 모델을 표시하는 각 하위 뷰에 대한 효율적이고 목표 지향적인 쿼리부터요 그런 다음 SwiftUI 뷰 수식어를 사용하여 런타임에 발생할 수 있는 오류를 캡처하고 표면화할게요 디스크 용량 부족이나 지원되지 않는 예측어처럼요 마지막으로 누락된 프로퍼티 옵저버를 다시 추가하여 UI 이벤트가 올바른 데이터를 모두 전달하고 예상하는 부수 효과가 전달되도록 합니다 SwiftData 앱에 필터링을 추가할 때 염두에 둘 두 가지 핵심 사항이 있어요 쿼리 내부에서 FetchDescriptor는 어떤 모델을 불러와서 모델 컨텍스트나 쿼리를 통해 표시할지 계획하는 방식이에요 둘째로, 모델은 앱의 주소 공간 외부에 존재하는 저장소 매체에 저장됩니다 로컬 파일 시스템의 데이터베이스나 원격 서버처럼요 이런 종류의 저장소는 퍼시스턴스 레이어의 기반이지만 메모리에서 읽는 것보다 몇 배 더 느릴 수 있어요 앱에서 퍼시스턴스 레이어를 설계할 때 어떤 데이터가 언제 어디에 있어야 하는지 생각하는 게 중요합니다 최적의 경험을 위해서요 설명해드릴게요 이전에는 여행, 활동, 목표에 관한 데이터가 allGoals 같은 전역 변수에 있었어요 앱의 컴파일된 바이너리의 일부로 불러와졌죠 이런 방식으로 데이터에 접근하는 건 빠르지만 allGoals에 많은 요소를 하드코딩하면 예를 들어 allGoals에요 앱의 메모리 사용량이 눈에 띄게 높아져요 앱이 실행되는 내내요 새 목표를 추가하면 그것에 관한 모든 정보가 사라집니다 앱을 닫을 때요 보셨듯이 다시 실행하면 사라져요 SwiftData를 사용하면 목표를 삽입하거나 기존 목표를 업데이트하고 저장할 수 있어요 모델 컨텍스트로요 저장되면 앱으로 다시 가져올 몇 가지 방법이 있어요

    하나의 방법이 있어요 이 코드는 모든 Goals를 가져온 다음 관련 없는 것들을 버립니다 지속적인 메모리는 적게 사용하지만 더 많은 I/O가 발생해요 이 방식은 도서관의 모든 선반에서 모든 책을 가져오도록 사서에게 요청하는 것과 비슷해요 좋아하는 작가의 책을 직접 고르기 위해서요 선반을 돌아다니는 사서에게 그 작가의 책만 요청할 수도 있었어요 선반을 다니는 중에요 예측어로 가져올 때는 사서에게 처음부터 원하는 것을 요청하는 것과 같아요 원하는 목표만 가져올 수 있어요

    GoalsView에서는 예측어와 함께 query 매크로를 사용할 거예요 이는 모델 컨텍스트에서 fetch를 호출하는 것과 동일해요 SwiftUI 뷰가 쿼리 결과가 변경될 때 자동으로 업데이트되는 이점이 있어요 지금 해봅시다 SwiftData를 가져오고

    이 dataSource 환경 프로퍼티를 교체합니다 달성된 목표를 달성된 시간순으로 정렬하여 가져오는 쿼리로요 그리고 이 두 번째 쿼리는 남은 관련 목표들을 가져옵니다 RecentTripsPageView에서도 같은 방식이에요

    SwiftData를 가져오고

    dataSource를 쿼리로 교체해요

    역순으로 여행을 요청하고 가져오기 한도를 5로 설정합니다 그러면 가장 최근 5개의 여행이 이 ForEach에 바로 들어갑니다 TripCollectionView에서는 모든 여행을 가져와서 계절별로 분류하려고 해요 각 계절은 여행 컬렉션이에요 각 tripCollection에 대해 이 뷰의 인스턴스가 하나씩 생깁니다 개별 TripCollectionView는 초기화될 때까지 어떤 계절을 표시할지 알 수 없어요 초기화될 때까지요 그래서 쿼리를 선언하고

    초기화 메서드에서 동적으로 구성할 거예요

    원하는 컬렉션과 일치하는 모든 여행에 대한 명시적인 쿼리입니다 쿼리가 예측어를 받고 예측어 내부에서 tripCollection 매개변수가 초기화 메서드에서 직접 캡처됩니다 데이터베이스에 가기 전에요 쿼리 결과는 이 ForEach에 들어갑니다

    다음으로 앱의 세 번째 탭인 SearchResultsListView가 있어요 오른쪽 미리보기에 표시된 수퍼 뷰가 검색 필드와 텍스트를 소유하고 이 뷰의 초기화 메서드에 값을 전달해요 다시 한번 dataSource를 쿼리 선언으로 교체하고 초기화 메서드의 매개변수들이 이 쿼리들을 위한 예측어 구성을 안내합니다 검색 텍스트가 비어 있으면 가장 최근 3개의 여행을 가져오도록 대체합니다 그렇지 않으면 이름이 검색 텍스트와 일치하는 모든 여행을 사전 순으로 정렬하여 가져옵니다 그런 다음 활동 검색도 동일하게 처리할게요 여행에 속한 활동의 이름과 텍스트가 일치하는지 확인하고 다시 프로퍼티 래퍼를 설정합니다

    마지막으로, List에서 쿼리된 값을 사용합니다

    이 목록에는 overlay 뷰 수식어도 있는데 검색 결과가 없으면 상단에 ContentUnavailableView를 표시합니다 원래 dataSource 조건을 trips.isEmpty와 activities.isEmpty를 직접 확인하는 것으로 교체하겠습니다

    앱을 다시 실행할 수 있을 만큼만 됐어요 Wishlist에서 여행을 추가하고 다시 실행해 볼게요

    이번에는 제 여행이 그대로 있어요

    "Northern Lights"네요 SwiftData로의 전환이 거의 완료됐어요 마무리해야 할 몇 가지 사항이 남아 있습니다 ActivityItemView에 있는 updateGoalAchievements 메서드를 살펴봅시다 사람들이 활동을 완료할 때 진행 값을 직접 업데이트합니다 오류가 발생할 수 있어요 상태 변수에 해당 오류를 캡처할게요 오류를 텔레메트리 시스템에도 전달할게요 향후 앱을 개선할 수 있도록요 적절한 경우 오류에서 복구하는 방법을 알려주는 알림을 표시할게요 사람들에게요 오류 케이스 하나가 처리됐어요 UI에서 상태가 오래될 수 있는 몇 가지 경우도 있어요 앞서 dateEdited를 설정하는 didSet 옵저버를 제거했어요 그리고 이제 그 동작을 다시 추가하겠습니다 버그입니다 다른 여행의 활동들을 dateEdited로 정렬하고 싶다고 해봅시다 정렬 드롭다운에서 "편집 날짜"를 선택할게요 그런 다음 "나무 아래서 명상"이라는 활동을 완료로 체크하면 목록 상단으로 이동해야 해요 프로퍼티 중 하나를 방금 업데이트했으니까요 하지만 그냥 거기 있어요 퍼시스턴스 레이어 관점에서는 오류가 발생하지 않지만 그래도 뭔가 올바르지 않아요 이제 퍼시스턴스를 구현했으니 dateEdited 프로퍼티에 대한 실시간 업데이트를 다시 활성화하겠습니다 Continuous Observation 기능을 사용하여 2027 릴리스에서 Observation 프레임워크에 추가된 기능이에요

    ActivityItemView의 초기화 메서드에서 새로운 withContinuousObservation 함수를 사용하여 옵저버를 설정합니다 이 뷰는 사람들이 활동을 편집할 수 있는 곳이라서 관찰하기에 좋은 장소입니다 누군가가 Activity의 isComplete나 이름을 변경할 때마다 Observation 프레임워크가 이 코드를 실행하여 dateEdited를 현재 시간으로 설정합니다 쿼리를 트리거하여 활동 목록을 자동으로 업데이트하게 됩니다 Trip에 부수 효과를 추가하기에도 자연스러운 장소입니다 활동 상태가 전환되거나 활동이 추가되거나 제거될 때마다 전체 여행의 isComplete 프로퍼티를 업데이트합니다

    이제 Apple의 선언적 퍼시스턴스 프레임워크를 여러분의 앱에 통합하는 데 필요한 것을 알게 됐어요 나만의 앱에요 앱 상태의 적절한 표현을 고려하는 것부터 시작하고 스키마를 구성하는 Model 유형을 선언하세요 그런 다음 예측어를 사용하는 목표 지향 쿼리를 작성하여 메모리 사용량과 디스크 저장소 간의 균형을 맞추세요 그리고 SwiftData와의 최적 상호운용성을 위해 SwiftUI 뷰를 계속 조정하는 방법에 대해 최신 정보를 유지하세요 이제 오늘의 마지막 활동을 완료로 체크하고

    배지를 획득하겠습니다

    들어주셔서 감사합니다, 즐거운 여행 되세요

    • 3:39 - Convert Activity to a persistent model with @Model

      import Foundation
      import SwiftData
      
      // SwiftData automatically generates Observable conformance
      @Model
      class Activity {
          var name: String
          var isComplete: Bool = false
          var dateCreated = Date.now
          var dateEdited = Date.now
      }
    • 6:06 - Add Codable conformance to TripCollection

      enum TripCollection: String, CaseIterable, RawRepresentable, Codable {
          case springEscapes
          case summerVibes
          case fallGetaways
          case winterRetreats
      }
    • 10:32 - Set up model relationships between Trip, TripImage, and Activity

      import Foundation
      import SwiftData
      
      @Model
      class Trip {
          var name: String
          var collection: TripCollection
        
          var photo: TripImage
          var thumbnailData: Data?
        
          @Relationship(deleteRule: .cascade, inverse: \Activity.trip)
          var activities: [Activity] = []
        
          private(set) var creationDate = Date.now
          var subtitle: String?
          var isComplete: Bool = false
      }
    • 13:21 - Enable interoperability between your schema and SwiftUI views

      import SwiftUI
      import SwiftData
      
      @main
      struct WishlistApp: App {
          let container: ModelContainer = {
              do {
                  let modelContainer = try ModelContainer(for: Trip.self, Activity.self, TripImage.self, Goal.self, TripGoal.self, ActivityGoal.self)
                  try SampleData.seedIfNeeded(in: modelContainer.mainContext)
                  return modelContainer
              } catch {
                  fatalError("Could not create model container: \(error)")
              }
          }()
      
          var body: some Scene {
              WindowGroup {
                  ContentView()
                      .preferredColorScheme(.dark)
              }
              .modelContainer(container)
          }
      }
    • 16:27 - Fetch achieved and upcoming goals

      @Query(filter: #Predicate<Goal> { $0.isAchieved }, sort: \Goal.dateAchieved, order: .reverse)
      private var achievedGoals: [Goal]
      
      @Query(filter: #Predicate<Goal> { !$0.isAchieved }, sort: \Goal.sortOrder)
      private var upcomingGoals: [Goal]
    • 16:49 - Fetch recent trips

      import SwiftUI
      import SwiftData
      
      struct RecentTripsPageView: View {
          // Fetch most recent trips in reverse chronological order
          @Query(FetchDescriptor<Trip>(sortBy: [SortDescriptor(\Trip.creationDate, order: .reverse)], fetchLimit: 5))
          private var trips: [Trip]
      
          @Namespace private var namespace
      
          var body: some View {
              TabView {
                  ForEach(trips) { trip in
                      NavigationLink {
                          TripDetailView(trip: trip)
                              .navigationTransition(
                                  .zoom(sourceID: trip.id, in: namespace))
                      } label: {
                          TripImageView(trip: trip)
                              .overlay(alignment: .bottomLeading) {
                                  VStack(alignment: .leading) {
                                      Text("RECENTLY ADDED")
                                          .font(.subheadline)
                                          .fontWeight(.bold)
                                          .foregroundStyle(.limeGreen)
      
                                      Text(trip.name)
                                          .font(.title)
                                          .fontWidth(.expanded)
                                          .fontWeight(.medium)
                                          .foregroundStyle(.primary)
                                  }
                                  .padding(.horizontal)
                                  .padding(.bottom, 54)
                              }
                              .matchedTransitionSource(id: trip.id, in: namespace)
                      }
                      .buttonStyle(.plain)
                  }
              }
              .tabViewStyle(.page)
              .containerRelativeFrame([.horizontal, .vertical]) { length, axis in
                  if axis == .vertical {
                      return length / 1.3
                  } else {
                      return length
                  }
              }
          }
      }
    • 17:26 - Dynamically construct a query in the initializer of TripCollectionView

      init(tripCollection: TripCollection, cardSize: TripCard.Size, namespace: Namespace.ID) {
          _trips = Query(filter: #Predicate<Trip> { $0.collection == tripCollection }, sort: \Trip.name)
          self.tripCollection = tripCollection
          self.cardSize = cardSize
          self.namespace = namespace
      }
    • 18:13 - Search for trips and activities by name

      import SwiftUI
      import SwiftData
      
      private struct SearchResultsListView: View {
          @Query(sort: \Trip.name) private var trips: [Trip]
          @Query(sort: \Activity.name) private var activities: [Activity]
      
          var searchText: String
          var namespace: Namespace.ID
      
          init(searchText: String, namespace: Namespace.ID) {
              self.searchText = searchText
              self.namespace = namespace
      
              if searchText.isEmpty {
                  _trips = Query(FetchDescriptor(sortBy: [SortDescriptor(\Trip.creationDate, order: .reverse)], fetchLimit: 3))
                  _activities = Query(filter: #Predicate<Activity> { _ in false })
              } else {
                  // All trips whose name matches searchText, sorted lexicographically
                  let tripSearchPredicate = #Predicate<Trip> { $0.name.localizedStandardContains(searchText) }
                  _trips = Query(filter: tripSearchPredicate, sort: \Trip.name)
                  // All matching activities that belong to a trip
                  let activitySearchPredicate = #Predicate<Activity> { $0.trip != nil && $0.name.localizedStandardContains(searchText) }
                  _activities = Query(filter: activitySearchPredicate, sort: \Activity.name)
              }
          }
      
          var body: some View {
              List {
                  if !trips.isEmpty {
                      TripSearchSectionView(trips: trips, namespace: namespace, title: searchText.isEmpty ? "Recent Trips" : "Trips")
                  }
      
                  if !activities.isEmpty {
                      ActivitySearchSectionView(activities: activities)
                  }
              }
              .overlay {
                  if trips.isEmpty && activities.isEmpty {
                      ContentUnavailableView(
                          "No results for “\(searchText)”",
                          systemImage: "magnifyingglass",
                          description: Text("Check spelling or try a new search.")
                      )
                  }
              }
              .listStyle(.plain)
          }
      }
    • 19:42 - Capture and report errors from ActivityItemView

      var body: some View {
          HStack(alignment: .firstTextBaseline, spacing: 17) {
              Group {
                  if isEditing {
                      rowContentWhenEditing
                  } else {
                      rowContentWhenNotEditing
                  }
              }
              .transition(.opacity.animation(.snappy))
              .animation(.snappy, value: isEditing)
          }
          .onDisappear {
              do {
                  try updateGoalAchievements()
              } catch {
                  updateError = error
                  reportError(error)
              }
          }
          .alert(error: $updateError) {
              // Customize the presentation of the error
          }
      }
    • 21:04 - Update dateEdited and propagate side effects on property changes

      init(activity: Activity, isLast: Bool, isEditing: Bool) {
          activity.token = withContinuousObservation(options: .didSet) { event in
              _ = activity.name
              _ = activity.isComplete
      
              if event.matches(\Activity.name) {
                  activity.dateEdited = .now
              }
      
              if event.matches(\Activity.isComplete) {
                  activity.dateEdited = .now
                  activity.trip?.isComplete = activity.trip?.activities.isEmpty == false
                  && activity.trip?.activities.allSatisfy { $0.isComplete } == true
              }
          }
          self.activity = activity
          self.isLast = isLast
          self.isEditing = isEditing
      }
    • 0:00 - Introduction
    • An introduction to the Wishlist sample app and the three steps for adopting SwiftData: identifying relevant state, defining schemas, and defining model relationships.

    • 1:05 - Identify relevant state
    • Identify the data types and variables in Wishlist — trip collections, goal statuses, and the DataSource — that will become SwiftData models connected through a ModelContext.

    • 3:17 - Define your schemas
    • Convert Activity, Trip, and Goal into @Model types. Covers handling property observers with the @Model macro, refactoring the Goal enumeration into a class hierarchy using inheritance with TripGoal and ActivityGoal subclasses, and inlining thumbnail data.

    • 9:41 - Define model relationships
    • Declare to-many relationships between Trip and Activity using the @Relationship macro, remove the now-redundant DataSource and TripEditModel helpers, and attach the modelContainer scene modifier to complete the model layer.

    • 13:33 - Update the view layer
    • Replace environment DataSource properties with @Query macros and targeted FetchDescriptor predicates in each subview. Covers autosave, surfacing runtime errors with SwiftUI view modifiers, and re-enabling dateEdited property observers using the new withContinuousObservation API.

    • 21:47 - Next steps
    • Key takeaways: design a schema that fits your data model, balance memory and disk usage with targeted queries, and plan for interoperability and extensibility as your app evolves.

Developer Footer

  • 비디오
  • WWDC26
  • 코딩 실습: SwiftData로 영속성 추가하기
  • 메뉴 열기 메뉴 닫기
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    메뉴 열기 메뉴 닫기
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    메뉴 열기 메뉴 닫기
    • 손쉬운 사용
    • 액세서리
    • Apple Intelligence
    • 앱 확장 프로그램
    • App Store
    • 오디오 및 비디오(영문)
    • 증강 현실
    • 디자인
    • 배포
    • 교육
    • 서체(영문)
    • 게임
    • 건강 및 피트니스
    • 앱 내 구입
    • 현지화
    • 지도 및 위치
    • 머신 러닝 및 AI
    • 오픈 소스(영문)
    • 보안
    • Safari 및 웹(영문)
    메뉴 열기 메뉴 닫기
    • 문서(영문)
    • 튜토리얼
    • 다운로드
    • 포럼(영문)
    • 비디오
    메뉴 열기 메뉴 닫기
    • 지원 문서
    • 문의하기
    • 버그 보고
    • 시스템 상태(영문)
    메뉴 열기 메뉴 닫기
    • Apple Developer
    • App Store Connect
    • 인증서, 식별자 및 프로파일(영문)
    • 피드백 지원
    메뉴 열기 메뉴 닫기
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(영문)
    • Mini Apps Partner Program
    • News Partner Program(영문)
    • Video Partner Program(영문)
    • Security Bounty Program(영문)
    • Security Research Device Program(영문)
    메뉴 열기 메뉴 닫기
    • Apple과의 만남
    • Apple Developer Center
    • App Store 어워드(영문)
    • Apple 디자인 어워드
    • Apple Developer Academy(영문)
    • WWDC
    최신 뉴스 읽기.
    Apple Developer 앱 받기.
    Copyright © 2026 Apple Inc. 모든 권리 보유.
    약관 개인정보 처리방침 계약 및 지침