cockscomblog?

cockscomb on hatena blog

SwiftUIにおけるActionパターン

SwiftUIはWWDC20で大きく更新された。アプリ全体をSwiftUIで作る方法が確立されたほか、新しい抽象がいくつも導入されている。どれも非常に興味深いが、本記事ではその中からActionパターンを見出し、紹介する。

Actionの導入

WWDC20ではSwiftUIに、以下の4つの「Action」が導入された。

また対応するプロパティがEnvironmentValuesに追加されている。

ドキュメントに使い方が載っている。

struct SupportView : View {
    @Environment(\.openURL) var openURL

    var body: some View {
        Button(action: contactSupport) {
            Text("Email Support")
            Image(systemName: "envelope.open")
        }
    }

    func contactSupport() {
        openURL(mailToSupport)
    }
}

@Environment(\.openURL) var openURLとしてEnvironmentから取得しているのが、OpenURLAction構造体の値である。これをcontactSupport()関数の中で、openURL(mailToSupport)という風に呼び出している。

構造体の値を関数のように呼び出しているが、これは「SE-253 Callable values of user-defined nominal types」で議論され、Swift 5.2で導入された機能だ。このような値をcallable valueという。callAsFunctionメソッドを持っている構造体の値やクラスのインスタンスは、関数のように呼び出すことができる。関数のシグネチャcallAsFunctionメソッドのシグネチャと一致する。またcallAsFunctionメソッドはオーバーロードできる。

Actionという抽象

4つのActionが、「Action」と名付けられていることには意味がある。Actionの特徴は、以下のようになる。

  • Actionの名前はAction接尾語を持つ(XxxActionのように)
  • Actionの名前は動詞と目的語で構成される
  • Actionはcallable valueである(callAsFunctionメソッドを持っている)
  • ActionはEnvironment経由で取得できる
  • Actionを表すEnvironmentValuesのプロパティはActionの名前からAction接尾語を除いたものになる(OpenURLActionopenURLになる)
  • (おそらく)Actionは値型である

最後だけは「おそらく」と書いたが、Actionを参照型で作って状態を共有するようなケースはおおよそ考えられないし、またそうあるべきではないと思われるから、値型(つまり構造体など)で作るのが普通だろう。

これらの特徴を備えたActionは必然的に、コンテキストの親(祖先)が提供する機能を、子(子孫)から呼び出す、というユースケースになる。

独自のActionを作る

Actionとして抽象化できそうな処理があれば、独自のActionを作ることができる。

Actionとなる構造体を作るのは簡単だ。

struct PrintStringAction {
    func callAsFunction(_ string: String) {
        print(string)
    }
}

これをEnvironmentに入れるには、以下のようにKeyを作って、EnvironmentValuesを拡張する。

struct PrintStringActionKey: EnvironmentKey {
    static var defaultValue: PrintStringAction {
        return PrintStringAction()
    }
}

extension EnvironmentValues {
    var printString: PrintStringAction {
        get {
            return self[PrintStringActionKey.self]
        }
        set {
            self[PrintStringActionKey.self] = newValue
        }
    }
}

これだけで、@Environment(\.printString) var printStringとして利用できる。

(この例はActionとして抽象化するのが適切ではない……。)

SwiftUIにおけるAction

「Action」という語は、伝統的なCocoaにおいてはTarget-Actionパターンに登場し、さらにUIKitにはUIActionが登場している。あるいはFluxでも使われる。このように、文脈に依存して意味が少しずつ異なるが、SwiftUIではここまで説明したようなものが「Action」パターンである。

ということで、SwiftUIにおけるActionパターンを見出した。パターンというのは、再利用可能な設計のプラクティスである。

Actionパターンは、SwiftUIにおいて必要不可欠な抽象化になっていくだろう。

WWDC20ファーストインプレッション

WWDC20Keynoteをみて、ドキュメントを乱読し、Platforms State of the Unionをみて、ちょっと寝て、仕事をしている。今はめちゃくちゃ眠たい。仕事休んだらよかった。

予想の振り返り

一昨日、ちょっとふざけた感じで、各メディアの予想と筆者の考えをまとめて公開した。

今年はiOS 14の早期ビルドがリークしていたと言われている。そのせいか、各メディアの予想はかなり網羅的だったと言える。筆者の予想も全体的にはいい線だったと思う。

「SwiftUI 2」と書いた内容は、ウインドウをSwiftUIで作れるとか、穏当に実現している。SwiftUIのパラダイムにはあまり大きな変化がないが、それでも注目すべきことがあるので、いずれ紹介したい。

「ARM Mac」は、各メディアの予想と一致している部分が大きい。2021年ではなく2020年末に最初の製品が出荷されるであろうことは朗報だ。また移行キットも提供される。「iPad Proをその目的で使えるとおもしろい」と書いたが、移行キットにA12Zが搭載されるので、当たらずとも遠からずというところか。

