일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- swift 6
- swiftdata
- Swift Tuist
- Combine
- KeyPath
- SeSAC
- Subscribe
- swift 5.9
- Tuist Swift
- ribs
- swift
- ios database
- Tuist
- 네트워크 통신
- Subject
- RxSwift
- swift db
- observable
- Firebase Analytics
- ios swiftdata
- ios
- arc
- xcode
- swift database
- realm
- Firebase
- GCD
- 카카오뱅크 ios
- SwiftUI
- JSON
- Today
- Total
천원의 개발
iOS SwiftData CRUD를 구현해보자 본문
우선 SwiftData는 데이터 모델링 및 관리를 위한 프레임워크로 iOS 17이상 부터 사용이 가능한 데이터베이스입니다.
개발하면서 오류 로그들을 확인해 보면 CoreData를 기반으로 만들어져 발생하는 오류가 CoreData에서 발생하는 오류와 비슷한 경우가 많았습니다.
기본적으로 SwiftUI와 친화적으로 만들어져있어서 SwiftUI와 함께 사용하는게 더 간편하지만, UIKit과 함께 사용하는 자료는 적은 것 같아 UIKit을 기반으로 어떻게 사용하면 좋을지 고민한 내용을 공유하고자 해당 글을 작성합니다.
모델 생성
@Model atributte 키워드를 통해서 모델의 생성이 가능합니다. 저는 영화의 정보를 저장할 Movie 모델을 생성하고 @Attribute(.unique)를 활용하여 모델의 기본키를 지정해 주었습니다.
import SwiftData
@Model
final public class Movie {
@Attribute(.unique) public var id: String?
public var title: String
public var content: String
public var image: [Data]
public var date: Date
public var rate: Int
public var heart: Int
public init(
title: String,
content: String,
image: [Data],
date: Date,
rate: Int
) {
self.id = UUID().uuidString
self.title = title
self.content = content
self.image = image
self.date = date
self.rate = rate
self.heart = 0
}
}
PersistentModel
우선 다양한 Model의 CURD를 제공해주기 위해서 제네릭형태로 Repository를 구현을 하였습니다. 그러기 위해서 모델들의 공통적으로 가지고 있는 프로토콜 제약을 찾아보았고 PersistentModel이 SwiftDate의 인터페이스를 제공함을 확인할 수 있었습니다.
ModelContainer
초기화 구문에서 모델의 CRUD에 필요한 ModelContainer 를 생성해 주었습니다.
public final class SwiftDataRepository<T: PersistentModel>: SwiftDataRepositoryProtocol {
public init() {
let configure = ModelConfiguration("\(T.self)")
do {
container = try ModelContainer(for: T.self, configurations: configure)
} catch {
fatalError(error.localizedDescription)
}
}
let container: ModelContainer
}
ModelContainer를 생성하기 위해서는 모델의 메타타입과 ModelConfiguration이 필요한데 여기서 만났던 이슈는 ModelConfiguration의 초기화 구문을 살펴보면 name을 옵셔널 형태로 가지고 있는데
ModelConfiguration() 형태로 생성하게 되면 하나의 모델에서는 정상적으로 동작하지만 모델의 개수가 2개 이상이 되면 충돌이 발생하니ModelConfiguration 생성 시에는 항상 name을 지정해 주면 좋을 것 같습니다. 저는 모델의 메타타입을 사용하여 name을 지정해 주었습니다.
Insert Data
Repository의 인터페이스는 아래와 같이 두었습니다. 해당 프로젝트에서는 clean architecture 적용하여 인터페이스는 Domain 모듈에 두어 Usecase에서 접근이 가능하도록 하였습니다.
public protocol SwiftDataRepositoryProtocol {
associatedtype T: PersistentModel
func insertData(data: T) async
func fetchData() async throws -> [T]
func deleteData(data: T) async
func deleteAllData() async
}
비동기 처리는 async await 스타일로 진행을 하였고 초기화 시점에서 만든 container를 활용하여 insert와 save를 진행합니다.
public func insertData(data: T) {
Task { @MainActor in
let context = container.mainContext
context.insert(data)
do {
try context.save()
} catch {
print("Error saving context: \(error.localizedDescription)")
}
}
}
Read Data
아래는 모든 데이터를 가져오지만 FetchDescriptor 생성 시 predicate에 가져오려는 데이터의 조건을 추가해 준다면 원하는 데이터를 소팅해서 가져올 수 있습니다.
@MainActor
public func fetchData() async throws -> [T] {
let descriptor = FetchDescriptor<T>(predicate: nil)
let context = container.mainContext
let data = try context.fetch(descriptor)
return data
}
Predicate 예시 코드
let moviePredicate = #Predicate<Movie> {
$0.rate > 4 &&
$0.title.contains("birthday") &&
$0.date > today
}
fetchData 시 매개변수로 받아서 사용해도 좋을 것 같습니다.
Delete Data
public func deleteData(data: T) async {
let context = await container.mainContext
context.delete(data)
do {
try context.save()
} catch {
print("Error saving context: \(error.localizedDescription)")
}
}
특정 데이터를 삭제하는 함수이고 해당 함수와 fetch함수를 활용하여 전체 삭제 함수를 구현하였습니다.
public func deleteAllData() async {
do {
let data = try await fetchData()
for item in data {
await deleteData(data: item)
}
} catch {
print("Error fetching or deleting all data: \(error.localizedDescription)")
}
}
Repository 사용해보기
Usecase에서 repository의 인스턴스를 생성해서 사용하는 sample code 입니다. repository의 타입을
SwiftDataRepositoryProtocol 인터페이스에 두고 repository의 associatedtype 조건을 Movie로 설정하여 Movie 정보를 DB에서 fetch하는 함수를 구현해 주었습니다.
public protocol FetchMovieUsecaseProtocol {
func execute() async -> [Movie]
}
public final class FetchMovieUsecase<Repository: SwiftDataRepositoryProtocol>: FetchMovieUsecaseProtocol where Repository.T == Movie {
private let repository: Repository
public init(repository: Repository) {
self.repository = repository
}
public func execute() async -> [Movie] {
do {
let data = try await repository.fetchData()
return data
} catch {
fatalError(error.localizedDescription)
}
}
}
여기까지 간단히 정리한 SwiftData의 CRUD 였습니다.
참고:
https://developer.apple.com/documentation/swiftdata
https://developer.apple.com/videos/play/wwdc2023/10196
https://developer.apple.com/videos/play/wwdc2023/10187
전체 코드:
public final class SwiftDataRepository<T: PersistentModel>: SwiftDataRepositoryProtocol {
public init() {
let configure = ModelConfiguration("\(T.self)")
do {
print("configure Init")
container = try ModelContainer(for: T.self, configurations: configure)
} catch {
fatalError(error.localizedDescription)
}
}
let container: ModelContainer
public func insertData(data: T) {
Task { @MainActor in
let context = container.mainContext
context.insert(data)
do {
try context.save()
} catch {
print("Error saving context: \(error.localizedDescription)")
}
}
}
@MainActor
public func fetchData() async throws -> [T] {
let descriptor = FetchDescriptor<T>(predicate: nil)
let context = container.mainContext
let data = try context.fetch(descriptor)
return data
}
public func deleteData(data: T) async {
let context = await container.mainContext
context.delete(data)
do {
try context.save()
} catch {
print("Error saving context: \(error.localizedDescription)")
}
}
public func deleteAllData() async {
do {
let data = try await fetchData()
for item in data {
await deleteData(data: item)
}
} catch {
print("Error fetching or deleting all data: \(error.localizedDescription)")
}
}
}
'iOS&Swift🍎 > iOS' 카테고리의 다른 글
iOS Background Tasks(백그라운드에서 API 호출하기) (0) | 2024.02.20 |
---|---|
iOS KeyPath 정리 (1) | 2023.12.26 |
iOS Continuation 정리 (0) | 2023.11.24 |
iOS Tuist 적용하여 여러 프로젝트 관리하기 (0) | 2023.10.24 |
iOS Preview 보면서 codebase로 작업하기 (0) | 2023.07.12 |