【SwiftData】@Query の引数と Descriptor の関係

@Query て何となく直感で使ってたけど、記述が豊富ですよね。


@Query(sort: \Snippet.creationDate, order: .reverse)
var snippets: [Snippet]


@Query(sort: [SortDescriptor(\Snippet.creationDate)])
var allSnippets: [Snippet]
 
@Query(
  filter: #Predicate<Snippet>  { $0.isFavorite },
  sort: [SortDescriptor(\.creationDate)] 
)
var favoriteSnippets: [Snippet]


static var fetchDescriptor: FetchDescriptor<Recipe> {
  let descriptor = FetchDescriptor<Recipe>(
    predicate: #Predicate { $0.isFavorite == true },
    sortBy: [.init(\.createdAt)]
  )
  descriptor.fetchLimit = 10
  return descriptor
}

@Query(FavoriteRecipesList.fetchDescriptor) private var favoriteRecipes: [Recipe]

ドキュメント見てみると、書き方が多すぎ。

👉 Query() | Apple Developer Documentation hatena-bookmark

どれもデータ取得時の操作をしてるのだろうけど、

上手に使えるように整理しておきたい。

 

■ 引数

ドキュメントから引数まわりをすべて抽出して眺めながら並べ替えてみる。


Query<Value, Element>(filter: Predicate<Element>?, sort: KeyPath<Element, Value>, order: SortOrder, transaction: Transaction?)
Query<Value, Element>(filter: Predicate<Element>?, sort: KeyPath<Element, Value?>, order: SortOrder, transaction: Transaction?)
Query<Value, Element>(filter: Predicate<Element>?, sort: KeyPath<Element, Value>, order: SortOrder, animation: Animation)
Query<Value, Element>(filter: Predicate<Element>?, sort: KeyPath<Element, Value?>, order: SortOrder, animation: Animation)

Query<Element>(filter: Predicate<Element>?, sort: [SortDescriptor<Element>], transaction: Transaction?)
Query<Element>(filter: Predicate<Element>?, sort: [SortDescriptor<Element>], animation: Animation)

Query<Element>(FetchDescriptor<Element>, transaction: Transaction?)
Query<Element>(FetchDescriptor<Element>, animation: Animation)

Query(transaction: Transaction)
Query(animation: Animation)

Transaction、Animation を除くと、引数は、


filter: Predicate<Element>
sort: KeyPath<Element, Value>
order: SortOrder
sort: [SortDescriptor<Element>]
FetchDescriptor<Element>

の5つであることがわかります。

上のサンプルコードからも、直感的に、


Predicate  →  抽出条件のフィルター (filter)
KeyPath    →  並び替えのキー (sort)
SortOrder  →  並び替えの順序 (order)

は意味が分かりますが。

「Descriptor」

とは何なのか。

 

■ SortDescriptor

「並び替えのキーと順序の入れ物」のようです。

並び替え順序は省略できます。


@Query(
  sort: [
    SortDescriptor(\Movie.title), 
    SortDescriptor(\Movie.releaseYear, order: .reverse)
  ]
) 
var movies: [Movie]

SortDescriptor に入れることで、複数の並び替えのキーを配列に入れて @Query にセットできるようになります。

なので、細かい並び替えの指定ができます。

👉 SortDescriptor | Apple Developer Documentation hatena-bookmark

 

■ FetchDescriptor

「抽出条件の Predicate と 並び替えの配列 [SortDescriptor] の入れ物」です。


init(
  predicate: Predicate<T>? = nil,
  sortBy: [SortDescriptor<T>] = []
)

👉 init(predicate:sortBy:) | Apple Developer Documentation hatena-bookmark

なので、@Query に入れる抽出条件と並び替えの指定は、すべてこれに入れることができるようになります。

しかし、@Query より ModelContext.fetch() で使われる方が多い印象。

👉 FetchDescriptor | Apple Developer Documentation hatena-bookmark

 

■ まとめ

ざっくりそれぞれの関係を表でまとめておきます。

以下、GitHub 公開コードより抜粋列挙。

@Query を使う場合は、大体どれかと似た形になりそうです。


@Query private var birds: [Bird]


@Query(sort: \Backyard.creationDate)
private var backyards: [Backyard]


@Query(
  filter: #Predicate<BirdFood> { $0.id == "Nutrition Pellet" }
)
private var birdFood: [BirdFood]


@Query(
  filter: #Predicate<Episode> {$0.finishedPlaying != true },
  sort: [
    SortDescriptor(\Episode.pubDate, order: .reverse)
  ]
) 
var episodes: [Episode]


@Query(
  sort: [
    SortDescriptor(\Station.order), 
    SortDescriptor(\Station.stationName)
  ]
)
private var stations: [Station]


@Query(
  FetchDescriptor<Album>(
    predicate: #Predicate { $0.parentAlbum == nil },
    sortBy: [SortDescriptor<Album>(\.name)]
  )
) 
var albums: [Album]

基本的な操作はこれだけでいけるかな。無理かな。


【SwiftUI】View の 強制再描画

ProgressView を使って簡単なインジケーターをつくる。