「新しい可能性」と称して、ホーム画面のウィジェットに言及しているが、これも各メディアの予想通りである。「実装はApp Extensionになるだろうけど、全体的なアーキテクチャApple Watchに近い可能性」と書いているが、実際、Apple Watchのコンプリケーションに似たAPIだと思う。ついでにコンプリケーションもウィジェットも、SwiftUIに寄っていった。

macOS 11

f:id:cockscomb:20200623140835j:plain
macOS Big Sur

一番の驚きは、次世代のmacOSとなるBig Surが、ついにmacOS 11にナンバリングされ、ビジュアルデザインが刷新されたことだ。知る限り、ビジュアルデザインの変更はどのメディアでも予想していなかったし、自分でもまったく予測不可能だった。

思えばmacOSのタイトルバーは、この何年かのアップデートで、徐々に形を変えていた。iOSアプリがアプリ・セントリックな(つまりドキュメント・セントリックではない)パラダイムを採用する中、macOSにおいてもシングル・ウインドウなアプリケーションがマジョリティになっていった。結果として、ウインドウのタイトルバーの意味が相対化されたのだろう。

こうしてBig Surでは、ついにタイトルバーのないウインドウが普通になった。タイトルバーのあった場所を、まるでUIKitのナビゲーションバーのようなツールバーが占めている。このようにすることで、ウインドウはまるでiOS/iPadOSのアプリのようになる。だからCatalystmacOSに移植されたアプリも、一級市民となった。

Dockに並ぶアプリのアイコンも、iOS/iPadOSのアプリに合わせるように角丸になった。しかしそれでも何かを主張するように、iOSアプリとは違うディテールや立体感を与えられている。マルチウインドウが前提のGUIシステムでは、奥行き表現の重要性が相対的に高い。macOSのこの奥行きが、少なくとももうしばらくの間、macOSmacOSに留めるのかもしれない。

ファーストインプレッション

最高。

WWDC20で何が発表される?予想をまとめてみました!

2020年のWWDCは、新型コロナウイルスによる感染症拡大のため、ほかの様々なイベントと同様に開催形式が変わる。現地時間の22日(日本時間では23日)、KeynoteとPlatforms States of the Unionが行われ、その後に様々なセッションビデオが公開される予定だ。

WWDC20

期待

Appleプラットフォームの開発者としては、WWDCは一年で最大の楽しみである。昨年はCatalystやSwiftUIが発表され、たいへん盛り上がった。今年は何が発表されるのか、否が応でも期待が高まる。

SwiftUI 2

今年はとうぜん、SwiftUIの大幅なアップデートが出てくるだろう。画面いっぱいに「SwiftUI 2」の文字が大写しになるのが待ち遠しい。SwiftUIでアプリを作ろうとするとすぐに気づくのだが、SwiftUIはまだ機能不足である。

WKWebViewをラップしたWebViewを作るとする。ブラウザの「戻る」機能を作るために、WebViewが戻れるのかを知りたい。これにはWKWebViewのcanGoBackプロパティを使えばいいはず。実際に戻るにはgoBack(_:)メソッドを呼び出す。こういうのが、SwiftUIでは素直に実装できない。Reactであれば、Refという仕組みがあって、実際に作成されたコンポーネントへの参照が得られるが、SwiftUIにはない。TextFieldのイニシャライザのように、onXxxというコールバックを渡すとか、なんらかのシグナルをPublisherとして渡すとか、そういうことになってしまう。これは面倒なので、このようにViewが内包する状態を参照したり、あるいはViewに何かシグナルを送ったり、そういう機能が追加されると嬉しい。

SwiftUIがカバーする範囲がもっと拡がる必要もある。macOSのアプリをSwiftUIで作った経験から言えば、例えばウインドウそのものをSwiftUIで作れるとか、ウインドウのツールバーとか、そういう部分もカバーされていてほしい。ただし、現状のCocoaにはResponder-chainを活用した部分が多く、SwiftUIでこれを解決するのは骨が折れるだろう。

なんにしても、SwiftUIが非常におもしろい、未来の約束されたフレームワークであることは疑いようがない。宣言的なUIフレームワークであることはもちろん、実際のプレゼンテーションと、コード上の表現が意図的に分離されているところに、その本質がある。コード上では同じButtonでも、macOSiOS/iPadOS、watchOS、tvOSで、それぞれ見かけが異なる。このことは、プラットフォーム毎に最適化されたUIを提供する上で都合がいい。あるいはこの先の、まったく新しいプラットフォーム(もちろんARグラスだ)においても、SwiftUIは役に立つことだろう。

