【SwiftData】@Query の検索条件や並び順を変更する

@Query は Observable で便利です。

引数に、検索条件や並び順を書いておけば、

監視して変更があれば、更新してくれます。


👉 【SwiftData】@Query の引数と Descriptor の関係 hatena-bookmark

しかし、この検索条件や並び順の基準は

ユーザーの意図によって変更の必要があることのほうが多いです。

どうやって、書き換えるか。


struct UserListView: View {
  @Query var users: [User]
  
  init(sort: SortDescriptor<User>) {
    _users = Query(sort: [sort])

👉 How to dynamically change a query's sort order or predicate - a free SwiftData by Example tutorial hatena-bookmark


struct BirdsSearchResults<Content: View>: View {
  @Binding var searchText: String
  @Query private var birds: [Bird]
  private var content: (Bird) -> Content
    
  init(searchText: Binding<String>, @ViewBuilder content: @escaping (Bird) -> Content) {
    _searchText = searchText
    _birds = Query(sort: \.creationDate)
    self.content = content
  }

👉 sample-backyard-birds/Multiplatform/Birds/BirdsSearchResults.swift at 1843d5655bf884b501e2889ad9862ec58978fdbe · apple/sample-backyard-birds hatena-bookmark

どうやら、今現在としては、


_birds = Query(sort: \.creationDate)

というような

「_ (アンダースコア)」

を使った内部的な Query の書き換えが本筋といったところでしょうか。

Query の引数には、

SortDescriptorFetchDescriptor もあるので、

複雑な条件により抽出も並び替えも可能です。

👉 【SwiftData】@Query の引数と Descriptor の関係 hatena-bookmark

しかし、他の方法ないんですかね。


【Apple】歴代 Apple WWDC やイベントの動画を一括にダウンロードする方法 WWDC24 も追加更新中

いわゆる ffmpeg を使った シェルスクリプトです。

以下のようなものです。


ffmpeg -i https://devstreaming-cdn.apple.com/videos/wwdc/2020/10691/2/A92788CB-81ED-4CCF-B6B1-4DD7A1F3E87D/hvc_2160p_16800/prog_index.m3u8 -c copy "Session - 10691 temp.mp4"
ffmpeg -i https://devstreaming-cdn.apple.com/videos/wwdc/2020/10691/2/A92788CB-81ED-4CCF-B6B1-4DD7A1F3E87D/audio_english_192/prog_index.m3u8 -c copy "Session - 10691 temp.aac"
ffmpeg -i "Session - 10691 temp.mp4" -i "Session - 10691 temp.aac" -c copy "Session 10691 - [email protected]"
rm "Session - 10691 temp.mp4"
rm "Session - 10691 temp.aac"

しかし、なぜか QuickPlayer で映像が見えず音声だけしか再生されない。

VLCなど別の動画プレーヤーではフツーに見れます。

現在開催中の WWDC24 の動画分も更新中の様子。

まとめてみたい人は便利かもしれません。

👉 dmthomas/AppleVideoDownloadScripts: Script to download higher resolutions of Apple event videos using ffmpeg hatena-bookmark


【Swift】ファイルやディレクトリ操作するための extension をまずは作った

ファイルの操作がなんか混乱してストレス。

直感的にまずは作っておく。


FileManager.default.showContents(.temporaryDirectory)

// /HOME/tmp/ 
// /HOME/tmp/.DS_Store [6 kB]
// /HOME/tmp/CFNetworkDownload_1Ji1Ym.tmp [1.8 MB]
// /HOME/tmp/CFNetworkDownload_3ravkG.tmp [1.8 MB]
// /HOME/tmp/CFNetworkDownload_ArezWZ.tmp [1.8 MB]
// /HOME/tmp/CFNetworkDownload_Q2vc3J.tmp [920 kB]
// /HOME/tmp/CFNetworkDownload_WnW3m7.tmp [1.8 MB]
// /HOME/tmp/CFNetworkDownload_s4wpzr.tmp [1.8 MB]

既存の URL、FileManager にぶつからないように作りたい。

あくまで、補助、簡素化。

使い勝手で Gist 更新していきたいです。

いまどきの スマホOS は必要以上の高機能で、

開発者向け SDK や public API までも初心者泣かせの仕様です。


 

🤔 参考

👉 【Swift】FileManager を使いたい hatena-bookmark
👉 【Swift】そのディレクトリ内を再帰的に確認する hatena-bookmark
👉 【Swift】ファイルやディレクトリのパスが長すぎていやだ - URL.shortPath() hatena-bookmark
👉 【Swift】URL で特定のディレクトリやファイルを指す hatena-bookmark
👉 【Swift】その URL が ファイル なのか ディレクトリ なのか 存在しないのか hatena-bookmark


【Apple WWDC24】Swift ネタをサンプルコードとともに眺めていくページ

いよいよです。



👉 WWDC24 - Apple Developer hatena-bookmark

Apple 公式の GitHub では、

「sample」

の prefix をつけてリポジトリを公開していますよね。

洗練されていて、Swift の方向性も示されてるようで。

👉 Repository search results hatena-bookmark

いや、もしかしたら、GitHub 公開は遅れるかもしれん。

(更新中 ...)



【Swift】FileManager を使いたい

最初、無駄にややこしくて使いづらい気がした。

しかし、整理できるとそうでもない。


let fileManager = FileManager.default
let documents = URL.documentsDirectory

Button("create file") {
  // new.txt
  let text = "Hello!"
  let to = documents.appending(component: "new.txt")
  try? text.write(to: to, atomically: true, encoding: .utf8)
}

Button("read file") {
  // new.txt
  let file = documents.appending(component: "new.txt")
  let text = (try? String(contentsOf: file, encoding: .utf8)) ?? "ERROR"
  print(text)
}

Button("create directory") {
  // some/
  let directory = documents.appending(component: "some/")
  try? fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
}

Button("copy file") {
  // new.txt - copy -> some/copied.txt
  let at = documents.appending(component: "new.txt")
  let to = documents.appending(component: "some/copied.txt")
  try? fileManager.copyItem(at: at, to: to)
}

Button("move file") {
  // new.txt - move -> some/moved.txt
  let at = documents.appending(component: "new.txt")
  let to = documents.appending(component: "some/moved.txt")
  try? fileManager.moveItem(at: at, to: to)
}

Button("copy directory") {
  // some/ - copy -> another/
  let at = documents.appending(component: "some/")
  let to = documents.appending(component: "another/")
  try? fileManager.copyItem(at: at, to: to)
}

Button("delete file") {
  // some/moved.txt
  let dir = documents.appending(component: "some/")
  let file = dir.appending(component: "moved.txt")
  try? fileManager.removeItem(at: file)
}

Button("delete directory") {
  // some/ and another/
  let dir1 = documents.appending(component: "some/")
  let dir2 = documents.appending(component: "another/")
  try? fileManager.removeItem(at: dir1)
  try? fileManager.removeItem(at: dir2)
}

つづいて、ディレクトリを指定するだけで、

そのディレクトリ内に存在するディレクトリとファイルの状態を表示できるようにしておきます。

実体の確認を頻繁にしやすくしておくこと大事。



extension FileManager {
  private func contents(directory url: URL) -> [URL] {
    (try? contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])) ?? []
  }

  func showContents(_ url: URL = .documentsDirectory) {
    print(
      String(format: "%@ %@", url.shortPath(), url.isFile ? "[\(url.fileSize)]" : "")
    )

    if url.isDirectory {
      contents(directory: url)
        .sorted(by: {
          let lr = [$0, $1].map {
            ($0.path().components(separatedBy: "/").dropLast().joined(), $0.path())
          }
          return lr[0] < lr[1]
        })
        .forEach { content in
          showContents(content)
        }
    }
  }
}


FileManager.default.showContents(.documentsDirectory)

// /HOME/Documents/ 
// /HOME/Documents/new.txt [6 bytes]
// /HOME/Documents/another/ 
// /HOME/Documents/another/copied.txt [6 bytes]
// /HOME/Documents/some/ 
// /HOME/Documents/some/copied.txt [6 bytes]
// /HOME/Documents/some/moved.txt [6 bytes]

👉 contentsOfDirectory(at:includingPropertiesForKeys:options:) | Apple Developer Documentation hatena-bookmark

これぐらいでいいか。

Permission とか mask の話しはまたそのうちやりたいです。



 

🤔 まとめ

コツとしては、URL の取り扱い。


ファイルやディレクトリは URL を使って指す。


URL を path() などを使って String に変換するのは表示直前のみ。


URL は実体の情報を保持していない。

という当たり前の言葉が思いつく。

しかし、最初は混乱したし、すると思う。

String のほうが直感的で人間に近いし、

古い API を見てると String ベースでファイルを操作してる。

フツーに検索しても古い記述がヒットすることが多い。

 

🤔 参考

👉 【Swift】ファイルやディレクトリ操作するための extension をまずは作った hatena-bookmark
👉 【Swift】URL で特定のディレクトリやファイルを指す hatena-bookmark