【SwiftUI】computed property vs func - computed property を上手に使いたい

なんとなく使っていた computed property と func。

気にはなっていたので。

以下の条件に当てはまる場合は、functionよりpropertyを使う方がよい。

- 例外を投げない
- 計算量が少ない(または初回実行時にキャッシュされる)。
- オブジェクトの状態が変化していない場合、何度呼び出しても同じ結果を返す。

(O(1)ではないcomputed propertyではその旨をドキュメント(コメント)に明記すること。 プロパティアクセスは一般的に計算コストが安価だと見なされるので、そうでない場合はその旨の明記が必要である。)

👉 [Swift] FunctionとComputed Propertyの使い分けの基準 #Swift - Qiita hatena-bookmark

* Top Hightlight
A property expresses an inherent quality of an instance, while a method performs an action.

プロパティはインスタンスの固有の品質を表現し、メソッドはアクションを実行します。

👉 Functions vs Computed property — What to use? | by Aaina jain | Swift India | Medium hatena-bookmark

うむむ、具体的にどう考えたらいいのか。

Apple 公式サンプルをさらう。


struct BackyardsSearchSuggestions: View {
  @Query private var backyards: [Backyard]
    
  var events: [BackyardVisitorEvent] {
    Set(backyards.compactMap(\.currentVisitorEvent))
      .sorted { ($0.backyard?.name ?? "") < ($1.backyard?.name ?? "") }
      .sorted { ($0.bird?.speciesName ?? "") < ($1.bird?.speciesName ?? "") }
    }
    
