【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 はデータの非同期操作のために作られてる」感じがする。

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

 

🧑🏻‍💻 参考



「iOS デバイスで位置情報対応の Google アプリを使用して設定を変更してください」とは ?

位置がタイムラインに正確に表示できてない。

設定を見てみると、

👉 Google - マイ アクティビティ hatena-bookmark

iOS デバイスで位置情報対応の Google アプリを使用して設定を変更してください

なんやこれ ?

 

🗺️ Google マップ コミュニティ

Google マップ コミュニティで調べます。

タイムラインがいつのまにか記録されなくなり、調べたら「iOS デバイスで位置情報対応の Google アプリを使用して設定を変更してください」となっていて、使っているデバイスが選択出来なくなっていました。googleマップは今までと同じアカウントで同じアプリを使用してるのですが…どうすれば直るでしょうか?

👉 iOS デバイスで位置情報対応の Google アプリを使用して設定を変更してください - Google マップ コミュニティ hatena-bookmark

iPhone で。


設定

 ↓

プライバシーとセキュリティ

 ↓

位置情報サービス

 ↓

Google Maps

これでも、直らん。

 

🗺️ まとめ

精度 (細かさ) が落ちたんでしょうね。

また、AndroidとiOSでは、この元データのスポットの採取タイミングに違いがあり、Androidに比べてiOSのほうが取得頻度が低いため、類推するための情報が少なくなってしまい、余計に直線になってしまうことがあるような気がしています。

取得頻度は、過去は結構な頻度で取得されていたのですが、いま時点は非常に少ないと思います。

👉 タイムライン表示で、元データはスポットとして残っているのに大部分が経路として参照されず、数点を直線で結んでしまうのはなぜでしょう。 - Google マップ コミュニティ hatena-bookmark

ここでも、また、悲しい感じとなってますね。

「iOS デバイスで位置情報対応の Google アプリを使用して設定を変更してください」 は消すことができませんし。


【SwiftUI】再描画の伝播 - @State と @Binding

なんとく「対」で使う雰囲気だけで使っていた @State と @Binding。

そうでもないらしい。

 

🔄 @Binding はいらない

親のカウンターの値を子に渡す。


struct Parent: View {
  // Left side of mutating operator isn't mutable: 'self' is immutable
  // private var count = 0
  @State private var count = 0

  var body: some View {
    VStack {
      Button("\(count)") {
        count += 1
      }
      Child(count: count)
    }
    .padding()
    .background(.yellow)
  }
}

struct Child: View {
  let count: Int

  var body: some View {
    Text("\(count)")
    .padding()
    .background(.white)
  }
}

どうやら、子で @Binding を付けずに受け取っても再描画される。

@State がなくても伝わる? と思ったが不可。

struct 内のプロパティは var でも immutable

らしい。

 

🔄 @Binding を付ける

親に「$」、子に「@Binding」を付けて渡す。


struct Parent: View {
  @State private var count = 0

  var body: some View {
    VStack {
      Button("\(count)") {
        count += 1
      }
      Child(count: $count) // *
    }
    .padding()
    .background(.yellow)
  }
}

struct Child: View {
  @Binding var count: Int // *

  var body: some View {
    Text("\(count)")
    .padding()
    .background(.white)
  }
}

挙動は、最初と同じ。

あれ、@Binding って何だったの?

 

🔄 子で値を更新する

今度は、子でも受け取った値を更新してみます。


struct Parent: View {
  @State private var count = 0

  var body: some View {
    VStack {
      Button("\(count)") {
        count += 1
      }
      Child(count: $count)
    }
    .padding()
    .background(.yellow)
  }
}

struct Child: View {
  @Binding var count: Int

  var body: some View {
    Button("\(count)") {
      count -= 1 // *
    }
    .padding()
    .background(.white)
  }
}

親も更新されます。

双方向に再描画できるようです。

Jetpack Compose では「単方向」のみですが、その前の Android View でいうところの「View Binding」に似ています。

 

🔄 まとめ

  • struct 内のプロパティは var でも変更できないので @State を付ける。
  • その値の変化は子に再描画を @Binding なしで伝播できる。
  • 子で @Binding で受ければ、さらに親にも伝播できる。

参照してるイメージというべきか。

👉 【SwiftUI】@State を 子 View でどう受けるか 🤔 - @Binding hatena-bookmark