【SwiftUI + SwiftData】List のアイテムの Preview

List や LazyVStack などの、

一つの要素の View の #Preview を

簡単に表示するというだけの件。

よくある List 的な View。

その一つの Item の View。


struct ItemView: View {
  var item: Item

  var body: some View {
    HStack {
      Spacer()
      Text("\(item.i)")
      Spacer()
      Text("\(item.s)")
    }
    .font(.largeTitle)
    .monospaced()
    .background()
  }
}

この一つの View だけを #Preview で簡単に表示したい。

 

🤔 #Preview 用の View

Container のセットはより上位の View でセットするのが定石。

 

🤔 使い方

一つ入れ子でクロージャ。


#Preview {
  PreviewOneModelView { item in
    ItemView(item: item)
  }
  .modelContainer(for: Item.self, inMemory: false)
}

 

🤔 参考

Apple 公式サンプルより簡易化して抜粋しました。


public struct ModelPreview<Model: PersistentModel, Content: View>: View {
    var content: (Model) -> Content
    
    public init(@ViewBuilder content: @escaping (Model) -> Content) {
        self.content = content
    }
    
    public var body: some View {
        ZStack {
            PreviewContentView(content: content)
        }
        .backyardBirdsDataContainer(inMemory: true) // * inMemory
    }
    
    struct PreviewContentView: View {
        var content: (Model) -> Content
        
        @Query private var models: [Model]
        @State private var waitedToShowIssue = false
        
        var body: some View {
            if let model = models.first { // * first
                content(model)
            } else {
                ContentUnavailableView {  // * could not get first item
                    Label {
                        Text(verbatim: "Could not load model for previews")
                    } icon: {
                        Image(systemName: "xmark")
                    }
                }
                .opacity(waitedToShowIssue ? 1 : 0)
                .task {
                    Task {
                        try await Task.sleep(for: .seconds(1))
                        waitedToShowIssue = true
                    }
                }
            }
        }
    }
}

👉 sample-backyard-birds/BackyardBirdsData/General/ModelPreview.swift · apple/sample-backyard-birds 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


【Swift】ファイルやディレクトリのパスが長すぎていやだ - URL.shortPath()

ファイルやディレクトリを操作していると、

パスの確認をしますよね。

例えば、


let documents = URL.documentsDirectory

としておいて、


print(documents.path)

あれ、Deprecated ですか。


print(documents.path())

として表示すると、


/Users/me/Library/Developer/Xcode/UserData/Previews/Simulator Devices/AA651DE-1A5C-4AA0-80D0-ADC0FF5AA467/data/Containers/Data/Application/35DFAAB4-E576-4318-9F17-DEC9F0DA259A/Documents

長い、長すぎる。

なんせ URL.homeDirectory までが長すぎる。

ファイル操作ごときが、

なぜか辛く感じるのは、

これのせいでしょうか。

短縮形のエクステンソン作ります。

ただの置換です。


extension URL {
  func shortPath(percentEncoded: Bool = true) -> String {
    path(percentEncoded: percentEncoded)
      .replacingOccurrences(
        of: URL.homeDirectory.path(percentEncoded: percentEncoded),
        with: "/HOME/"
      )
  }
}

いくつかの URL で確認。


print(documents.shortPath())
// /HOME/Documents/

print(
  documents
    .appending(component: "Documents and Settings/")
    .shortPath()
)
// /HOME/Documents/Documents%20and%20Settings/

print(
  documents
    .appending(component: "Documents and Settings", directoryHint: .isDirectory)
    .shortPath(percentEncoded: false)
)
// /HOME/Documents/Documents and Settings/

これで、ログウィンドウがスッキリしました!

ごときが !

 

🤔 参考

👉 【Swift】URL で特定のディレクトリやファイルを指す hatena-bookmark



【Swift】その URL が ファイル なのか ディレクトリ なのか 存在しないのか

これはディレクトリ。


URL.documentsDirectory

では、これは何を指しているのか。


URL.documentsDirectory.appending(component: "xxx")

ファイルなのか、ディレクトリなのか、存在しないのか。


appendingPathComponent(_:) が Deprecated なので、

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

appending(component:directoryHint:) としています。

👉 appending(component:directoryHint:) | Apple Developer Documentation hatena-bookmark

 

🤔 ディレクトリなのか


print(
  URL.documentsDirectory
    .appending(component: "xxx")
    .hasDirectoryPath
)

// false

👉 hasDirectoryPath | Apple Developer Documentation hatena-bookmark

「ディレクトリではない」のですが、

そのURLにファイルがあるのか、

または、何も存在しないのか、

分かりません。

 

🤔 URL.resourceValues(forKeys:) を使う

👉 resourceValues(forKeys:) | Apple Developer Documentation hatena-bookmark

この形でよく使われています。


extension URL {
  var isDirectory: Bool {
    (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
  }
}


print(
  URL.documentsDirectory
    .isDirectory,
  URL.documentsDirectory
    .appending(component: "xxx")
    .isDirectory
)

// true false

extension 内の条件式の左辺は、


URL先にリソースが存在してディレクトリのとき
→ true

URL先にリソースが存在してディレクトリでないとき
→ false

URL先にリソースが存在しないとき 
→ nil

となります。

URLの指すリソースが、ファイルとディレクトリのみであるとすれば、以下のように書くことができますね!


extension URL {
  private var resourceIsDirectory: Bool? {
    (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory
  }

  var exists: Bool {
    resourceIsDirectory != nil
  }

  var isFile: Bool {
    resourceIsDirectory == false
  }

  var isDirectory: Bool {
    resourceIsDirectory == true
  }
}


let documents = URL.documentsDirectory

print(
  documents.exists,
  documents.isDirectory,
  documents.isFile
)
// true true false

let documentsXXX = URL.documentsDirectory
  .appending(component: "xxx")

print(
  documentsXXX.exists,
  documentsXXX.isDirectory,
  documentsXXX.isFile
)
// false false false

 

🤔 まとめ

URLResourceValues が扱う値はいろいろです。

👉 URLResourceValues | Apple Developer Documentation hatena-bookmark

以下に、まとめておきます。

そもそもは、

「ファイルの存在の確認時に、制限のゆるい String に置き換えてからの FileManager.fileExists(atPath:) を使うのが面倒すぎる。」

ということがきっかけでした。

👉 fileExists(atPath:) | Apple Developer Documentation hatena-bookmark

実体とURLは直感と違います。

👉 【Swift】URL で特定のディレクトリやファイルを指す hatena-bookmark