cockscomblog?

cockscomb on hatena blog

Swift ConcurrencyのwithTaskCancellationHandlerとSendable

Swift 5.5がリリースされた。おめでとうございます。

Swift 5.5の目玉はもちろんSwift Concurrencyだ。言語機能として並行処理がサポートされた。async/awaitの構文だけでなく、Structured Concurrencyとしての整理や、actorの導入など、野心的な取り組みと言える。

Swift Concurrency

Swift Concurrencyに直接関係するSwift Evolutionの提案はこれだけある。

これだけの提案が行われ、実装されたことに圧倒される。

The Swift Programming Language」にもConcurrencyの章が追加されている。こちらではわかりやすく説明されているので、概要を掴みやすい。

そろそろ本題。

タスクのキャンセル

Structured Concurrencyでは、親のタスクがキャンセルされると子のタスクもキャンセルされる。キャンセルされたかどうかはTask.isCancelledTask.checkCancellation()とかで確認できる。

withTaskCancellationHandler

タスクがキャンセルされたときに何か実行するにはwithTaskCancellationHandlerを使う。

func withTaskCancellationHandler<T>(operation: () async throws -> T, onCancel handler: @Sendable () -> Void) async rethrows -> T

onCancel@Sendableであることは重要なので、覚えておいてください。

コールバックを引数に取る関数からasync関数を作るwithUnsafeThrowingContinuationwithTaskCancellationHandlerを組み合わせる例が、SE-0300 Continuations for interfacing async tasks with synchronous codeのAdditional examplesにある。

import Foundation

func download(url: URL) async throws -> Data? {
    var urlSessionTask: URLSessionTask?

    return try await withTaskCancellationHandler {
        try await withUnsafeThrowingContinuation { continuation in
            urlSessionTask = URLSession.shared.dataTask(with: url) { data, _, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: data)
                }
            }
            urlSessionTask?.resume()
        }
    } onCancel: {
        urlSessionTask?.cancel()
    }
}

最新のAPIに合わせて少し書き換えているが、これは次のエラーでコンパイルできない。

Reference to captured var 'urlSessionTask' in concurrently-executing code

var urlSessionTask: URLSessionTask?varであるため、エラーが起きている。これはdiag::concurrent_access_of_local_captureというメッセージだった。

diag::concurrent_access_of_local_capture

エラーを出しているのはここ。

ちょっとコードを読むと、「flow-sensitive concurrent captures」なる機能についても言及されているが、今はフラグで無効になっている。

これはキャプチャした後に変更されないとわかっていればOKとするもので、今回のケースではいずれにせよコンパイルが通らないのが正しいように見える。

varじゃなくてletにして、参照型を間に挟めばコンパイルが通る。

import Foundation

class Wrapper<Wrapped> {
    var value: Wrapped
    init(_ value: Wrapped) { self.value = value }
}

func download(url: URL) async throws -> Data? {
    let urlSessionTask: Wrapper<URLSessionTask?> = Wrapper(nil)

    return try await withTaskCancellationHandler {
        try await withUnsafeThrowingContinuation { continuation in
            urlSessionTask.value = URLSession.shared.dataTask(with: url) { data, _, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: data)
                }
            }
            urlSessionTask.value?.resume()
        }
    } onCancel: {
        urlSessionTask.value?.cancel()
    }
}

一見よいように思うかもしれないが、var変数のキャプチャはNGでこれはOKなのって不思議じゃないですか?ということで、Other Swift Flags-Xfrontend -warn-concurrencyを設定する。

f:id:cockscomb:20210926102445p:plain
Other Swift Flags

-warn-concurrency

-warn-concurrencyってなんなんだという話だけど、これは互換性と関係している。Swift 5系では破壊的な変更を避けつつ、並行処理でデータ競合を避けるための道具を提供する。そしてSwift 6ではチェックを厳密にすることで、デフォルトでデータの競合について安全になる。-warn-concurrencyオプションをつけると、Swift 6で問題になる箇所を予め知るためのもの、ということらしい。

さて、-warn-concurrencyオプションをつけると、該当の箇所に次の警告が現れる。

Cannot use let 'urlSessionTask' with a non-sendable type 'Wrapper<URLSessionTask?>' from concurrently-executed code

onCancel@Sendableクロージャだが、そこからSendableではない値を参照していることで、警告が出た。class Wrapper<Wrapped>@unchecked Sendableにすれば警告は消えるが、Wrapperがちゃんとスレッドセーフなわけではないので、コンパイラを誤魔化しているだけの効果しかない。

Sendable

Swift 6でチェックされる項目の一つはSendableについてである。