  var body: some View {
    ForEach(events) { event in

👉 sample-backyard-birds/Multiplatform/Backyards/BackyardsSearchSuggestions.swift at main · apple/sample-backyard-birds hatena-bookmark


struct BirdFoodPickerSheet: View {
  var backyard: Backyard
    
  @Query(sort: [.init(\BirdFood.priority, order: .reverse), .init(\BirdFood.name, comparator: .localizedStandard)])
  private var birdFood: [BirdFood]
    
  @Environment(\.dismiss) private var dismiss
  @State private var presentingBirdFoodShop = false
    
  private let metrics = BirdFoodStoreMetrics.birdFoodStore

  var premiumFood: [BirdFood] {
    birdFood.filter(\.isPremium)
  }
    
  var otherFood: [BirdFood] {
    birdFood.filter { !$0.isPremium }
  }

👉 sample-backyard-birds/Multiplatform/Backyards/BirdFoodPickerSheet.swift at main · apple/sample-backyard-birds hatena-bookmark


struct BirdsSearchSuggestions: View {
  @Query private var birds: [Bird]
    
  var speciesNames: [String] {
    Set(birds.map(\.speciesName)).sorted()
  }
    
  var body: some View {
    ForEach(speciesNames, id: \.self) { speciesName in

👉 sample-backyard-birds/Multiplatform/Birds/BirdsSearchSuggestions.swift at main · apple/sample-backyard-birds hatena-bookmark


struct PlantsSearchSuggestions: View {
  @Query private var plants: [Plant]
    
  var speciesNames: [String] {
    Set(plants.map(\.speciesName)).sorted()
  }
    
  var body: some View {
    ForEach(speciesNames, id: \.self) { speciesName in

👉 sample-backyard-birds/Multiplatform/Plants/PlantsSearchSuggestions.swift at main · apple/sample-backyard-birds hatena-bookmark


struct BackyardBirdsPassShop: View {
  @Environment(\.dismiss) private var dismiss
  @Environment(\.passIDs.group) private var passGroupID
  @Environment(\.passStatus) private var passStatus
    
  private var showPremiumUpgrade: Bool {
    passStatus == .individual || passStatus == .family
  }

👉 sample-backyard-birds/Multiplatform/Shop/BackyardBirdsPassShop.swift at main · apple/sample-backyard-birds hatena-bookmark


struct BackyardsSearchSuggestions: View {
  @Query private var backyards: [Backyard]
    
  var events: [BackyardVisitorEvent] {
    Set(backyards.compactMap(\.currentVisitorEvent))
      .sorted { ($0.backyard?.name ?? "") < ($1.backyard?.name ?? "") }
      .sorted { ($0.bird?.speciesName ?? "") < ($1.bird?.speciesName ?? "") }
  }
    
  var body: some View {
    ForEach(events) { event in

👉 sample-backyard-birds/Multiplatform/Backyards/BackyardsSearchSuggestions.swift at main · apple/sample-backyard-birds hatena-bookmark

View 内の利用状況だけをみると、

ほぼ @Query の加工にしか使ってない。

逆に言えば

@Query の加工には Computed Property が使える。

ということは言えそう。

ちなみに、View 内に func はほとんど見当たらない。


【iOS】System Sound Wheel Picker

ひとつの View だけで

その端末の組み込みのシステム音を

確認できたらいいかも、

ということで。

GIF って音声出ませんでした ?



以下、音が出てます。



確認の範囲として、

SystemSoundID は 1 から 4000 まで。

実際に音を並列で鳴らして 0.0025 秒以下の音は無視してます。

非同期の処理はここでも難しい印象を受けます。

👉 【Swift】SystemSoundID 一覧がないのですが hatena-bookmark


【Swift】非同期関数の直列実行 - withCheckedContinuation

ビルトイン非同期関数の直列処理って自力でやるにはつらいですよね。

まず、ボタンを押して、音を鳴らします。


Button("Play1") {
  AudioServicesPlaySystemSound(1000)
}

純粋なUIスレッドではないところで再生処理がされています。

いわゆる

「UIスレッドと非同期された処理」

ですね。

続いて、


Button("Play2") {
  AudioServicesPlaySystemSound(SystemSoundID(1000))
}

これも同じ音が再生されます。

この組み込みシステム音声を鳴らす関数


func AudioServicesPlaySystemSound(_ inSystemSoundID: SystemSoundID)

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

の引数の型は SystemSoundID ですので直しておきます。

中を見てみると、


/**
    @typedef        SystemSoundID
    @abstract       SystemSoundIDs are created by the System Sound client application
                    for playback of a provided AudioFile.
*/
public typealias SystemSoundID = UInt32

UInt32 の typealias であることが分かります。

この SystemSouldID は他にもたくさんいろいろあります。

が、端末やOSバージョンによって異なるので注意が必要なようですね。


続いて、11個連続再生してみます。


Button("Play3") {
  for i in 1000...1010 {
    AudioServicesPlaySystemSound(SystemSoundID(i))
  }
}

あれ。

残念なことに音が重なって同時に再生されます。

それぞれ、どんな音が鳴っているか分かりません。

非同期処理の関数が並列で処理されているのです。

 

🤔 非同期関数の直列化

連続で順番に音を再生したい。

言い換えれば、

非同期関数の直列処理

をしたいですよね。

まず、音声再生処理終了のタイミングから処理を実行できる

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

を使って音を1つだけ鳴らしてみます。


Button("Play4") {
  AudioServicesPlaySystemSoundWithCompletion(SystemSoundID(1000)) {
    print("completed.")
  }
}

これで、終了のタイミングを取得できます。

続いて、これを連続で実行します。

今どきの Swift では、以下の関数を使うらしいです。

👉 withCheckedContinuation(function:_:) | Apple Developer Documentation hatena-bookmark
👉 withCheckedThrowingContinuation(function:_:) | Apple Developer Documentation hatena-bookmark

これを使って書き換えます。


Button("Play5") {
  Task {
    for i in 1000...1010 {
      await withCheckedContinuation { continuation in
        AudioServicesPlaySystemSoundWithCompletion(SystemSoundID(i)) {
          continuation.resume()
        }
      }
    }
  }
}

クロージャー内は同期処理となって await が必要となりますので Task をかぶせてます。

音声なので、特にスレッドをメインに指定することなどは不要のようです。

以下、それぞれの実行音声動画です。



 

🤔 まとめ

「非同期処理の直列化」は用意されている関数の

👉 withCheckedContinuation(function:_:) | Apple Developer Documentation hatena-bookmark
👉 withCheckedThrowingContinuation(function:_:) | Apple Developer Documentation hatena-bookmark

を使うと良い。