【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


【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

を使うと良い。




【Swift】SystemSoundID 一覧がないのですが

ちょっとしたときに、

あらかじめ用意されているシステム音を使いたい。


AudioServicesPlaySystemSound(1000)

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

これだけで音が鳴る。

「1000」 は SystemSoundID というもののようなので、

「どんな音が鳴るのかリスト」を探すが、

公式で見つけられなかった。

以下にそれらしきもの。


👉 AudioServices - iPhone Development Wiki hatena-bookmark

結構、古そうなので今どきの状態を確認したい。

OSやバージョンなど環境によっても違うっぽい。

なので、コードからのぞいてみる。

 

🔊 欠番ID

ID は 2000 ぐらいまでの整数なのですが、

非公式のリストを見る限り、番号が歯抜け状態。

音声データの長さで「欠番らしき」を見分ける。

実際に再生してその時間を取得しました。


let start = Date()
AudioServicesPlaySystemSoundWithCompletion(1000) {
   let elapsed = Date().timeIntervalSince(start)
   print(elapsed)
}

// 1.566209077835083

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

おおまかに 再生時間 50ミリ秒 以下が欠番っぽい。

 

🔊 グリッドでボタン

ボタンを並べて Preview や Simulator や 実機 でどんな音があるのか確かめます 。



 

極力、deprecated な関数は避けていきたいです。

 

🔊 まとめ

実際に音声ファイルを作成する前に、雰囲気を確認したいときに使いたくなります。

しかし、ID が分からない。

そんな人用。

ファイルで探すなら、各環境内でディレクトリ名で走査するといいのですね !


Library/Audio/UISounds
❯ tree .
.
├── 3rd_party_critical.caf
├── AuthenticationMatch_Full.caf
├── AuthenticationMatch_Short.caf
...
├── ussd.caf
├── warsaw.caf
└── wheels_of_time.caf

4 directories, 321 files

あと、アプリとかショートカットもありました。

👉 「Play System Sounds」をApp Storeで hatena-bookmark
👉 System Sounds - Shortcuts hatena-bookmark