Sendableの提案は受理されているが、現時点では実装済みになっていない。Swift 6でデフォルトでチェックされるようになるのを待っているのだと思うが、Swiftの標準ライブラリでは既に使われているし、自分たちで使うこともできる。

Sendableというのはactor間でやり取りできる型を表すマーカープロトコルである。マーカープロトコルはコンパイラに対する目印。

Sendableにできるかどうかは、例えば次のようになっている。

  • structのような値型は、Copy on Writeなので、actor間でやり取りしても競合が起きない。変更可能なプロパティがSendableであればSendableにできる
  • classは参照型なので、変更可能なプロパティを持っていないときだけSendableにできる。ただしfinalじゃないといけない
  • actorは参照型だけど内部状態をデータ競合から保護しているのでSendable

classだけど、内部的にデータ競合を防ぐ仕組み(ロックとか)を持っていることもある。そういうときは@unchecked Sendableに指定できる。これはコンパイラにチェックされないSendableである。

そしてクロージャ@Sendableアノテーションすると、クロージャもSendableになる。SendableなクロージャがキャプチャできるのはSendableだけである。

Swift Atomics

ここでとりあえずapple/swift-atomicsManagedAtomicLazyReferenceを使ってみる。要するにデータの競合が排除されていればいいので、今回の用途には大袈裟な感じもするが、使ってしまう。

import Foundation
import Atomics

func download(url: URL) async throws -> Data? {
    let urlSessionTask = ManagedAtomicLazyReference<URLSessionTask>()

    return try await withTaskCancellationHandler {
        try await withUnsafeThrowingContinuation { continuation in
            let task = urlSessionTask.storeIfNilThenLoad(URLSession.shared.dataTask(with: url) { data, _, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: data)
                }
            })
            task.resume()
        }
    } onCancel: {
        urlSessionTask.load()?.cancel()
    }
}

ManagedAtomicLazyReferenceは、遅延して初期化される参照型をメモリ管理しつつ保持してくれる。

Swift Atomicsの1.0.2時点ではManagedAtomicLazyReferenceはSendableではないが、次のバージョンではSendableになるはずだ。

次のようにしてこれを先取りする。

extension ManagedAtomicLazyReference: @unchecked Sendable where Instance: Sendable {}
extension URLSessionTask: @unchecked Sendable {}

勢いよくURLSessionTaskもSendableにしてしまっているが、スレッドセーフなはずなので大丈夫だと思う。

ここまでやって、ついに-warn-concurrencyでも警告が出なくなったはずだ。

実際にはresumeする前にもキャンセルのチェックをしないと、タイミングによってうまくキャンセルされないはずなので、チェックする。ここまでのコードをまとめると次のようになるはず。

import Foundation
import Atomics

extension ManagedAtomicLazyReference: @unchecked Sendable where Instance: Sendable {}
extension URLSessionTask: @unchecked Sendable {}

func download(url: URL) async throws -> Data? {
    let urlSessionTask = ManagedAtomicLazyReference<URLSessionTask>()

    return try await withTaskCancellationHandler {
        return try await withUnsafeThrowingContinuation { continuation in
            let task = urlSessionTask.storeIfNilThenLoad(URLSession.shared.dataTask(with: url) { data, _, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: data)
                }
            })
            if Task.isCancelled {
                continuation.resume(throwing: CancellationError())
                return
            }
            task.resume()
        }
    } onCancel: {
        urlSessionTask.load()?.cancel()
    }
}

いかがでしたか?

withTaskCancellationHandlerを題材にしてSwift Concurrencyの-warn-concurrencyを試してみたつもりですが、いかがでしたか?なんとなく大袈裟に感じたように思う。現実には、URLSessionはすでにSwift Concurrencyに対応したインターフェースを持っているので、こういうコードを書くことはない。とはいえ-warn-concurrencyすると、普通のアプリでもSendableに関連した警告がいくらでも出てくるのではないか。

Swiftは安全な言語を志向しているから、デフォルトでデータ競合からも安全になっていく。(これは別にSwiftに限ったことではない。)このことはポジティブに受け止めていいはずだし、Swift 6が楽しみである。

とはいえ現状では、-warn-concurrencyでやっていくのは少し難しい。スレッドセーフなclassでもSendableがついていなかったりするので、単純にやりにくい。Swift自体もConcurrencyのサポートを始めたところで、Swift 6までにまだよくなるだろうから、焦らなくてもいいはずだ。

特にアプリの開発では、たまに-warn-concurrencyを有効にしてみて、なるほど、くらいでいいような気がする。反対にライブラリの場合は、今のうちから-warn-concurrencyを設定しておくと、将来的な破壊的変更を避けられるかもしれない。

こちらからは以上です。

WWDC21大夢想

