【SwiftUI】 onChange() の 書き方

iOS17でクロージャ引数が1つのものは Deprecated と警告が出たりするので、


👉 onChange(of:perform:) | Apple Developer Documentation hatena-bookmark

こんなかんじで書いてました。


Text("\(count)")
  .onChange(of: token) { _, newValue in
    // do something ...
  }

クソですね !

 

■ クロージャの引数

クロージャの引数なしと2つのものがあるんですね !


func onChange<V>(
    of value: V,
    initial: Bool = false,
    _ action: @escaping () -> Void
) -> some View where V : Equatable

👉 onChange(of:initial:_:) | Apple Developer Documentation hatena-bookmark


func onChange<V>(
    of value: V,
    initial: Bool = false,
    _ action: @escaping (V, V) -> Void
) -> some View where V : Equatable

👉 onChange(of:initial:_:) | Apple Developer Documentation hatena-bookmark

よって、引数なしのほうを使って、

フツーに以下のように書けば良かったのか。


Text("\(count)")
  .onChange(of: token) {
    print("\(token)")
  }

なるほど。

IDE の選択肢の一番上をなんとなく選択してたのだろうと思います。

きちんと、ドキュメントを見ることって大事。

考えてみると、引数2つの記述


Text("\(count)")
  .onChange(of: token) { oldValue, newValue in
    // do something ...
  }

は、oldValue が必要でない限り使う必要がないな。


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


【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