struct AutoProgressView: View {
  @State private var value = 0.0
  private let total = 10.0
  private let duration = 1.0

  var body: some View {
    ProgressView(value: value, total: total)
      .task {
        while value < total {
          try? await Task.sleep(for: .seconds(duration))
          value += duration
        }
      }
  }
}

便利ですねこれ。

👉 ProgressView | Apple Developer Documentation hatena-bookmark

しかし、これを、再スタートしようとすると、

できない。どうやるのこれ。


struct TestView: View {
  var body: some View {

    AutoProgressView()

    Button("restart") {
      // ?
    }

  }
}

たいそうにタイマーまで付けてやりたくない。

View をリフレッシュするだけでいい。

 

■ 強制再描画

「View に id を付けてそれを更新すればいい」らしい。

👉 id(_:) | Apple Developer Documentation hatena-bookmark


struct TestView: View {
  @State var id = false

  var body: some View {

    AutoProgressView()
      .id(id)

    Button("restart") {
      id.toggle()
    }

  }
}

 

■ まとめ

さすが、先人先生。

ありがとうございます。

👉 [SwiftUI] ViewのIdentityと再描画を意識しよう hatena-bookmark

ちなみに、task の中身のループは、スリープなしで破棄時に消化されているようです。


【Swift】公式サンプル Logger の使い方


print("debug: \(value)")

とかではつらいのかな。

と思いつつ抽出して使ってみる。

👉 Code search results hatena-bookmark


import OSLog
private let logger = Logger(subsystem: "BackyardBirds", category: "BackyardBirdsPassStatus")


logger.log("""
  Processing transaction ID \(unsafeTransaction.id) for \
  \(unsafeTransaction.productID)
""")


logger.debug("""
  Transaction ID \(t.id) for \(t.productID) is verified
""")


logger.error("""
  Transaction ID \(t.id) for \(t.productID) is unverified: \(error)
""")


logger.info("Providing updated status to data generation")

出力レベルがあるんですね。

出力は、


2023-09-16 18:26:11.842691+0900 Develop[99999:999999] [Standard] Debug: Uhooi
2023-09-16 18:26:11.842722+0900 Develop[99999:999999] [Standard] Info: Uhooi
2023-09-16 18:26:11.842766+0900 Develop[99999:999999] [Standard] Notice: Uhooi
2023-09-16 18:26:11.842811+0900 Develop[99999:999999] [Standard] Error: Uhooi
2023-09-16 18:26:11.842858+0900 Develop[99999:999999] [Standard] Fault: Uhooi

{Timestamp} {Library}[{PID}:{TID}] [{Category}] {Message} の形式で出力されています。

👉 os.Loggerの説明と使い方(Swift) #Swift - Qiita hatena-bookmark

なるほど。

 

■ やってみる


import OSLog
private let logger = Logger(subsystem: "subsystem", category: "category")

// ... 

  static func create(modelContainer: ModelContainer) {
    
    logger.debug("debug Creating service instance.")
    logger.trace("trace Creating service instance.")
    logger.info("infoCreating service instance.")
    logger.notice("notice Creating service instance.")
    logger.error("error Creating service instance.")
    logger.warning("warning Creating service instance.")
    logger.fault("fault Creating service instance.")
    logger.critical("critical Creating service instance.")

    shared = SoundDataService(modelContainer: modelContainer)
  }


debug Creating service instance.
trace Creating service instance.
infoCreating service instance.
notice Creating service instance.
error Creating service instance.
warning Creating service instance.
fault Creating service instance.
critical Creating service instance.

あれ、タイムスタンプ的なのデフォルトで出ないのか。

subsystem とか category とかも何の意味があるのか。

print() で良くね?

なんなのこれ。

あ、これか。

extension 化しとくのが楽かもしれません。


extension OSLog {
  static let ui = Logger(subsystem: "com.satoriku.OSLog", category: "ui")
  static let network = Logger(subsystem: "com.satoriku.OSLog", category: "network")
  static let viewCycle = Logger(subsystem: "com.satoriku.OSLog", category: "viewcycle")
}

👉 【Xcode/Swift】OSLogを使ってアプリログを出力する方法(ロギング) - iOS-Docs hatena-bookmark


import Foundation
import os

extension Logger {
    private static var subsystem = Bundle.main.bundleIdentifier!
    static let segue = Logger(subsystem: subsystem, category: "segue")
    static let note = Logger(subsystem: subsystem, category: "note")
    static let flashcard = Logger(subsystem: subsystem, category: "flashcard")
    static let deck = Logger(subsystem: subsystem, category: "deck")
    static let settings = Logger(subsystem: subsystem, category: "settings")
    static let option = Logger(subsystem: subsystem, category: "option")
}

👉 Flashcard-Adder/Flashcard Adder/Logger.swift at bfa5f3526fba48f5c6596f92d7648e4137fa60c9 · Nonameentered/Flashcard-Adder hatena-bookmark
👉 OSLog and Unified logging as recommended by Apple - SwiftLee hatena-bookmark

👉 Logger | Apple Developer Documentation hatena-bookmark