毎年この時期になると、毎日のようにWWDCのことを夢想している。

去年はSwiftUIのアップデートとApple Silicon搭載のMac、ホーム画面のウィジェットに期待していた。

去年の期待は、いろいろなことをうまく言い当てているようにも見えるし、少し過剰なところもあった。WWDC20では叶わなかったいくつかの部分については、引き続きWWDC21でも期待している。

ではWWDC21では何が発表されるのか。

Swift

2014年にSwiftが発表されてから7年になる。SwiftはOSSで開発されているので、次にどのようなアップデートがあるか、事前に窺い知ることができる。

swift-evolutionによると、次のバージョンはSwift 5.5となり、特に並行処理の言語的なサポートに注力されている。async/awaitの構文や並行処理の単位としてのTask、actorモデルの導入が決まっている。

OSSではありながらも、プロジェクトの大きな方向性はAppleが握っており、Appleプラットフォームと足並みを揃えている。裏を返せば、Swiftの開発状況からAppleプラットフォームの方向性を占えるということになる。

例えば基本的な部分でも、Concurrency Interoperability with Objective-Cによって、既存のAPIがSwiftの並行処理の仕組みで扱いやすくなるだろう。しかし何よりも、SwiftUIに期待が集まる。

SwiftUI

SwiftUIが今年も大きく強化されることは疑いようがない。

Swiftの並行処理とSwiftUIがどのように連携するのかは、大きなトピックだ。少なくともCombineフレームワークとの相互運用性が担保されるはずだ。例えばStructured concurrencyTaskとCombineのFutureは形が似ているし、あるいはCombineのPublisherAsyncSequenceはコンセプトが近いように感じられる。これらがうまく連携しないはずはない。

CombineとSwiftの並行処理が関連づけられるだけでも、最低限SwiftUIからSwiftの並行処理が扱える。しかしそれでは少しまどろっこしい。ObservableObjectにasyncメソッドを生やしたくなるし、それをTask.detatchすることなくViewから直接呼び出せたらとも思う。ViewbodyプロパティにEffectful Read-only Propertiesでasyncやthrowsをつけられるようになればおもしろい。おもしろいが、これはとりもなおさず副作用を持つということで、扱いが難しくなる懸念もある。

ところで、actorモデルがSwiftUIとの接点を持つのか計りかねている。プロセッサがメニーコアの時代を迎えたいま、複数のコアを効果的に活用することは至上命題であり、actorモデルは並行性の複雑さに対する強力な武器になる。Actorを導入すると、actorで分離されたデータを参照するような処理はすべてasyncになる。SwiftUIがこのような非同期的な処理をうまく扱えると、actorを使いやすいはずだ。

他にも、Extend Property Wrappers to Function and Closure Parametersでプロパティラッパが色々なところで使えるようになるが、これがSwiftUIの新しいAPIに利用される可能性もある。

当然ながら、新しい組み込みのViewも多く追加されるだろう。ナビゲーション周りがもう少し抽象化されると便利なように思う。テストを書くための仕組みも必要だ。

Swift Package Manager

Swift Package Managerもまた、Swift 5.5でしっかりと進化する。これはおそらくXcodeに影響を与える。Xcode 11からパッケージ管理にSwift Package Managerが利用できるようになっているが、引き続き連携が改良されることだろう。Xcodeがパッケージ管理の機能を強化していくことで、サードパーティのツールに依存せずに開発が行えるようになっていく。

要するに、Xcode for iPadに期待している。

パッケージ管理の強化に伴ってもう一つ期待したいのは、Appleが提供するオフィシャルなライブラリの導入である。Androidで言うJetpackのように、OSに含まれるフレームワークとは別のライブラリが導入されるとおもしろい。OSに含まれるライブラリはOSと同時にしか更新されないし、利用者がOSをアップデートしない限りは、アプリ側で新しい機能を使えない。裏を返すと、OSに内蔵できるフレームワークというのは、より保守的なものになる。しかしSwift Package Managerで導入できるライブラリはもっと自由度が高い。高い頻度で改善できるし、バージョンの古いOSに向けてバックポートすることもできる。Appleが高品質で多様なライブラリを提供することで、アプリのエコシステムが大きく改善されるはずだ。

データ

Core Dataは、SwiftUIから扱いやすいようなサポートが行われている。Environmentに.managedObjectContextが用意されているほか、@FetchRequestFetchedResultsを使って簡単にデータを取得できる。さらにNSManagedObjectObservableObjectになっていることで、データの変更に追従できる。

いっけんよさそうに思われるが、実際に使ってみると、少し使いにくい。例えばNSManagedObjectのプロパティは、プリミティブ型でない限り、デフォルト値が指定されていてもOptionalになってしまう。

