【Swift】URLSession で Passing argument of non-sendable type '(any URLSessionTaskDelegate)?' outside of main actor-isolated context may introduce data races

今日現在、以下の設定でやっています。


 

🧑🏻‍💻 お天気情報を取得する

無料で公開されている WEB API を使います。

シンプルに取得する。


import SwiftUI

struct TestWeatherView: View {
  private let url = URL(string: "https://wttr.in/?format=3")!

  var body: some View {
    Text(try! String(contentsOf: url))
  }
}

#Preview {
  TestWeatherView()
}

ちょっと窮屈なので砕いていきます。


struct TestWeatherView: View {
  @State private var text = ""
  private let url = URL(string: "https://wttr.in/?format=3")!

  var body: some View {
    Text(text)
      .onAppear {
        text = try! String(contentsOf: url)
      }
  }
}

OK です。

次は、Web クライアントを汎用性のある URLSession に変えます。


struct TestWeatherView: View {
  @State private var text = ""
  private let url = URL(string: "https://wttr.in/?format=3")!

  var body: some View {
    Text(text)
      .task {
        let (data, _) = try! await URLSession.shared.data(from: url)
        text = String(data: data, encoding: .utf8)!
      }
  }
}

ここで、警告がでます。

Passing argument of non-sendable type '(any URLSessionTaskDelegate)?' outside of main actor-isolated context may introduce data races

これは、なんですか。

 

🤔 Passing argument of non-sendable type '(any URLSessionTaskDelegate)?' outside of main actor-isolated context may introduce data races

URLSession のドキュメントを見ておきます。


func data(from url: URL) async throws -> (Data, URLResponse)

👉 data(from:) | Apple Developer Documentation hatena-bookmark

Task 内の以下の部分


let (data, _) = try! await URLSession.shared.data(from: url)

左辺はメインスレッド、

--- 境界 ---

右辺はバックグラウンドスレッド

👉 【Swift】concurrency をマスターするための一つのきっかけ hatena-bookmark

ということで、

いわゆる「アクター境界」を越えているので、

data() の返り値は Sendable でなければなりません。

リファレンスやコードを追いかけてみると、

Data, URLResponse 共に Sendable に準拠しています。

👉 Data | Apple Developer Documentation hatena-bookmark
👉 URLResponse | Apple Developer Documentation hatena-bookmark

あ、Tuple かな ?

などと思いましたがなんか違う。

👉 Pitch: User-defined tuple conformances - Evolution / Pitches - Swift Forums hatena-bookmark

 

🧑🏻‍💻 Actor を使う

ActorSendable に準拠しています。


なので、これでバックグラウンドの通信部分をラップします。


actor WeatherA {

  // OK
  static func getData(url: URL) async -> (Data, URLResponse) {
    try! await URLSession.shared.data(from: url)
  }

  // OK
  nonisolated func getData(url: URL) async -> (Data, URLResponse) {
    try! await URLSession.shared.data(from: url)
  }
}

これで、無警告でOKとなりました。

 

🧑🏻‍💻 extension 化してメインスレッドを避ける

こんな方法でもいけます。


extension URLSession {
  func dataEx(url: URL) async -> (Data, URLResponse) {
    try! await data(from: url)
  }
}

 

🧑🏻‍💻 まとめ

分かれば、なるほど感あるけど、分からなければ全く謎で時間だけ食うので、

そんな誰かと自分用のメモとして。

class でも警告なしでいけるようです。



Actor はデータの非同期操作のために作られてる」感じがする。

外野から使ってみた雰囲気だけだけれども。

 

🧑🏻‍💻 参考



【Swift】最もシンプルに実行時間を計測する

これくらいでいいよなあ。


let start = Date()
try? await Task.sleep(for: .milliseconds(1234))
let end = -start.timeIntervalSinceNow
print(String(format: "%.3f", end))
// 1.246



【SwiftData】@Query の検索条件や並び順を変更する

@Query は Observable で便利です。

引数に、検索条件や並び順を書いておけば、

