iOS

SwiftData

미니미니니

오늘은 SwiftData가 뭔지, 어떻게 사용하면 되는지 알아보겠습니다.

 

SwiftData에 관련된 Apple Developer문서를 보면 다음과 같이 소개를 하고있습니다.

"SwiftData를 사용하면 선언적 코드를 사용하여 데이터를 쉽게 유지하고, Swift 코드를 사용하여 데이터를 쿼리 및 필터링할 수 있으며, SwiftUI와 매끄럽게 통합할 수 있습니다."

 

이 이야기를 풀어서 이해해보면 SwiftData의 특징은 크게 세가지가 있습니다.

  • 선언적 구문: 클래스앞에 @Model만 선언해주면 SwiftData가 알아서 스키마 설정이나 저장소 구성을 처리해줍니다. 그래서 따로 복잡한 설정을 할 필요가 없습니다.
  • Swift 친화적: Swift 코드를 사용해서 작성하기 때문에 따로 언어에 대한 공부가 필요없고, Swift의 매크로와 같은 다양한 기능을 사용할 수 있습니다.
  • SwiftUI와의 통합: SwiftUI 뷰 안에서 @Query와 같은 프로퍼티 래퍼를 사용하여 데이터를 쉽게 가져올 수 있고, 데이터가 변경되면 자동으로 뷰가 업데이트되는 등 앱의 상태관리를 도와줍니다.

개념과 특징에 대해서 알아보았으니 직접 사용해보면서 사용 방법을 익혀보겠습니다.

1. 데이터 모델 정의

import Foundation
import SwiftData

@Model
final class TodoItem {
    var task: String
    var timestamp: Date
    var isCompleted: Bool

    init(task: String, timestamp: Date) {
        self.task = task
        self.timestamp = timestamp
        self.isCompleted = false
    }
}

class로 모델을 정의하고, @Model 을 선언해주면 해당 class가 SwiftData의 데이터 모델이 됩니다.

2. 데이터 저장컨테이너 설정

import SwiftUI
import SwiftData

@main
struct SwiftDataDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: TodoItem.self)
    }
}

.modelContainer(for: )에 만들어놓은 데이터 모델을 넣어서 데이터 저장소를 만들어줍니다. 

이렇게 지정하면 하위 뷰에서 이 저장소에 접근할 수 있습니다.

3. 데이터 사용

import SwiftUI
import SwiftData

struct ContentView: View {
  @Environment(\.modelContext) private var modelContext
  @Query(sort: \TodoItem.timestamp, order: .reverse) private var items: [TodoItem]

  var body: some View {
    NavigationStack {
      List {
        ForEach(items) { item in
          HStack {
            // 할 일 텍스트
            Text(item.task)
            // 완료된 항목에 취소선 추가
              .strikethrough(item.isCompleted, color: .secondary)
              .foregroundStyle(item.isCompleted ? .secondary : .primary)

            Spacer()

            // 완료 여부를 나타내는 체크마크
            if item.isCompleted {
              Image(systemName: "checkmark.circle.fill")
                .foregroundStyle(.green)
            } else {
              Image(systemName: "checkmark.circle")
                .foregroundStyle(.gray)
            }
          }
          .contentShape(Rectangle()) // 행 전체를 탭 영역으로 만듭니다.
          .onTapGesture {
            // 탭해서 완료 상태를 변경합니다.
            toggleCompletion(for: item)
          }
        }
        .onDelete(perform: deleteItems) // 스와이프하여 삭제하는 기능
      }
      .navigationTitle("할 일 목록")
      .toolbar {
        ToolbarItem(placement: .navigationBarTrailing) {
          Button(action: addItem) {
            Label("Add Item", systemImage: "plus")
          }
        }
      }
    }
  }

  private func addItem() {
    let newItem = TodoItem(task: "새로운 할 일 \(Date())", timestamp: Date())
    modelContext.insert(newItem)
  }

  private func deleteItems(offsets: IndexSet) {
    for index in offsets {
      modelContext.delete(items[index])
    }
  }

  private func toggleCompletion(for item: TodoItem) {
    withAnimation {
      item.isCompleted.toggle()
    }
  }
}

UI를 그리는 코드랑 섞여있어서 길어보이지만 실제로 SwiftData를 사용하기위한 코드는 아주 간단합니다.

한줄씩 살펴보면서 어떻게 사용했는지 알아보겠습니다.

@Environment(\.modelContext) private var modelContext

데이터 컨테이너를 가져옵니다.

@Query(sort: \TodoItem.timestamp, order: .reverse) private var items: [TodoItem]

TodoItem에 @Query를 통해 정렬조건을 추가해서 timestamp기준으로 내림차순 정렬을 해서 최신 항목이 가장 위로 오게 만들었습니다.

//todo 추가
modelContext.insert(newItem)
//todo 삭제
modelContext.delete(items[index])
//todo 값 변경
item.isCompleted.toggle()

각가 추가/삭제/변경을 할 때에는 위와같이 하면 됩니다. 

위 코드를 보면 한가지 이상한 점이 있습니다. 값의 추가, 삭제는 modelContext가 관여하지만 변경은 그냥 Query로 가져온 item을 수정합니다. 

값 변경은 item의 내부 속성을 변경하는 것이기 때문에 item을 변경해주는 것이고, 추가와 삭제는 데이터베이스의 목록을 수정하는 것이기 때문에 modelContext를 수정해야하는 것입니다. 

 

오늘은 간단하게 어떻게 모델을 만들고 데이터베이스를 만들어서 값을 입력, 출력, 삭제를 해보았습니다. 정말 소개하는 대로 간단하고 쉽게 작성할 수 있는 것 같습니다. 더 심화활용이나 프로젝트를 진행하다 소개할만한 것이 있다면 또 다시 글을 작성하도록 하겠습니다.