Swiftから扱いやすいSwiftDataのようなものが登場するのを期待する声がある。Swiftの並行処理ともうまく連携されていたらさらに便利そうだ。これは別にCloudKitでもいいかもしれない。

OS

WWDCでは各OSのメジャーアップデートが発表されるのが常である。

iPadOS

今年一番の期待はiPadOSのアップデートだろう。Apple M1を搭載した高性能なiPad Proが発売され、その評判のほとんどは、ハードウェアの性能をソフトウェアが活かせていない、というものだ。当然そんなことはAppleだって百も承知のはずであるから、iPadOSは飛躍的な改善がなされるに違いない。そうでないと困る。

マルチタスキングに関する改善は、想像しやすい。自由にウインドウを配置できるようになるとは思いにくいが、それでもふたつ以上のアプリを自由に行き来しながら利用できるようになってほしい。また外部ディスプレイとポインティングデバイスを利用しているときに、外部ディスプレイをミラーリングではなく拡張されたデスクトップとして利用できるようになってほしい。

プロ向けのアプリは当然必要だ。Final Cut ProやLogic Pro、そしてXcodeiPadバージョンが発表されることに期待したい。Xcodeは例えばSwiftUIアプリだけを開発できるのでもよさそうだ。ところで、プロ向けのアプリが作りやすいように、UIKit側にも拡張があっていいはずだ。iPadOS 14でサイドバーや新しいピッカーが追加されたように、例えば複数のUIWindowをタブで扱えるようなUIとか、そういうものが増えてもいいと思う。

iOS

iOS 14ではホーム画面のウィジェットが一大トピックだった。ウィジェットiOSをより魅力的なものにしたと思う。ウィジェットの体験を演繹すると、いくつかの可能性が考えられる。

ひとつはインタラクティブウィジェットである。ウィジェットはその仕組みから、リンクを設定する以外にはインタラクティブな要素がない。これは余分なリソースを使わないための仕組みであるから、それが変わるとは思いにくいが、例えば事前に用意しておいた表示と行き来させるとか、そういった対応は可能なはずだ。

また別な発想で、アプリのアイコンをウィジェットと同じ仕組みで変えられるようにする、というのも考えられる。天気予報のアプリとかで有用だろうと思うし、特に技術的な制約があるとは思えない。

あるいはホーム画面にとどまらずに、ロックスクリーンの表示をカスタマイズできるようにする方向性もある。ロックスクリーンをうまく設定できるようになると、通知以外の方法で即時性の高い情報を得られるようになりそうだ。

加えて、iOS 5から搭載されているSiriも、ついに10歳だ。近年では音声アシスタントとしてだけでなく、様々なAI機能のブランドになってきているが、そろそろ大きなアップデートがあってもよさそうに思う。

macOS

昨年のmacOS 11でユーザーインターフェースがアップデートされ、またApple Silicon搭載のMaciOSアプリがそのまま実行できるようになった。macOS 12は、それと較べると規模の小さなアップデートになると噂されている。過去の例からすると、ユーザーインターフェースはもう少し調整されていくだろうから、macOS 12でも改善が期待できる。

iOSやiPadOSと較べれば、macOSは始めから生産性のためのOSである。新型コロナウイルスパンデミックですっかり様相が変わったこの世界では、生産性もまた再定義されつつある。このことからして、macOS 12では(もちろん他のプラットフォームでも)コラボレーションに関連する新しい機能がフィーチャーされるのではないかと予想する。特にメッセージアプリは、macOS 12でMac Catalyst製になったこともあって、iOSと合わせて改善しやすい状況になっており、わかりやすい例になると思う。

他にも、iOSやiPadOSからmacOSに持ち込まれるものがあるかもしれない。例えばショートカットアプリがmacOSにも導入されると便利だろうと思う。Automatorと競合するのがネックではある。あるいはTestFlightのmacOSバージョンがあってもよさそうだ。

watchOS

watchOSは、ヘルスケア関係の機能が強化されるであろうこと以外には、あまり想像が及ばない。今秋に発売されるであろうApple Watch Series 7でハードウェアの刷新があるとすれば、watchOSアプリもその影響を受けて、ユーザーインターフェースがリフレッシュされる可能性がある。

tvOS

tvOSはここ数年、比較的マイナーなアップデートにとどまっている。順当に考えれば、今年もそうなる可能性が高いように思われる。HomeKitに関連した部分では、今年ついに共通企画となるMatterが策定されており、対応する機器が出てくるであろうという情勢だ。tvOSは家庭内の機器のハブとしての役割が担わされており、なんらかのアップデートがあるかもしれない。

サービス

