【SwiftUI】task の id を使って処理を何度も繰り返す

この id を書き換えることで task のクロージャ内の処理を繰り返すことができるんですね !

onAppear と同様に初回だけだと思ってましたわ。


func task<T>(
    id value: T,
    priority: TaskPriority = .userInitiated,
    _ action: @escaping () async -> Void
) -> some View where T : Equatable

id
The value to observe for changes. The value must conform to the Equatable protocol.

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

 

■ やってみる

公開されている無料現在時刻取得のAPIを使います。


# curl "http://worldtimeapi.org/api/timezone/Asia/Tokyo"

{
  "abbreviation": "JST",
  "client_ip": "2675:6780:4e0:3200:a181:c317:3902:c121",
  "datetime": "2024-03-26T22:10:13.824894+09:00",
  "day_of_week": 2,
  "day_of_year": 86,
  "dst": false,
  "dst_from": null,
  "dst_offset": 0,
  "dst_until": null,
  "raw_offset": 32400,
  "timezone": "Asia/Tokyo",
  "unixtime": 1711458613,
  "utc_datetime": "2024-03-26T13:10:13.824894+00:00",
  "utc_offset": "+09:00",
  "week_number": 13
}

👉 World Time API: Simple JSON/plain-text API to obtain the current time in, and related data about, a timezone. hatena-bookmark

クライアント側はシンプルな実装にしておきます。


let url = URL(string: "https://worldtimeapi.org/api/timezone/Asia/Tokyo.txt")!
var lines: [String] = []
for try await line in url.lines {
  lines.append(line)
}
print(String(lines[2].components(separatedBy: "T")[1]))

// 22:10:13.824894+09:00

👉 【SwiftUI】シンプルに HTTPリクエスト でお天気情報取得 hatena-bookmark

task (id:) を使っておいて、ボタンで id を更新させます。

 

■ まとめ

「id」って便利な使えるやつなんですね !

👉 【SwiftUI】View の 強制再描画 hatena-bookmark


【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 の中身のループは、スリープなしで破棄時に消化されているようです。