Swift Concurrencyメインスレッドクイズ5問

実行環境

>swift --version
swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
Target: arm64-apple-macosx14.0

> xcodebuild -version
Xcode 15.3
Build version 15E204a

モチベーション

コードレビューをしている時に、viewWillTransition(to:with:)内で行う表示関係の処理はそのメソッド自体がメインスレッドで呼ばれるから、明にメインスレッドでやるように書かなくても良いのでは?という話題があった。その時は根拠が無くて体感レベルの理解だったが、体感でコードを書きたくないので、id:maiyama4 の以下の記事や各種プロポーザルを見て理解を深めた。

zenn.dev

知識の定着のために、Thread.isMainThread の値がどうなるかのクイズを5問作ったので解いてみてほしい。

Question 1

gist.github.com

答えと解説

答え

true

解説

Question 2

gist.github.com

答えと解説

答え

true

解説

Question 3

gist.github.com

答えと解説

答え

false

解説

Question 4

gist.github.com

答えと解説

答え

false

解説

Question 5

gist.github.com

答えと解説

答え

true

解説

おわりに

Question 4については先週の自分だったら答えと解説をセットで答えられなかった。難しい、というか実行されるActorの意識が無かった。また、メインスレッドで実行したい関数はTask側でMainActorを指定するのではなくて、関数自体 or そのメソッドを持つクラスや構造体に指定した方が良さそう。なぜなら、呼び出し先がasync functionならActorを引き継がないため、呼び出し元でメインスレッドで実行するかどうかを考えなくて済むから。

Apollo iOSの @defer ディレクティブの実装を見る

モチベーション

Apollo iOSのリリースページを見ていたら、preview-defer.1 というものがあった。

github.com

実験的に @defer ディレクティブの実装が行われている。v1.9.2時点のロードマップにも書いてある。

https://github.com/apollographql/apollo-ios/blob/1.9.2/ROADMAP.md#defer-support

今回はそのディレクティブについて調べたりコードを読んだのでメモを書く。

@defer ディレクティブ

即座に返す必要がないということを表現するディレクティブ。 2024/03/10時点ではまだGraphQL specificationに追加されていない概念で、RFCはある。

github.com

クエリするデータの中で取得に時間がかかるものは後回しにしてそれ以外を早く受け取りたい時がある。そういう時に今までだとクエリの分割やプリフェッチで対応していたが、リクエスト数が増えたり、リクエストが増えるということは個々でレスポンスをハンドリングしないといけなくなる等の課題があった。

解決方法として、 @defer ディレクティブが考えられた。@defer が付与されたもの以外は最初のレスポンスで返され、付与されたものが後続のレスポンスで返ってくる。

Swiftにおけるコード生成後の @defer の表現

Property Wrapperとして表現されている。@defer を付与したフラグメントがコード生成をされるとそのProperty Wrapperが付与される。

https://github.com/apollographql/apollo-ios/blob/preview-defer.1/Sources/ApolloAPI/Deferred.swift

  • wrappedValue: 値を受け取っていれば値を返し、そうでなければnilを返している。
  • projectedValue: .pending, .notExecuted, .fullFilled(Fragment) をcaseに持つEnumを返している。

基本的にwrappedValueを使うだけで事足りそうで、.notExecuted の時にハンドリングしたい時だけprojectedValue使えば良さそう。表示するデータが部分的に取得失敗して、その時用に表示の出し分けをする、ということが考えられそう。コメント内容的に実装時に分かりたいエラーな気もするので違うかも。

ちなみに、Apollo Kotlinでは既に実装されており、nullableで表現されている。Apollo iOSでも案としてあったが、表現が複雑になるため採用されなかった模様*1

https://www.apollographql.com/docs/kotlin/fetching/defer/#:~:text=In%20the%20generated%20code%20for%20this%20query%2C%20the%20onUser%20field%20for%20the%20fragment%20will%20be%20nullable.

その他メモ

  • 元々Apollo iOSの実装で PossiblyDeferred というものがあって名前ややこしいなと思ったけどリネームされるようだ。
  • クライアント側で解決しないので、サーバー側も実装する必要がある。サーバー側の実装は Transfer-Encoding: chunkedContent-Type: multipart/mixed を組み合わせて通信するのが一般的のようだ。
    • Transfer-Encoding: chunked: 分割して送るパート
    • Content-Type: multipart/mixed: 異なる種類のデータを送るパート

参考

transformPreference(_:_:)を使った子要素からの値伝搬

開発環境

$xcodebuild -version
Xcode 15.2
Build version 15C500b
  • 動作確認したシミュレータ: iPhone 15 Pro Max(iOS 17.2)

モチベーション

SwiftUIでviewの親と子それぞれに preference(key:value:) を指定すると、親のPreference値だけがその上位存在の onPreferenceChange(_:perform:) にて受け取ることができる。子の値も送りたい...。