そしてもう一つ、ポストInterface Builderの時代の到来だ。Interface Builderは、今やXcodeの一機能であるが、もともとは独立したアプリケーションとして、Project Builder(Xcodeの前身)を補完するものだった。最近はあまり聞かれないが、Interface BuilderはRADツールと呼ばれていた。SwiftUIの時代では、SwiftUIのコード自体がRADツールであろう。SwiftによるDSLめいた記述でUIを宣言できる。そしてこれは、例えばXcode for iPadのような開発環境において、UI開発の本流となるだろう。

ARM Mac

iPad Proが、「ほとんどのノートパソコンより高速」であると宣伝されるようになってしばらく経つ。少なくともベンチマークの結果からは、この文句に嘘はない。

Macに搭載されると噂されているApple A14ベースの独自のプロセッサは、12コアで、既存のMacBook Airに搭載されているIntelのプロセッサよりも高性能とされる。A14プロセッサはTSMCの5 nmプロセスで製造されると言われている。現在のIntelは、14 nmもしくは10 nmプロセスでプロセッサを作っており、A13までの7 nmプロセスにも到達していない。IntelとARMを単純には比較できないとはいえ、プロセスルールが半分であれば、Intelプロセッサよりも高い性能を得られるというのにも信憑性がある。

開発者からみたとき、ARMへの移行はどれくらい大変なのか。かつてPowerPCからIntelへの移行時に行われたように、Rosettaのような互換レイヤーが提供される可能性もある。そうでなくても、単純なソフトウェアであれば、ソースコードから再コンパイルするくらいで対応できることが多いだろう。バイナリを直接扱っている場合、バイトオーダーやアラインメントによっては対応が必要になるかもしれない。少し厄介なのは、コンパイル済みのSDKやライブラリを使っている場合で、それらの対応を待つ必要がある。最も困難なのは、細かなチューニングが必要なアプリケーションだろう。

消費者向けにARMのMac端末が発売されるのは2021年という噂である。しかし開発者向けには、それに先立ってARM Macの移行キットが提供される可能性もある。PowerPCからIntelへの移行時には、移行キットがリースで提供された。あるいはiPad Proをその目的で使えるとおもしろいが、飛躍しすぎだろうとも思う。

新しい可能性

iOS 14は、前年の失敗から、安定性とパフォーマンスを重視したリリースになると噂される。それでも戦略上、新機能がまったくないということにはならない。

iOSは、その当初からサードパーティに対しては大きな制約の中での開発を強いている。それはセキュリティのためであったり、あるいは端末のリソースを奪い合わないように、という目的がある。反面で、技術的に解決が可能になり、かつそれが必要だと認められれば、新しいAPIが開放されてきた。例えば初期の頃の、アプリのバックグラウンドでの動作を制限する代わりに、Push Notification Serviceを提供する、というのがそれだ。

iOS 14でも、いくらかのAPIが解放されることが期待される。その一つが、ホーム画面のウィジェットだ。iOSにはこれまでにもToday Widgetがあるが、すごく便利かというと、そうでもないと思う。そもそもiOSのホーム画面は、本格的なオーバーホールが必要な時期をとっくに過ぎている。初めてiPhoneが出た頃、私たちが1日に使うアプリの数はたかが知れていた。しかし近年、何もかもがスマートフォンで行える時代においては話が違う。ホーム画面にウィジェットを並べることができたら、Apple Watchのコンプリケーションのように、そのとき必要な情報を一望できるかもしれない。実装はApp Extensionになるだろうけど、全体的なアーキテクチャApple Watchに近い可能性もある。

もう一つ、デフォルトのアプリを変えられるようになるという噂がある。ChromeSafariの代わりにデフォルトにできたら、喜ぶ人も多いだろう。ついでにChromiumのようなものも許可してくれると嬉しい。Xcode for iPadの噂も含め、特にiPadOSをデスクトップクラスにしようという流れがあると思う。当然、現在の制約を大きく緩めるときが来ていると思う。

付録:メディアの予想

Appleに関する様々な情報を扱うメディアのうちいくつかが、WWDCで発表される可能性のある内容を予想している。予想と言っても、リーク情報をもとにしているものもあり、精度にはグラデーションがある。

以下の記事を参照して、大まかに一覧にした。

iOS 14

よりカスタマイズ可能なホーム画面

  • リスト表示オプション
    • ソート
      • 最近使った順
      • 未読の通知を優先
    • Siriの提案
  • ホーム画面ウィジェットAvocado

壁紙

  • デフォルトの壁紙がコレクションに分類される
  • ホーム画面では壁紙を単色に、あるいはブラー、あるいは暗くする
  • サードパーティが壁紙のコレクションを提供でき、それがiOSの設定に統合される

サードパーティのアプリをデフォルトに設定する機能

  • Webブラウザやメール、ミュージックプレーヤー
  • ChromeGmailのようなアプリをSafariやMailに代わってデフォルトに設定できる