Appleはサービス面でも拡大を続けている。iCloudWWDC 2011で発表されたサービスで、こちらも今年10周年を迎えようとしている。WWDC21でもiCloudに新しい機能が加わる可能性はあるだろう。

あるいはまだ日本でサービスインしていない、Apple NewsやApple Fitnessについても、サービスする地域の拡大はもちろん、その強化が期待されるところである。ニュースとプラットフォーマーの関係は昨今でもホットなトピックである。信頼できるニュースが21世紀も生き残っていくためには、そのエコシステムが重要である。

ハードウェア

昨年のWWDC20でMacApple Siliconへ移行させていくことが発表されてから1年経った。その間にMacBook AirMacBook ProMac Mini、そしてiMacの一部が、Apple M1を搭載するようになった。サードパーティApple Siliconへの対応状況なども含めて、現況がしっかりと宣伝されるのは想像に難くない。

WWDC21ではその続報が望まれる。Apple M1XなのかApple M2なのか(Apple A14と同じ世代ならM1Xであろうと思われるが)わからないものの、より高性能な(TDPの大きな)Apple Siliconを搭載したMacがアナウンスされる可能性は高いだろう。何しろApple SiliconはAppleにとっての虎の子であるはずで、Macがこれほど注目されるタイミングは他にない。

ほか

Apple GlassesのようなXRプラットフォームは毎年期待している。そろそろ何か発表されてもよいようにも思うが、なにもわからない。

ということで

WWDCへの期待が溢れ出ている。

SwiftUIのDynamicPropertyを試す

SwiftUIにはDynamicPropertyというprotocolがある。

これを使ってみようという趣旨の記事を見かけた。

ので、私も試してみました。

@Now

import Combine
import SwiftUI

class Clock: ObservableObject {
    @Published private(set) var date: Date = Date()

    init() {
        Timer.publish(every: 1, on: .main, in: .default)
            .autoconnect()
            .assign(to: &$date)
    }
}

@propertyWrapper
struct Now: DynamicProperty {
    @StateObject private var clock = Clock()

    var wrappedValue: Date {
        get {
            clock.date
        }
    }
}

こういうのを作っておいて

import SwiftUI

struct ContentView: View {
    static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .none
        formatter.timeStyle = .long
        return formatter
    }()

    @Now private var date: Date

    var body: some View {
        Text(date, formatter: Self.dateFormatter)
    }
}

こう使う。

これでContentViewは毎秒更新され、現在時刻を表示し続ける。

DynamicPropertyって何

ドキュメントには、Viewが再計算される前にDynamicPropertyupdateメソッドを呼んでくれるくらいの情報しかないが、実際にはもうちょっといろいろあるようだ。

上記の例でもわかるように、@ObservedObjectの更新によってViewが再描画される。

ここで急に、SwiftUIのCore Dataサポートのように、@FetchRequest的な形で何かできるんじゃないか、と気づくと思う。

実際にRealmにはそういう機能があった。

現実のユースケース

DynamicPropertyでは、StateなりStateObject(またはObservedObject)なり、あるいはEnvironment(やEnvironmentObject)を、Viewのpropertyで使えるようだ。

これは少しReact Hooksに似ている。ReactのuseStateとSwiftUIの@Stateの対称性のように、ReactのCustom Hookとちょっと似ている。

とはいえ、@Nowの例は、単にObservableObjectをそのまま使えばいいわけで、独自のDynamicPropertyを作ることが正当化されるような場面は稀かも。

Relayに学ぶGraphQLのスキーマ設計

2018年の初めくらいから、仕事でGraphQL APIを何度も作っている。サーバーサイドもクライアントサイドも実装している。

最近クライアント側にRelayを使ってみている。

GraphQLのクライアントとしてはApolloを使う場合が多いと思うが、Facebook製のRelayもかなりよくできている。以前はTypeScriptに対応していなかったが、今はTypeScriptも使える。最近のバージョンではhooksのAPIがexperimentalではなくなり、ReactのSuspense API(Suspense for Data Fetchingは使わずに)と合わせて使える。

RelayはGraphQLのスキーマに制約を設けることで、クライアント側のAPIがデータの再取得やページネーションなどを抽象化している。換言すると、Relayからデータの再取得やページネーションに必要なスキーマ上の制約を学べる、ということだ。

ということで、スキーマの設計について抽象的な理解を得たので、それを記す。

RelayのGraphQL Server Specificationに倣う

GraphQL Clientとしてrelayを利用することもそうでないこともあるが、いずれにしても、これに倣っておくとうまくいくことが多い。

RelayのGraphQL Server Specificationは、再取得とページネーションのための仕様である。

Node

id という ID! 型のフィールドを持った Node インターフェースを定義する。