import SwiftUI

struct CustomPreferenceKey: PreferenceKey {
  typealias Value = [String]
  
  static var defaultValue: Value = []
  
  static func reduce(value: inout Value, nextValue: () -> Value) {
    value += nextValue()
  }
}

struct ContentView: View {
  var body: some View {
    VStack {
      VStack {
        Text("Hello, world! 1")
          .preference(key: CustomPreferenceKey.self, value: ["child-1"])
        Text("Hello, world! 2")
          .preference(key: CustomPreferenceKey.self, value: ["child-2"])
      }
      .preference(key: CustomPreferenceKey.self, value: ["parent-1"])
    }
    .onPreferenceChange(CustomPreferenceKey.self) { value in
      print("received: \(value)")  // received: ["parent-1"]
    }
  }
}

transformPreference(_:_:)

この問題に出くわした当時はbackgroundにviewを生成し、親子関係にならないようにして回避した。落ち着いた後に「わりとよくある使用パターンだから公式が何か用意しているのではないか」と思ってワークアラウンドを施している悔しさからPreferenceのドキュメントを片っ端から読んでいると、使ったことがない transformPreference(_:_:) を見つけた。

transformPreference(_:_:) | Apple Developer Documentation

「transform...? よく見たらanchorPreferenceにもtransformAnchorPreference(key:value:transform:)があるな。anchorePreferenceは元々transformクロージャを受け取るのにどういうことだ?」と思って手元で触ってみたらビンゴで、子要素から伝搬されたPreference値を加工してその親に渡すものみたい。加工ができるということは追加もできる。

struct ContentView: View {
  var body: some View {
    VStack {
      VStack {
        Text("Hello, world! 1")
          .preference(key: CustomPreferenceKey.self, value: ["child-1"])
        Text("Hello, world! 2")
          .preference(key: CustomPreferenceKey.self, value: ["child-2"])
      }
      .transformPreference(CustomPreferenceKey.self) { value in  // change
        value += ["parent-1"]
      }
    }
    .onPreferenceChange(CustomPreferenceKey.self) { value in
      print("received: \(value)")  // received: ["child-1", "child-2", "parent-1"]
    }
  }
}

子も自分(親)もその上位存在にPreference値を送りたい時は transformPreference(_:_:) を使おう。

transformPreference(_:_:)は重ねがけ可能

ドキュメントから予想はできるが実際に試して重ねがけができることを確認した。

struct ContentView: View {
  var body: some View {
    VStack {
      VStack {
        VStack {
          Text("Hello, world! 1")
            .preference(key: CustomPreferenceKey.self, value: ["child-1"])
          Text("Hello, world! 2")
            .preference(key: CustomPreferenceKey.self, value: ["child-2"])
        }
        .transformPreference(CustomPreferenceKey.self) { value in
          value += ["parent-1"]
        }
      }
      .transformPreference(CustomPreferenceKey.self) { value in
        value += ["grand-parent-1"]
      }
    }
    .onPreferenceChange(CustomPreferenceKey.self) { value in
      print("received: \(value)")  // received: ["child-1", "child-2", "parent-1", "grand-parent-1"]
    }
  }
}

子から transformPreference(_:_:) を使えば良いのでは?

これも予想できるが動く。

struct ContentView: View {
  var body: some View {
    VStack {
      VStack {
        VStack {
          Text("Hello, world! 1")
            .transformPreference(CustomPreferenceKey.self) { value in
              value += ["child-1"]
            }
          Text("Hello, world! 2")
            .transformPreference(CustomPreferenceKey.self) { value in
              value += ["child-2"]
            }
        }
        .transformPreference(CustomPreferenceKey.self) { value in
          value += ["parent-1"]
        }
      }
      .transformPreference(CustomPreferenceKey.self) { value in
        value += ["grand-parent-1"]
      }
    }
    .onPreferenceChange(CustomPreferenceKey.self) { value in
      print("received: \(value)")  // received: ["child-1", "child-2", "parent-1", "grand-parent-1"]
    }
  }
}

子も親も全ての階層で transformPreference(_:_:) を使えば良いのでは?と思ったが、preference(key:value:) はinputキーワードを持つクロージャが無いので、ただ値を送信するのみに徹するので素直に使い分けると良さそう。preference(key:value:) ができることは transformPreference(_:_:) もできる。

少し踏み込む

preference(key:value:) を親子に指定した時に親のPreference値だけその上位存在で受け取れるという点はSwiftUIのViewが内部で構築しているであろうView Treesの構成が絡んでいそう。View Treesにおいて親は子の後に評価されること、preference(key:value:) がその親に値を伝搬しないことを考えると子ではなく親のPreference値が優先される理由が予想できる気がする。「View Treesにおいて親は子の後に評価される」というのはSwiftUIのViewにおいて親のサイズは基本的に子のサイズによって決まるのでおそらくそうではないか、と予想している。