監視して変更があれば、更新してくれます。


👉 【SwiftData】@Query の引数と Descriptor の関係 hatena-bookmark

しかし、この検索条件や並び順の基準は

ユーザーの意図によって変更の必要があることのほうが多いです。

どうやって、書き換えるか。


struct UserListView: View {
  @Query var users: [User]
  
  init(sort: SortDescriptor<User>) {
    _users = Query(sort: [sort])

👉 How to dynamically change a query's sort order or predicate - a free SwiftData by Example tutorial hatena-bookmark


struct BirdsSearchResults<Content: View>: View {
  @Binding var searchText: String
  @Query private var birds: [Bird]
  private var content: (Bird) -> Content
    
  init(searchText: Binding<String>, @ViewBuilder content: @escaping (Bird) -> Content) {
    _searchText = searchText
    _birds = Query(sort: \.creationDate)
    self.content = content
  }

👉 sample-backyard-birds/Multiplatform/Birds/BirdsSearchResults.swift at 1843d5655bf884b501e2889ad9862ec58978fdbe · apple/sample-backyard-birds hatena-bookmark

どうやら、今現在としては、


_birds = Query(sort: \.creationDate)

というような

「_ (アンダースコア)」

を使った内部的な Query の書き換えが本筋といったところでしょうか。

Query の引数には、

SortDescriptorFetchDescriptor もあるので、

複雑な条件により抽出も並び替えも可能です。

👉 【SwiftData】@Query の引数と Descriptor の関係 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】 文字コードの変換 を整理してみた

前にやってみた、これ。


2つの型(プロトコル)の間を相互に変換するだけでも、いろんな書き方があるのだな。

などと思いつつ気になってたのが文字コードの変換。

👉 文字コード - Wikipedia hatena-bookmark

今回、おおまかに整理しておきたい。

 

🧑🏻‍💻 String の内部エンコーディングは UTF-8

Swift 5 switches the preferred encoding of strings from UTF-16 to UTF-8 while preserving efficient Objective-C-interoperability.

👉 Swift.org - UTF-8 String hatena-bookmark

String 内部のエンコーディングは「UTF-8」になったらしいです。

 

🧑🏻‍💻 String、 Data とファイル間での相互変換

図で。大体こんな。

まずは、エンコーディングの引数名が、encoding:, using:, as: と多様なので初見、分かりづらかった。


「String から」または「Stringへ」変換する場合に

「相手」 (「変換先」または「変換元」) のエンコーディングを指定する。

String 自体は「UTF-8」として意識。

Data と ファイル間の変換にはエンコーディングの指定は不要。

Data はそれぞれに変換済みのバイトシーケンス。

 

🧑🏻‍💻 変換してみる

コードを書いて試してみます。

👉 【Swift】よく使いそうな String の format hatena-bookmark


"あいうえお"
  .data(using: .shiftJIS)!.map { String(format: "%02X", $0) }.joined()
// 10 bytes 82A082A282A482A682A8

からスタートして一通り。

以前書いた extension を使っています。

👉 【Swift】ファイルやディレクトリ操作するための extension をまずは作った hatena-bookmark

これで大体いけるはず。

注意としては、「変換できない文字もある」ということ。


o  Shift_JIS → UTF-8 
x  UTF-8     → Shift_JIS

対応する文字がない。

 

🧑🏻‍💻 参考

👉 String.Encoding | Apple Developer Documentation hatena-bookmark
👉 shiftJIS | Apple Developer Documentation hatena-bookmark
👉 init(data:encoding:) | Apple Developer Documentation hatena-bookmark
👉 init(contentsOf:encoding:) | Apple Developer Documentation hatena-bookmark
👉 data(using:allowLossyConversion:) | Apple Developer Documentation hatena-bookmark
👉 write(to:atomically:encoding:) | Apple Developer Documentation hatena-bookmark
👉 init(contentsOf:options:) | Apple Developer Documentation hatena-bookmark
👉 write(to:options:) | Apple Developer Documentation hatena-bookmark