AR

  • 新しいARアプリケーション(Gobi
    • 周囲の情報をARで得る

Siri

Messages

  • 送信済みのメッセージを取り消す
  • @でメンション
  • グループメッセージでメンションのみを通知する
  • グループメッセージでの入力中表示
  • メッセージを未読にする

Safari

  • 組み込みの翻訳機能で、サードパーティのアプリやサービスなしに、Webページを翻訳する
    • 翻訳機能はApp Storeなどにも拡がる
  • 翻訳はNewral Engineによってローカルで実行される

地図

  • Apple Storeとの連携
    • 地図アプリからGenius BarやTrade Inのサービスを確認できる
    • Appleの認定サービスプロバイダでも同様
  • カップルシートや子ども割引、プライベートルームを持つ施設のハイライト
  • IMAX上映を行う映画館のフィーチャー

探す

  • 誰かがスケジュールされた時刻に特定の場所に到着しなかったことの通知
    • 例えば、子どもが学校に到着しなかった、あるいはパートナーが職場に到着しなかった
  • 合わせて、設定された時刻よりも早く出発したという通知
  • ARを使って視覚的に見つけられる

Podcasts

  • Apple MusicのFor Youタブのような機能
  • Podcastの製作者が視聴者にボーナスコンテンツを提供できる

Clips

  • QRコードを読み取ったときに、ネイティブのカードUIを表示する
    • インストールされていないアプリでも、Over-The-Airパッケージとしてアプリの一部をダウンロードする

Keychain

  • 同一のパスワードを複数のサイトで使い回していることに対する警告
  • 二要素認証に使われるトークンの保存

CarPlay

  • CarPlayの壁紙のカスタマイズ

CarKey

HomeKit

  • 電灯のためのNight Shift
    • HomeKitが時刻に合わせて自動的に電灯の色温度を調節する
  • HomeKitのセキュアビデオ機能に顔の識別機能

アクセシビリティの強化

  • 聴覚を失った人のために、火災警報やサイレン、ドアのノック、ドアベル、あるいは赤ちゃんの泣き声のような、重要な音声を検知して、触覚フィードバックに変換する

パフォーマンスと安定性

そのほか

  • 新しいスタンドアローンのフィットネスアプリ(Seymour
  • AnimojiやMemojiに関する何か
  • #shotoniphoneチャレンジが写真アプリに統合される
  • Apple PayのAlipayサポート

iPadOS 14

Apple Pencil

  • 入力フィールドに手書きで入力し、テキストに変換する

Safari

  • Webサイト上でApple Pencilの入力を完全にサポート

Xcode for iPad

Final Cut Pro X for iPad

watchOS 7

文字盤

  • 文字盤の設定の共有
    • iMessageやAirDrop、そのほかの方法で共有する
  • Infographシリーズの新しい文字盤、Infograph Pro
    • タキメーターを特徴とする
  • Internationalウォッチフェイス
  • 写真の文字盤で共有アルバムをソースにする

子ども向けのApple Watch

  • 保護者のiPhoneでセットアップ/管理する仕組み
  • SchoolTime
    • 学校にいる時間に使えるアプリやコンプリケーションを管理する
  • アクティビティのカロリー消費を、動いていた時間に

睡眠トラッキング

  • 新しいSleepアプリで睡眠をトラッキングする
  • iPhoneのHealthアプリから睡眠の目標を設定
    • 睡眠時間や質を向上させるための助言も含まれる
  • コントロールセンターに睡眠モード
  • 新しいハードウェアが必要かもしれない

血中酸素飽和度

  • 血中酸素飽和度の測定
  • 新しいハードウェアが必要かもしれない

そのほか

  • アプリのアーキテクチャが、iPhoneのエクステンションベースではなくなる
  • フィットネスアプリ
  • ECG機能のアップグレード

macOS 10.16

Messages

  • 新しいMessagesアプリ
    • iOS/iPadOSからCatalystアプリとして持ち込まれる

Shortcuts

  • macOSでもShortcutsが使える

tvOS 14

Kids Mode

  • 子ども用のアカウントを作成
    • 使用できるアプリを管理

Screen Time

  • Screen TimeがtvOSにも拡張される

そのほか

  • フィットネスアプリ
  • コンテンツによりフォーカスしたApple TV+の再デザイン
  • Apple TVのオーディオの出力先としてHomePodのステレオペアをデフォルトに設定できる

HomePod

ハードウェア

再デザインされたiMac

  • これまでより細いベゼル
    • iPad Proのようなデザイン
  • 21.5インチではなく23インチに
  • ラインナップがすべてSSD
    • Fusion Driveは廃止
  • 第10世代IntelCoreプロセッサ(Comet Lake)が採用
  • AMDのNaviアーキテクチャGPU
  • Apple T2セキュリティチップがiMacとして初めて搭載

Apple TV

  • 新しいApple Remote
  • Apple A12X Bionicチップ
  • HDMI 2.1
  • 64 GBもしくは128 GBのストレージ
    • 現行モデルの2倍

AirPods Studio

  • オーバーイヤーヘッドフォン
  • 頭部や首への装着を識別
  • イコライザのカスタマイズ
  • アクティブノイズキャンセリング
  • ハイエンドのプレミアムバージョンとフィットネスにフォーカスした軽量のバージョンがあるかもしれない
  • 磁力によってパッドやヘッドバンドを交換できるかもしれない

ワイヤレス充電マット

  • 小さなワイヤレス充電マット

HomePod

  • 現行モデルの半分程度の大きさ

AirTag

  • 財布やバッグ、鍵に取り付けるトラッカー
  • 探すアプリからトラックできる
  • 空間認識に対応した超広帯域チップの技術を含むかもしれない

ARM Macへの移行

  • MacのラインナップをARMプロセッサへ移行する計画
  • A14をベースにする
    • 12コアのカスタムARMプロセッサ
      • 8つの高性能コアと4つの高効率コア
    • Intelのプロセッサを用いる現行のMacBook Airより著しく強力
    • GPUの性能やAIに関する演算性能も注目に値する向上が起きる
  • コンシューマ向けのARMチップが搭載されたMacハードウェアはまだ発表されない
    • 発売は2021年からになる
  • 開発者向けに移行のロードマップが示される
  • 何らかの開発用移行キットのようなハードウェアが提供されるかもしれない
    • PowerPCからIntelへの移行の際にはApple Developer Transition Kitがリースされた

Swift AWS Lambda Runtimeを試す

2020年5月末に、 SwiftをAWS Lambdaで動作させるプロジェクトが発表された。swift-server/swift-aws-lambda-runtimeがそれである。ということで、AWS CDKでAPI GatewayとSwiftのLambda Handlerを作ってみた。

Lambda Runtime

AWS Lambdaは、AWSのFaaS。提供されているランタイムを使えば、ソースコードをアップロードするだけで、関数が実行できる。提供されているランタイムはNode.jsやPythonRubyJava、Go、.NET Coreである(徐々に拡充されていった)。そして2018年のre:Inventで、Lambda LayerとLambda Runtime APIが発表され、swift-aws-lambda-runtimeでは、このRuntime APIを利用している。

Lambda Runtime APIを乱暴に説明すると、こうだ。bootstrapという実行ファイルを用意しておくと、自動的にこれを起動してくれる。bootstrapは内部でイベントループを回す。イベントループの内部では、HTTPでイベントを取得し、それを処理して、結果をHTTPで送る、というのを繰り返す。

swift-aws-lambda-runtimeは、イベントループを回して、イベントを取得して結果を返す、というところをやってくれる。

Lambda

f:id:cockscomb:20200615001416p:plain
AWS Lambda

swift-aws-lambda-runtimeの主要な開発者であるFabian Fettさんのチュートリアルを参考に進める。

Getting started with Swift on AWS Lambda

まずはSwift Package Managerでpackageを作る。

$ swift package init --type executable

API Gatewayを使いたいので、Package.swiftAWSLambdaEventsの依存も追加。

import PackageDescription

let package = Package(
    name: "Handler",
    platforms: [
        .macOS(.v10_13),
    ],
    products: [
        .executable(name: "Handler", targets: ["Handler"]),
    ],
    dependencies: [
        .package(
            url: "https://github.com/swift-server/swift-aws-lambda-runtime.git",
            .upToNextMajor(from: "0.1.0")),
    ],
    targets: [
        .target(
            name: "Handler",
            dependencies: [
                .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
                .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"),
            ]
        ),
    ]
)

API Gatewayのリクエストを受けて適当なJSONを返すのは、以下のようになる。

import AWSLambdaEvents
import AWSLambdaRuntime
import Foundation

struct Response: Codable {
    let message: String
}

Lambda.run {
    (
    context,
    request: APIGateway.Request,
    callback: @escaping (Result<APIGateway.Response, Error>) -> Void
    ) in
    context.logger.debug("\(request)")
    let encoder = JSONEncoder()
    do {
        let response = Response(message: "OK")
        let json = try encoder.encode(response)
        callback(
            .success(
                APIGateway.Response(
                    statusCode: .ok,
                    headers: ["Content-Type": "application/json"],
                    body: String(bytes: json, encoding: .utf8)
                )
            )
        )
    } catch {
        callback(.failure(error))
    }
}

Lambda Runtimeの説明で書いたようなイベントループとかそういうのは、すっかり抽象化されている。

あとはこれをbootstrapという実行ファイルにして、Lambdaにアップロードすればいい。

パッケージング

実行ファイルはAmazon Linux 2で作る。Dockerの公式なSwiftイメージに、Amazon Linux 2のものが用意されているので、今回はswift:5.2-amazonlinux2を使う。

チュートリアルを真似て、簡単なシェルスクリプトを用意する。

#!/bin/bash

set -eu

executable=$1

swift build --product $executable -c release

target=.build/lambda/$executable
rm -rf "$target"

mkdir -p "$target"
cp ".build/release/$executable" "$target/"
cp -Pv \
  /usr/lib/swift/linux/libBlocksRuntime.so \
  /usr/lib/swift/linux/libFoundation.so \
  /usr/lib/swift/linux/libFoundationNetworking.so \
  /usr/lib/swift/linux/libFoundationXML.so \
  /usr/lib/swift/linux/libdispatch.so \
  /usr/lib/swift/linux/libicudataswift.so \
  /usr/lib/swift/linux/libicudataswift.so.65 \
  /usr/lib/swift/linux/libicudataswift.so.65.1 \
  /usr/lib/swift/linux/libicui18nswift.so \
  /usr/lib/swift/linux/libicui18nswift.so.65 \
  /usr/lib/swift/linux/libicui18nswift.so.65.1 \
  /usr/lib/swift/linux/libicuucswift.so \
  /usr/lib/swift/linux/libicuucswift.so.65 \
  /usr/lib/swift/linux/libicuucswift.so.65.1 \
  /usr/lib/swift/linux/libswiftCore.so \
  /usr/lib/swift/linux/libswiftDispatch.so \
  /usr/lib/swift/linux/libswiftGlibc.so \
  "$target"
cd "$target"
ln -s "$executable" "bootstrap"
zip --symlinks lambda.zip *

swift buildして、Swiftのランタイムや標準ライブラリ(shared objectファイル)を集めてきて、実行ファイルをbootstrapという名前でsymlinkして、ZIPにまとめている。

$ docker run --rm -it swift:5.2-amazonlinux2 ls /usr/lib/swift/linux
libBlocksRuntime.so       libicui18nswift.so.65.1
libFoundation.so          libicuucswift.so
libFoundationNetworking.so    libicuucswift.so.65
libFoundationXML.so       libicuucswift.so.65.1
libXCTest.so              libswiftCore.so
lib_InternalSwiftSyntaxParser.so  libswiftDispatch.so
libdispatch.so            libswiftGlibc.so
libicudataswift.so        libswiftRemoteMirror.so
libicudataswift.so.65         libswiftSwiftOnoneSupport.so
libicudataswift.so.65.1       libswift_Differentiation.so
libicui18nswift.so        x86_64
libicui18nswift.so.65

あとはこれをDockerで動かす。

FROM swift:5.2-amazonlinux2

RUN yum -y update && yum -y install \
  zip

COPY build.sh /build.sh
WORKDIR /src
ENTRYPOINT ["/build.sh"]

ソースコード/srcにマウントするつもりなので、Dockerfileはこれだけ。

$ docker build --tag swift-lambda-builder .
$ docker run --rm --volume /Users/cockscomb/swift-lambda/handler:/src swift-lambda-builder Hanlder

あとはこういう感じで実行すると、マウントされたディレクトリ下に.build/lambda/Handler/lambda.zipができる。

AWS CDKを使う

パッケージングからデプロイの作業は退屈なので、Infrastructure as Codeって感じで、AWS CDKを使ってまとめてしまう。AWS CDKというのは、AWS CloudFormationをいい感じにしてくれるやつ。

CloudFormationは、YAMLとかで書かれたテンプレートをもとに、AWS上のリソース(ここではLambdaとか)を作成したり、更新したりしてくれる。CloudFormationで作ったリソースは、手で書き換えてはいけない。CloudFormationする時の単位をスタックと言う。

AWS CDKを使うと、CloudFormationのテンプレートをTypeScriptなどのプログラミング言語で書けるようになる。今回は使わないが、スタック間のリソースの依存関係もうまく表現できる。

CDKをセットアップしたら、以下のようなスタックを定義する。

import * as cdk from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";
import * as apigateway from "@aws-cdk/aws-apigateway";

export class ApiGatewaySwiftStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const code: lambda.AssetCode = // TODO: ここをなんとかする

    const handler = new lambda.Function(this, "Handler", {
      code,
      handler: "Handler",
      runtime: lambda.Runtime.PROVIDED,
    });

    new apigateway.LambdaRestApi(this, "Api", {
      handler,
    });
  }
}