interface Node {
  id: ID!
}

ID はグローバルに(型に関わらず)ユニークな識別子。内部的には base64(type + ":" + internal_identifier) のような実装にすることが多い。クライアントサイドでは透過的な値として扱い、パースを試みるべきではない。

トップレベルのクエリに node(id: ID!): Node を持つ。Node インターフェースに準拠する型は、このトップレベルクエリから取得できるようにする。

type Query {
  node(id: ID!): Node
}

このようにしておくと、Node はクライアントから便利に扱える。キャッシュのノーマライズもできるし、再取得も簡単である。

Connection

Connectionはページングに関連している。要するに、ページネーションを行うフィールドで Connection というのを返したりすると、カーソルベースのページネーションができる。

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}
type User implements Node {
  id: ID!
}
type UserEdge {
  node: User!
  cursor: String!
}
type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
}
type Query {
  users(first: Int, after: String, last: Int, before: String): UserConnection!
}

組み合わせ

例えば Userfriends(first: Int!, after: String!): UserConnection! というページネーションできるフィールドがあるとき、2ページ目を取得するには User から指定しなければならない。

type User implements Node {
  id: ID!
  friends(first: Int!, after: String!): UserConnection!
}
query {
  node(id: "xxx") {
    ... on User {
      friends(first: 10, after: "yyy") {
        ...
      }
    }
  }
}

このようにネストしたフィールドでのページネーションでは、目的のフィールドまでのパスが一意に定まる必要がある。

Viewerパターン

サービスにもよるが、GraphQL APIを認証情報付きで呼び出している利用者を、type Viewer として表し、トップレベルから viewer フィールドで取得できるようにする。

type Viewer {
  name: String!
}

type Query {
  viewer: Viewer
}

このようなフィールドがあると、例えばログインしている利用者の名前を表示する際に便利なショートカットになる。従って利用者自身に紐づくリソースは Viewer に持たせるとよい。

Viewer 以外にも Visitor のような語彙でもよさそうではあるが、統一されている方が便利なので、特別な理由がなければ合わせるとよい。また、Relayではこのようなフィールドを Viewer に決め打ちして特別扱いしている。

取得可能性

type Query のフィールド、Node、そしてViewerパターンでは、いずれも目的のオブジェクトを直接取得可能である。使い分けは、グローバルにユニークなオブジェクトなら type Query のフィールド、そうでなければNode、そして現在の利用者を指すショートカットとしてViewerパターン、となるだろう。

Relayではもうひとつ、@fetchable ディレクティブと fetch__Xxx というトップレベルのクエリを組み合わせることができる。

directive @fetchable(field_name: String!) on OBJECT

type User @fetchable(field_name: "id") {
  id: ID
}

type Query {
  fetch__User(id: ID!): User
}

見ての通り、ほとんどNodeと同じである。

合わせて4つが、直接取得可能なフィールドということになる。Relay Compiler(Rustで書かれている)でも、これらが特別扱いされている。

GraphQLで扱うオブジェクトは、なるべく直接取得ができるように設計されていると、取り回しがよい。例えばパーマリンクとして扱うには直接取得が可能でなければならないだろう。

Mutationの戻り値

Mutationでオブジェクトを作成、更新、もしくは削除したときの戻り値をどうするか。

更新

GraphQLクライアントは普通、内部にノーマライズされたキャッシュを保持している。

例えばTwitterを作るとして、ツイート一覧画面について考える。一覧でツイートを選択して、個別のツイートの画面で「いいね」する。その後ツイート一覧に戻ってきたとき、一覧の中でも該当のツイートが「いいね」状態になっていてほしい。

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type Tweet implements Node {
  id: ID!
  text: String!
  likeCount: Int!
  viewerLiked: Boolean
}

type TweetEdge {
  node: Tweet!
  cursor: String!
}
type TweetConnection {
  edges: [TweetEdge!]!
  pageInfo: PageInfo!
}

type Query {
  node(id: ID!): Node
  timeline(first: Int!, after: String): TweetConnection!
}

このとき、TweetNode なので、GraphQLクライアントは id の値をキーとしてキャッシュをノーマライズする。Mutationとして like(tweetID: ID!): Tweet! が用意されていれば、これの戻り値を使ってキャッシュを更新できるため、「いいね」状態の一貫性が維持される。

要するに、特に Node の更新については、戻り値は Node そのものであればよい。

作成

作成されたオブジェクトが一覧にも表示される場合、取得されたConnectionのキャッシュにも要素を付け加える必要がある。

大まかには、戻り値が Node であればよい。RelayのConnectionを利用している場合、戻り値をEdgeにしてもよい。戻り値を使ってConnectionのキャッシュを書き換える必要がある。

