MENU

【SwiftData入門】FetchDescriptorとは?使い方を徹底解説

iOS 17で登場したSwiftDataは、これまでのCore Dataに代わる新しいデータ永続化フレームワークです。その中核を担うのがFetchDescriptorです。

この記事では、FetchDescriptorの基本から実践的な使い方まで、Swift初心者にもわかりやすく解説します。

目次

FetchDescriptorとは?

FetchDescriptorは、SwiftDataでデータベースからデータを取得するための設定を定義する構造体です。

データの取得条件、ソート順、取得件数などを指定して、必要なデータだけを効率的に取得できます。

Core DataのNSFetchRequestとの違い

これまでCore Dataを使っていた方は、NSFetchRequestとの違いが気になるかもしれません。

項目NSFetchRequestFetchDescriptor
型安全性実行時エラーの可能性コンパイル時に型チェック
構文文字列ベース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の使用をおすすめします。

プログラミングの独学におすすめ
プログラミング言語の人気オンラインコース
独学でプログラミングを学習している方で、エラーなどが発生して効率よく勉強ができないと悩む方は多いはず。Udemyは、プロの講師が動画で実際のプログラムを動かしながら教えてくれるオンライン講座です。講座の価格は、セール期間中には専門書籍を1冊買うよりも安く済むことが多いです。新しく学びたいプログラミング言語がある方は、ぜひUdemyでオンライン講座を探してみてください。
目次