Lambda Functionを作って、ランタイムをProvidedにして、API Gatewayにくっつけている。AWS CDKのAPI Referenceに、各パッケージの主要な使い方が載っているので、とっつきやすい。

import "source-map-support/register";
import * as cdk from "@aws-cdk/core";
import { ApiGatewaySwiftStack } from "../lib/api-gateway-swift-stack";

const app = new cdk.App();
new ApiGatewaySwiftStack(app, "ApiGatewaySwiftStack", {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION,
  },
});

ここで作ったclass ApiGatewaySwiftStackを、appの側でこういう感じでnewしてやればOK。

// TODO: ここをなんとかする、と書いたところでさっきのlambda.zipを作ってやりたいのだけど、どうしたものかと思っていたら、オフィシャルの@aws-cdk/aws-lambda-nodejsパッケージで、Builder classを作ってDockerでなんかしているのを見つけたので、真似する。

import * as lambda from "@aws-cdk/aws-lambda";
import { spawnSync, SpawnSyncReturns } from "child_process";
import * as path from "path";

interface Options {
  dir: string;
  executable: string;
}

export class Builder {
  private static imageName: string = "swift-lambda-builder";

  constructor(private readonly options: Options) {}

  private docker(args: string[]): SpawnSyncReturns<string> {
    const returns = spawnSync("docker", args);
    if (returns.error) {
      throw returns.error;
    }
    if (returns.status !== 0) {
      throw new Error(
        `[Status ${
          returns.status
        }] stdout: ${returns.stdout?.toString().trim()}\n\n\nstderr: ${returns.stderr?.toString().trim()}`
      );
    }
    return returns;
  }