Relayの場合、@appendEdge / @prependEdge もしくは @appendNode / @prependNode といったディレクティブが用意されており、Connectionの前後に要素を付け加えるのが少し簡単になっている。

削除

オブジェクトを削除する場合も、やはり一覧での表示から取り除く必要がある。

Connectionから取り除くには、Nodeid さえわかっていればいい。従って戻り値は ID! で十分である。

Relayでは @deleteEdge ディレクティブが用意されている。

まとめると

GraphQLのスキーマを設計するとき、とりあえず従っておくべき指針をRelayから得た。

まずはRelayのGraphQL Server Specificationに従っておく。NodeとConnectionを実装する。

そして直接的に取得可能な、type Query のフィールドやNode、Viewerパターンを意識する。

Mutationの戻り値は、作成や更新ならNode、削除はIDが基本になる。

例外はいくらでもあると思うが、最初はこれくらいから考えると概ね妥当と思う。

Work From Living Roomで使うマイク

プロローグ

西暦2021年、COVID-19のパンデミックによって人類が活動の抑制を余儀なくされてからおよそ1年が経っていた。一部のデスクワーカーは在宅勤務、「Work From Home(WFH)」にシフトすることで、他者との物理的な接触を避けながら職務を継続した。ビデオ会議が対面でのミーティングにとって代わり、人々はこぞってカメラやマイク、照明を買い漁った。


……というプロローグでやっているわけだが、人によって住環境も様々で、Work From HomeというかWork From BedroomとかWork From Living roomのような状況の人も多いと思う。かくいう私も部屋数の少ない賃貸住宅で暮らしており、まさにWFLの様相を呈している。

リビングの端で窓を背に陣取っていて、それなりに快適にやっているのだけど、ビデオ会議のときに生活音が入ってしまうのが気になっていた。特に子供がうるさくする音は、会議に同席する同僚たちは寛容にも気にしないでいてくれるのだけど、それでも居心地はよくない。そのうち引っ越したら仕事部屋を用意しよう、くらいにのんびり構えていたが、最近になって急に、指向性のあるマイクが気になり始めた。

ということでマイクについて調べてみました。

指向性と構造

指向性については、audio-technicaマイクロホンの指向特性のページがわかりやすい。なるほど、シンプルだけど存外に物理的な仕組みである。

ついでにaudio-technicaマイクロホンの内部構造のページを見ると、「ダイナミック型」や「コンデンサ型」のような駆動方式があり、空気の振動をどうやって電気信号に変換するかというものだが、方式によって感度などに差が出るということだった。

これまで、なんとなくコンデンサ型の方が音質がいいような印象を持っていたが、実際には用途に合わせるとよいようである。生活音を入れたくないならダイナミック型の方が合っているのかも。

何を買ったらいいのか

なんとなく理屈はわかってきたが、パソコンに容易に接続可能なダイナミック型のマイクはあまり多くない。Blue Yetiのような製品はコンデンサ型だった。オーディオインターフェースから揃えるとなると面倒。

というところでいろいろ教えてもらって、ゲーミング用のヘッドセットもいいと聞いたので心が揺れた。しかし昨年末に AirPods Maxを買ってしまっていて、ついでにPlayStation 5用のPULSE 3D ワイヤレスヘッドセットも持っているので、頭に着けるものがこれ以上増えることについて、妻がいい顔をしなかった。

結局どうしたかというとShure MV7を買いました。これは2020年11月に発売されたもので、なんとなくよさそうな雰囲気があるが、あまりわかっていない。とにかくUSB接続できて、ダイナミック型で、カーディオイド型の単一指向性がある。あと見た目がいい。

試しに買ってみるにはちょっとpriceyではあるが、しかしマイクの良し悪しについて、まして生活音をどれくらい拾わないかなんて、事前にわかるはずもない。だから決め手は id:nagayama さんが使っているという事実である。nagayamaさんが使っているなら間違いなくないですか。

ということでnagayamaさんが使っていることをもって妻も納得し、購入に至った。口から近い位置に設置できた方がいいので、適当な安いアームも買った。ディスプレイのUSBポートに接続したので、Macをディスプレイに繋げばマイクも使える状態になった。

2週間ほど使ってみたところ、同僚からの評判はいい。いい声になったとさえ言われる。録音して試してみた感じでは、同じ部屋で子供が騒がしくしていてもかなり小さな音量になっている。Discordのようにノイズキャンセリング処理のしっかりしたツールならほとんど伝わることがない。これが他の製品よりよい結果なのかどうかはわからない、かなり満足した。

f:id:cockscomb:20210228095551j:plain
ディスプレイを介して接続されたShure MV7


エピローグ

