iOS 17で登場したSwiftDataは、これまでのCore Dataに代わる新しいデータ永続化フレームワークです。その中核を担うのがFetchDescriptorです。
この記事では、FetchDescriptorの基本から実践的な使い方まで、Swift初心者にもわかりやすく解説します。
FetchDescriptorとは?
FetchDescriptorは、SwiftDataでデータベースからデータを取得するための設定を定義する構造体です。
データの取得条件、ソート順、取得件数などを指定して、必要なデータだけを効率的に取得できます。
Core DataのNSFetchRequestとの違い
これまでCore Dataを使っていた方は、NSFetchRequestとの違いが気になるかもしれません。
項目 | NSFetchRequest | FetchDescriptor |
---|---|---|
型安全性 | 実行時エラーの可能性 | コンパイル時に型チェック |
構文 | 文字列ベース | Swift文法ベース |
簡潔さ | やや冗長 | シンプルで読みやすい |
対応バージョン | iOS 3.0以降 | iOS 17以降 |
FetchDescriptorは、よりモダンで安全なSwiftの書き方に対応しています。
基本的な使い方
モデルの定義
まず、SwiftDataで使用するモデルを定義します。
import SwiftData
@Model
class Person {
var name: String
var age: Int
var city: String
init(name: String, age: Int, city: String) {
self.name = name
self.age = age
self.city = city
}
}
シンプルなデータ取得
最もシンプルなFetchDescriptorは、すべてのデータを取得します。
import SwiftData
// すべてのPersonデータを取得
let descriptor = FetchDescriptor<Person>()
ModelContextでの使用
実際にデータを取得するには、ModelContextを使用します。
let context = modelContext // ModelContextのインスタンス
do {
let people = try context.fetch(descriptor)
for person in people {
print("\(person.name): \(person.age)歳")
}
} catch {
print("データ取得エラー: \(error)")
}
フィルタリング(条件指定)
特定の条件に合うデータだけを取得したい場合は、predicateを使用します。
Predicateマクロの基本
iOS 17から、#Predicate
マクロを使って型安全な条件指定ができます。
// 20歳以上の人だけを取得
let descriptor = FetchDescriptor<Person>(
predicate: #Predicate { person in
person.age >= 20
}
)
さまざまな条件指定
// 完全一致
let tokyoPeople = FetchDescriptor<Person>(
predicate: #Predicate { $0.city == "東京" }
)
// 範囲指定
let adults = FetchDescriptor<Person>(
predicate: #Predicate { person in
person.age >= 18 && person.age < 65
}
)
// 文字列の部分一致
let nameContains = FetchDescriptor<Person>(
predicate: #Predicate { person in
person.name.contains("太郎")
}
)
// 複数条件(AND)
let tokyoAdults = FetchDescriptor<Person>(
predicate: #Predicate { person in
person.city == "東京" && person.age >= 20
}
)
// 複数条件(OR)
let condition = FetchDescriptor<Person>(
predicate: #Predicate { person in
person.city == "東京" || person.city == "大阪"
}
)
ソート(並び替え)
データを特定の順序で取得するには、SortDescriptorを使用します。
基本的なソート
// 名前の昇順でソート
let descriptor = FetchDescriptor<Person>(
sortBy: [SortDescriptor(\.name)]
)
// 年齢の降順でソート
let descriptor = FetchDescriptor<Person>(
sortBy: [SortDescriptor(\.age, order: .reverse)]
)
複数項目でのソート
// 都市名で昇順、同じ都市内では年齢の降順
let descriptor = FetchDescriptor<Person>(
sortBy: [
SortDescriptor(\.city, order: .forward),
SortDescriptor(\.age, order: .reverse)
]
)
取得件数の制限
大量のデータから一部だけを取得したい場合は、fetchLimitを使用します。
// 最新10件だけを取得
var descriptor = FetchDescriptor<Person>(
sortBy: [SortDescriptor(\.age, order: .reverse)]
)
descriptor.fetchLimit = 10
// または初期化時に指定
let descriptor = FetchDescriptor<Person>(
sortBy: [SortDescriptor(\.age, order: .reverse)]
)
descriptor.fetchLimit = 10
オフセット(スキップ)
データの途中から取得を開始したい場合は、fetchOffsetを使用します。
// 最初の20件をスキップして、次の10件を取得(ページネーション)
var descriptor = FetchDescriptor<Person>()
descriptor.fetchOffset = 20
descriptor.fetchLimit = 10
これはページネーション(ページ分割)を実装する際に便利です。
実践的な使用例
例1: 検索機能の実装
struct PersonListView: View {
@Environment(\.modelContext) private var context
@State private var searchText = ""
@State private var people: [Person] = []
var body: some View {
List(people) { person in
VStack(alignment: .leading) {
Text(person.name)
.font(.headline)
Text("\(person.age)歳 - \(person.city)")
.font(.subheadline)
.foregroundColor(.gray)
}
}
.searchable(text: $searchText)
.onChange(of: searchText) { oldValue, newValue in
fetchPeople()
}
.onAppear {
fetchPeople()
}
}
private func fetchPeople() {
let descriptor: FetchDescriptor<Person>
if searchText.isEmpty {
// 検索テキストがない場合は全件取得
descriptor = FetchDescriptor<Person>(
sortBy: [SortDescriptor(\.name)]
)
} else {
// 検索テキストで絞り込み
descriptor = FetchDescriptor<Person>(
predicate: #Predicate { person in
person.name.contains(searchText)
},
sortBy: [SortDescriptor(\.name)]
)
}
do {
people = try context.fetch(descriptor)
} catch {
print("取得エラー: \(error)")
}
}
}
例2: 年齢層別の統計
func getAgeStatistics() {
let context = modelContext
// 10代
let teens = FetchDescriptor<Person>(
predicate: #Predicate { $0.age >= 10 && $0.age < 20 }
)
// 20代
let twenties = FetchDescriptor<Person>(
predicate: #Predicate { $0.age >= 20 && $0.age < 30 }
)
// 30代以上
let thirties = FetchDescriptor<Person>(
predicate: #Predicate { $0.age >= 30 }
)
do {
let teenCount = try context.fetch(teens).count
let twentyCount = try context.fetch(twenties).count
let thirtyCount = try context.fetch(thirties).count
print("10代: \(teenCount)人")
print("20代: \(twentyCount)人")
print("30代以上: \(thirtyCount)人")
} catch {
print("エラー: \(error)")
}
}
例3: ランキング表示
func getTopUsers(limit: Int = 5) -> [Person] {
var descriptor = FetchDescriptor<Person>(
sortBy: [SortDescriptor(\.age, order: .reverse)]
)
descriptor.fetchLimit = limit
do {
return try context.fetch(descriptor)
} catch {
print("取得エラー: \(error)")
return []
}
}
SwiftUIとの統合
SwiftUIでFetchDescriptorを使う場合、@Query
マクロが便利です。
@Queryマクロの基本
import SwiftUI
import SwiftData
struct PersonListView: View {
@Query private var people: [Person]
var body: some View {
List(people) { person in
Text(person.name)
}
}
}
@Queryで条件指定
struct AdultListView: View {
@Query(
filter: #Predicate<Person> { $0.age >= 20 },
sort: \.name
) private var adults: [Person]
var body: some View {
List(adults) { person in
Text("\(person.name) - \(person.age)歳")
}
}
}
動的な条件変更
struct FilteredPersonListView: View {
@State private var minimumAge = 0
var body: some View {
VStack {
Slider(value: Binding(
get: { Double(minimumAge) },
set: { minimumAge = Int($0) }
), in: 0...100)
Text("最低年齢: \(minimumAge)歳")
FilteredList(minimumAge: minimumAge)
}
}
}
struct FilteredList: View {
let minimumAge: Int
@Query private var people: [Person]
init(minimumAge: Int) {
self.minimumAge = minimumAge
_people = Query(
filter: #Predicate<Person> { person in
person.age >= minimumAge
},
sort: \.age
)
}
var body: some View {
List(people) { person in
Text("\(person.name) - \(person.age)歳")
}
}
}
パフォーマンスの最適化
1. 必要なデータだけを取得
// 悪い例: すべてのデータを取得してからフィルタリング
let allPeople = try context.fetch(FetchDescriptor<Person>())
let adults = allPeople.filter { $0.age >= 20 }
// 良い例: データベースレベルでフィルタリング
let descriptor = FetchDescriptor<Person>(
predicate: #Predicate { $0.age >= 20 }
)
let adults = try context.fetch(descriptor)
2. 適切な件数制限
// ページネーションで必要な分だけ取得
var descriptor = FetchDescriptor<Person>()
descriptor.fetchLimit = 20
descriptor.fetchOffset = currentPage * 20
3. インデックスを活用
頻繁に検索する属性には@Attribute
でインデックスを設定します。
@Model
class Person {
var name: String
@Attribute(.unique)
var email: String
var age: Int
}
よくあるエラーと対処法
エラー1: 型不一致
// エラー例
let descriptor = FetchDescriptor<Person>(
predicate: #Predicate { $0.age == "20" } // Stringで比較
)
// 修正
let descriptor = FetchDescriptor<Person>(
predicate: #Predicate { $0.age == 20 } // Intで比較
)
エラー2: オプショナル型の扱い
@Model
class Person {
var name: String
var nickname: String? // オプショナル
}
// オプショナルの安全な比較
let descriptor = FetchDescriptor<Person>(
predicate: #Predicate { person in
person.nickname != nil && person.nickname == "タロウ"
}
)
エラー3: 複雑すぎる条件
Predicateマクロでサポートされていない複雑な条件は使えません。
// エラー: カスタム関数は使えない
let descriptor = FetchDescriptor<Person>(
predicate: #Predicate { person in
customValidation(person.name) // ×
}
)
// 対処法: データ取得後にフィルタリング
let allPeople = try context.fetch(FetchDescriptor<Person>())
let filtered = allPeople.filter { customValidation($0.name) }
Core Dataからの移行
Core DataからSwiftDataに移行する場合、以下のように書き換えます。
Core Data
let fetchRequest = NSFetchRequest<Person>(entityName: "Person")
fetchRequest.predicate = NSPredicate(format: "age >= %d", 20)
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
fetchRequest.fetchLimit = 10
let people = try context.fetch(fetchRequest)
SwiftData
var descriptor = FetchDescriptor<Person>(
predicate: #Predicate { $0.age >= 20 },
sortBy: [SortDescriptor(\.name)]
)
descriptor.fetchLimit = 10
let people = try context.fetch(descriptor)
より簡潔で型安全になっています。
まとめ
この記事では、SwiftDataのFetchDescriptorについて解説しました。
重要なポイント
- FetchDescriptorはSwiftDataでデータ取得の設定を定義する構造体
- #Predicateマクロで型安全な条件指定ができる
- SortDescriptorで柔軟なソートが可能
- fetchLimitとfetchOffsetでページネーションを実装できる
- SwiftUIの@Queryマクロと組み合わせると便利
- Core DataのNSFetchRequestより簡潔で安全
FetchDescriptorをマスターすれば、SwiftDataを使ったアプリ開発がよりスムーズになります。最初は基本的な使い方から始めて、徐々に複雑な条件指定やパフォーマンス最適化に挑戦してみてください。
iOS 17以降の新しいプロジェクトでは、Core DataよりもSwiftDataとFetchDescriptorの使用をおすすめします。