  public build(): lambda.AssetCode {
    this.docker(["build", "--tag", Builder.imageName, path.join(__dirname, "../builder")]);
    this.docker(["run", "--rm", "--volume", `${this.options.dir}:/src`, Builder.imageName, this.options.executable]);
    return lambda.Code.fromAsset(
      path.join(this.options.dir, "./.build/lambda/", this.options.executable, "lambda.zip")
    );
  }
}

こういうDocker CLIを呼び出すものを作って、さっきのところに埋める。

import * as cdk from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";
import * as apigateway from "@aws-cdk/aws-apigateway";
import * as path from "path";
import { Builder } from "./builder";

export class ApiGatewaySwiftStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const code = new Builder({
      dir: path.join(__dirname, "../handler"),
      executable: "Handler",
    }).build();

    const handler = new lambda.Function(this, "Handler", {
      code,
      handler: "Handler",
      runtime: lambda.Runtime.PROVIDED,
    });

    new apigateway.LambdaRestApi(this, "Api", {
      handler,
    });
  }
}

あとはこれを使ってデプロイする。事前にAWS CLIの設定をしておくとよい。

$ npm run cdk deploy

AWSアカウントでまだCDKを使ったことがなければ、先に$ npm run cdk bootstrapが必要かもしれない。

うまくいくと、API GatewayのURLが出力されるだろう。AWS ConsoleのCloudFormationを見ると、作成されたスタックが表示される。