パンデミックを機に、新しい暮らし方、働き方を模索する動きも多い。私の勤務先でも「フレキシブルワークスタイル制度」というのが導入されている*1。マイクを買ったことで、ビデオ会議への心理的な負担が小さくなったのはもちろん、いいマイクを持っているという不思議な充足感が得られた。

気をよくして、3歳の息子とPodcastごっこをしてみたが、ハマっているゲームについて話すばかり。10年後くらいに聞き返したい。

ちょっとスクリプトを書くくらいの気持ちで作るSwiftUIアプリ

12年前くらいからiOS向けのアプリを作ってきた。最初は学生の個人開発、途中から仕事、そして最近は(仕事ではあまりやらなくなったので)趣味的にやっている。UIKitで、はじめの頃はUITableViewが難関だった。毎年のアップデートでUIKitはどんどん拡充されて、Objective-CはMRCからARCへ、そしてSwiftも出た。

毎年の変化を差分で学んできて、振り返ってみると、当初のそれからは大きく変わっていて、便利なんだけど、とにかく膨大だ。

SwiftUIの登場

というところで、2019年にSwiftUIが出た。SwiftUIを使うと、宣言的にユーザーインターフェースを構築できる。UIKitでできること全てをSwiftUIで実現できるわけではないが、それでも2020年のアップデートでかなりカバー範囲が拡がった。

それで、SwiftUIでちょっと何か作ったりしている。例えばメニューバーに目玉を表示するやつなんかもそうだ。これはとても楽しい。

何かをリスト表示したいとき、SwiftUIならListを使って数行で書ける。UITableViewDataSourceを使っていたのと較べれば雲泥の差だ。とはいえ最近ではUIKitも進歩していて、そもそもリスト表示でもUICollectionViewを使うのが推奨されているし、iOS 13やiOS 14で追加された機能を使うと、以前とは別次元の使いやすさになる。

とにかくそういうわけで、SwiftUIでアプリを作るのは楽しい。Xcodeを開いて、適当なテンプレートを選んで、小一時間プログラミングすると、ちょっとくらいのものができる。SwiftUIなら、ちょっとスクリプトを書くくらいの気持ちGUIアプリを作れる。子供にアプリを作るのも、そういう感覚があったからできたことだ。

SwiftUIのこういう気安さは、極めて本質的なことだと思う。プロトタイプを開発するのにもいいが、自分だけにしか需要がないアプリを作るのにもいい。堅苦しく考えれば、ソフトウェア開発の民主化である。多くの人が、自分のほしいソフトウェアを自分で作れるようになったら、こんなにすごいことはない。自分というドメインのエキスパートは、自分自身に他ならない。

SwiftUIを学ぼう

f:id:cockscomb:20210214163704p:plain

ここまで書けば、SwiftUIについて学びたくなった人がほとんどだと思う。そういう人はまずSwiftUI Tutorialsをやってみるのもいいだろう。あとはとにかくドキュメントを読み込んだり、WWDCのビデオを全部みれば、だいたいのことはわかります。

ほかに信頼できる最新のリソースはないかな〜、というところで、来週(2021年2月22日)発売のWEB+DB PRESS Vol. 121で、同僚のid:yutailang0119id:kouki_danらとともに「iOS 14最前線」という特集を執筆しました。この特集では、SwiftUIの基本的な要素がなめるように紹介しているほか、使いやすくなっているUICollectionViewの最新情報や、iPadOSに最適化したりウィジェットを開発したりするための知識を詰め込んでいます。これはお得!どうぞお買い求めください。

WEB+DB PRESS Vol.121

WEB+DB PRESS Vol.121

  • 発売日: 2021/02/22
  • メディア: Kindle

メニューバーに目玉を表示するやつ

なんでかわからないけど、昔はデスクトップに目玉を表示する風習があったと思う。正月に突然そのことを思い出して、「macOS Eyball」とかでググったりした。最近は流行ってないらしい。

マウスカーソルを追いかける目玉を作るには、目玉の座標とマウスカーソルの座標を使って何か計算してやればいい。なつかしの三角関数っていうやつだ。どれどれと思ってちょっとコードを書いてみる。

元日にこんなことしているのもおかしいけど、子供の世話をしつつ数時間やったら、それなりに形になった。近頃はSwiftUIっていうやつがあるから、こういうのを作るのも気が楽。

ノスタルジックな気持ちになりたい人もいるかと思って、Mac App Storeで販売することにしました。みなさまの暖かいご支援に感謝いたします。もし利益が出たら次回作への意欲に変換します。

Eyeballs at Menu Bar

Eyeballs at Menu Bar

  • 尋樹 加藤
  • ユーティリティ
  • ¥120