f:id:cockscomb:20200615001508p:plain
AWS CloudFormation

適当にcurlしてみると、実際に動いている様子がわかる。

$ curl --include https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
HTTP/2 200 
content-type: application/json
content-length: 16
date: Sun, 14 Jun 2020 13:25:02 GMT
x-amzn-requestid: xxx
x-amz-apigw-id: xxx
x-amzn-trace-id: xxx
x-cache: Miss from cloudfront
via: 1.1 xxx.cloudfront.net (CloudFront)
x-amz-cf-pop: xxx
x-amz-cf-id: xxx

{"message":"OK"}

まとめ

上記の素朴なLambda Functionでは、コールドスタートとみられる場合で300から400 msの時間がかかった。一方でスタンバイからの実行では、2 ms程度だった。コールドスタートは、Lambda内部の初期化で150 ms、Lambda Functionの初期化が180 msというところ。それほど悪くはないと思う。メモリの消費は、メモリ容量を128 MBに設定したうちの51 MB。

コスト的には、Lambdaだけなら100万リクエストで1ドルかからないくらい、API Gatewayと合わせても5ドルくらいか。

ということで、SwiftでLambda Functionを作って、API Gateway経由で呼び出せるようにした。デプロイにはAWS CDKを使っている。完全なサンプルコードは以下。

実際にAWS LambdaをSwiftで開発するのかどうかというと、現時点では微妙なところである。しかしServer-side Swiftのエコシステムが、少しずつでも整っていくのは興味深い。

紛らわしい文字列をもっと紛らわしくする「InkBleed」をMac App Storeで販売開始

f:id:cockscomb:20200506153050p:plain
紛らわしい文字列をもっと紛らわしくするアプリの登場です。

インクがにじんだような読みにくいテキストを作れるMac向けの新しいアプリケーション

2020年5月7日、インクがにじんだような読みにくいテキストを作れる、Mac®︎向けの新しいアプリケーション、InkBleedがMac App Store®︎で販売開始となりました。InkBleedは、シンプルなインターフェイスにテキストを入力すると、インクがにじんだような画像をリアルタイムに生成します。画像を保存したり、コピーしたり、ドラッグしたりすることで、読みにくいテキストの画像を簡単に共有することができます。

InkBleedアプリケーションは、テキストを識別しにくくするためにmacOS®︎の最先端の技術を活用しています。Metal、Core TextとCore Graphics、Core Image、そしてユーザーインターフェイスにはSwiftUIを採用しています。

「インクがにじんでいると、テキストは判別しにくくなります。わかりやすいことが重要とされる現代で、わかりにくいことの豊さをお届けできることをとても嬉しく思います。インクのにじみのデジタル表現をぜひお楽しみください」と、ソフトウェアエンジニアリング担当、id:cockscombは述べています。

InkBleedのアイコンは、職場の同僚であり友人でもあるid:murata_sにデザインしてもらいました。美しいアイコンをDockに並べる喜びをお楽しみください。

【販売について】

InkBleedは本日からMac App Storeで有料で販売されます。InkBleedは、macOS Catalina 10.15以降を搭載し、Metalに対応しているMacコンピュータ1で利用することができます。

Download_on_the_Mac_App_Store_Badge_US-UK_RGB_blk_092917

id:cockscombについて】

id:cockscombは、趣味の一環としてこのソフトウェアを開発しています。「紛らわしい文字列をもっと紛らわしくする」というエントリで説明したように、紛らわしいテキストをもっと紛らわしくすると、ちょっとかっこいい、という表現をアプリにしたものです。どうかご了承ください。アプリの売り上げはid:murata_sと山分けし、Apple製品を購入するために使います。

在宅勤務にかこつけて自宅のデスク周りをアップグレード

京都も緊急事態宣言の対象になった4月中旬、職場で原則在宅勤務のお達しが出た。正直に言えば、3歳と0歳の子供がいる中での在宅勤務には乗り気になれなかった。それでも、社会が急速に変化する中で、自分もまた変化しなくてはいけないと思った。

f:id:cockscomb:20200504155910p:plain:w300

あまり広くない部屋に住んでいるので、以前からリビングの端に幅80センチの狭いデスクを置いている。

はじめ、12.9インチのiPad ProをSidecarにして、せめてものマルチディスプレイにした。SidecarはUSBケーブルを使うことで、Wi-Fi経由より安定して使える。またMagic Keyboardのおかげで、画面の角度が自由になる。

在宅勤務がある程度の期間続くだろうと見込んで、ディスプレイも注文した。本当は職場で使っている4Kディスプレイを持ち帰ることもできるのだけど、(どうせ前から欲しかったし、とか)いろいろと理由をつけて買ってしまうことにした。

一時的な在宅勤務のためとはいえ、長く使えるものを買いたい。4K解像度IPSパネル、USB Type-Cで繋ぐことができ、24インチから27インチ、一桁万円台、という条件でいろいろ調べ、候補を3つに絞った。

一般的にはDell U2720Qが一番バランスに優れた選択肢だろう。LG 27UL850-Wも悪くはない。しかしApple製品との連携を重視して、筆者はLG UltraFine 4K Displayを選んだ。画質もすばらしい。皆さんは真似しないでください1

合わせて ディスプレイアームも購入して、デスクに固定している。何年か前にデスクを買ったとき、ディスプレイアームのクランクが取り付けられるものを選んでいた。ディスプレイアームを使うことで、デスクをいくらか広く使える。小さいデスクでは重要なことである。そして何より、子供のイタズラでディスプレイがデスクから落下するのを防げる。

さらに有線LANも使えるようにBelkin USB-C to Gigabit Ethernet Adapter も導入。ディスプレイのUSBハブを活用している。

ディスプレイを購入したことで、生産性が劇的に向上した。ウインドウを並べて作業できる利便性は計りしれない。またUSB Type-Cのケーブル1本で、電源とディスプレイ、そして有線LANの全てにアクセスできる。便利そのもの。

f:id:cockscomb:20200502153117j:plain

ともあれ、家族の理解と協力のおかげでなんとか仕事ができている。このご時世にありがたい。私たちは、この不可逆的に変容してしまった社会で、この先も生きていかなければならない。先はあまり見えないけど、できることをやろうと思う。


  1. USB Power Deliveryが何Wとか、入力端子の充実度とか、グレアかノングレアか、DCI-P3あるいはAdobeRGBのカバー率、HDRへの対応、視野角とコントラスト比に最大輝度、スピーカーの有無、応答速度や可変フレームレートへの対応、電源が内蔵かどうかなど、色々な判断材料があると思います。