cockscomblog?

cockscomb on hatena blog

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への対応、視野角とコントラスト比に最大輝度、スピーカーの有無、応答速度や可変フレームレートへの対応、電源が内蔵かどうかなど、色々な判断材料があると思います。

SwiftUIではSingletonの代わりにEnvironmentを使うことができる

ここで言うSingletonというのは、ある種のグローバル変数を指している。そもそもクライアントアプリケーションの開発においては、実質的なグローバル変数が出現しやすい。環境にたった一つしか存在しない、存在すべきでない、というものが見出せる。例えばタイムゾーンがそれである。アプリケーション内のタイムゾーンはシステムに合わせるのが自然であり、アプリケーション中で複数存在することはほとんど起こり得ない。

iOSmacOSでは、タイムゾーンを表すのはFoundation frameworkのTimeZoneである。システムのタイムゾーンTimeZone.currentで取得できる。これはSingletonパターンのインターフェースに近い(本当にインスタンスが一つであるかどうかをここでは問題にしない)。

このように環境中で存在する個数が限定されるような値を表すのに、Singletonパターンが用いられることがある。しかしSingletonパターンを用いると、単体テストを書くのに不便だったり、あるいは多用することでコードベースのメンテナンス性が低下するなどとして、嫌われることも多い。このような問題は、DIのようなアプローチを用いることで軽減できるだろう。

SwiftUIのEnvironment

SwiftUIでは、タイムゾーンの取得に@Environmentを使う。以下のようにしてTimeZoneインスタンスを得られる。

struct TimeZoneView: View {
    @Environment(\.timeZone) var timeZone

    var body: some View {
        Text("\(timeZone)")
    }
}

@Environment property wrapperを使い、EnvironmentValuesのpropertyをKeyPathとして(\.timeZoneのように)与えると、環境の値を読み取ることができる。

Environmentの上書き

環境の値は上書きすることもできる。以下のように、何らかのviewに対してenvironment(_:_:) modifierを呼び出すと、そのviewの子孫の環境を上書きできる。

SomeView()
    .environment(\.timeZone, TimeZone(abbreviation: "GMT")!)

全てを上書きできるわけではなく、EnvironmentValuesのpropertyにsetが設定されているものだけを上書きできる。

タイムゾーンを上書きするのはあまり有意義とは言えないが、例えばEnvironmentValues.isEnabledを上書きするのは理解しやすいユースケースである。特定のview以下を全てdisabledにするのは、リーズナブルだ。ということで、SwiftUIのviewにはdisabled(_:) modifierがあり、実質的に.environment(\.isEnabled, false)のショートハンドであろう。

新しいEnvironmentを作る

EnvironmentValuesを拡張してpropertyを追加することで、独自の値をEnvironmentに持たせることができる。extensionしてもstored propertyは追加できないが、幸いなことにEnvironmentValuesEnvironmentValues.subscript(_:)によってストレージを提供してくれる。

例えばmacOSで、viewが所属するNSWindowを環境から取得できるようにしてみる。(Singletonの例にはならないが……。)

struct WindowKey: EnvironmentKey {
    static var defaultValue: NSWindow? {
        return NSApp.mainWindow
    }
}

extension EnvironmentValues {
    var window: NSWindow? {
        get {
            return self[WindowKey.self]
        }
        set {
            self[WindowKey.self] = newValue
        }
    }
}

このようにEnvironmentKeyに準拠した型を作って、EnvironmentValuesを拡張する。そして以下のように、NSHostingViewを初期化するときにNSWindowへの参照を与える。

let window = NSWindow(...)
let contentView = ContentView()
    .environment(\.window, window)
window.contentView = NSHostingView(rootView: contentView)

あとはいつでも@Environment(\.window) var windowとして、NSWindowにアクセスできる。

EnvironmentとDIコンテナ

こうしてみると、EnvironmentはViewをターゲットとしたDIコンテナのような役割を果たしている。これはObservableObjectを対象としたEnvironmentObjectでも同じだが、Environmentの場合はObservableObject以外もその対象になる。

またEnvironmentは、ViewModifierからも参照できる。EnvironmentalModifierがそれだ。EnvironmentalModifierは、それ自体がViewModifierでありながら、resolve(in:)メソッドを実装することで、EnvironmentValuesの値を元にして別のViewModifierを解決することができる。つまりEnvironmentはViewModifierに対しても依存を提供できる。

DIコンテナとしては万能ではないが、これまでSingletonパターンを使っていたようなものの置き換えとしては十分だろう。

EnvironmentとContext

Reactに親しんでいれば、EnvironmentはReactのContextとほとんど同じということに気づくだろう。つまり、Reactでは内部的にContextを使って実現されるようなライブラリと似たようなことが、Environmentでも実現できそうだ。

まとめ

SwiftUIのEnvironmentを説明して、独自に拡張する方法を紹介した。そしてDIコンテナとの類似性から、Singletonパターンの置き換えとしての可能性を示した。

SwiftUIは昨秋にリリースされたばかりで、不完全な印象を抱きがちではあるが、Environmentのような仕組みがしっかりと整備されており、高いポテンシャルを持っているように思う。

参考

SwiftUI 徹底入門

SwiftUI 徹底入門

紛らわしい文字列をもっと紛らわしくする

同僚にid:yashigani_wヤシガニ)とid:yigarashi(ワイイガラシ)がいる。いかにも似ているが、あるとき一緒に仕事をすることになって、紛らわしさが限界を超えた。

yashigani
yigarashi

そうとわかって見たら、なるほど「y」で始まって「i」で終わるのが似ているね、くらいに思うかもしれない。しかし不意にSlackやGitHubで見かけると、一瞬どちらかわからない。

この紛らわしさをより俯瞰してみるため、画像にしてみました。

f:id:cockscomb:20200101161439j:plain
Photoshopで作った

どうだ紛らわしいだろう。全体的な形がなんだか似ている。

ところでこの画像はPhotoshopで作った。テキストにガウシアンぼかしをかけて、その後から二値化した。なかなかかっこいいから、もっといろいろ試してみたい。

前置きが長くなったが、テキストにガウシアンぼかしをかけて、その後から二値化 するのを、macOSiOSの仕組みでやってみるのが、本題である。

Core Textでテキストの画像を作る

まずはテキストを画像化しないと始まらない。テキストの画像を作るのは、Core Text(とCore Graphics)で素朴に実装できる。

import CoreGraphics
import CoreText
import Foundation

struct Padding {
    let top: CGFloat
    let left: CGFloat
    let bottom: CGFloat
    let right: CGFloat

    static var zero: Padding {
        return Padding(top: 0, left: 0, bottom: 0, right: 0)
    }
}

func render(text: NSAttributedString, padding: Padding = .zero) -> CGImage {
    let framesetter = CTFramesetterCreateWithAttributedString(text)
    let frameSize = CTFramesetterSuggestFrameSizeWithConstraints(
        framesetter,
        CFRange(),
        nil,
        CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
        nil)

    let width = Int(ceil(frameSize.width + padding.left + padding.right))
    let height = Int(ceil(frameSize.height + padding.top + padding.bottom))

    let ctx = CGContext(
        data: nil,
        width: width,
        height: height,
        bitsPerComponent: 8,
        bytesPerRow: width * 4,
        space: CGColorSpaceCreateDeviceRGB(),
        bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)
    guard let context = ctx else {
        fatalError()
    }

    context.saveGState()
    context.setFillColor(.white)
    context.fill(CGRect(origin: CGPoint.zero, size: CGSize(width: width, height: height)))
    context.restoreGState()

    let path = CGPath(
        rect: CGRect(
            origin: CGPoint(
                x: (CGFloat(width) - frameSize.width) / 2,
                y: (CGFloat(height) - frameSize.height) / 2),
            size: frameSize),
        transform: nil)
    let frame = CTFramesetterCreateFrame(framesetter, CFRange(), path, nil)
    CTFrameDraw(frame, context)

    guard let image = context.makeImage() else {
        fatalError()
    }
    return image
}

NSAttributedStringからCore TextのAPIを使ってCGImageを作る。このときpaddingを設定できるようにすると、あとで都合がいい。

Core Imageで画像を加工する

画像の加工ではいつものように、Core Imageを使うことを検討する。以前CoreImage.CIFilterBuiltinsで試したように、CIFilter.gaussianBlur()を使えば、ガウシアンぼかしは簡単だ。あとは二値化するだけだが、ここで手が止まる。意外なことに、組み込みのフィルターには二値化が存在しない。

Metal Shading Languageによるカスタムカーネル

二値化は簡単そうなので手作りする。Core ImageカーネルをMetal Shading Languageで書く。ThresholdBinary.metalを以下の内容で作った。またXcodeのBuild Settingsで、Other Metal Compiler Flags-fcikernelを、User-Defined Settingsに追加したMTLLINKER_FLAGSキーに-cikernelを、それぞれ設定する必要がある。

#include <metal_stdlib>
#include <CoreImage/CoreImage.h>
using namespace metal;

extern "C" {
    namespace coreimage {
        float4 threshold_binary(sample_t source, float threshold) {
            float4 image = premultiply(source);
            float y = 0.299 * image.r + 0.587 * image.g + 0.114 * image.b; // BT.601
            float binary = y < threshold ? 0.0 : 1.0;
            return float4(binary, binary, binary, 1.0);
        }
    }
}

RGB値から輝度成分を取り出すために、YUV色空間のYを計算する。これが閾値を超えていれば1、そうでなければ0とすることで、二値化できる。

このカーネルは以下のように読み込める。

import CoreImage

let thresholdBinaryKernel: CIColorKernel = {
    guard
        let url = Bundle.main.url(forResource: "default", withExtension: "metallib"),
        let data = try? Data(contentsOf: url)
    else {
        fatalError("Unable to get metallib")
    }
    guard
        let kernel = try? CIColorKernel(
            functionName: "threshold_binary", fromMetalLibraryData: data)
    else {
        fatalError("Unable to create CIKernel from threshold_binary")
    }
    return kernel
}()

Core Imageによる画像の加工

二値化のカーネルが書けたので、あとは実際に画像を加工するだけだ。

import CoreImage
import CoreImage.CIFilterBuiltins

let ciContext: CIContext = CIContext()
func process(image: CGImage, sigma: Float, threshold: Float) -> CGImage? {
    let ciImage = CIImage(cgImage: image)

    let gaussianBlur = CIFilter.gaussianBlur()
    gaussianBlur.inputImage = ciImage.clampedToExtent()
    gaussianBlur.radius = sigma
    guard let blurredImage = gaussianBlur.outputImage else {
        return nil
    }

    guard
        let processedImage = thresholdBinaryKernel.apply(
            extent: ciImage.extent, arguments: [blurredImage, threshold]
        )
    else {
        return nil
    }

    guard
        let cgImage = ciContext.createCGImage(
            processedImage, from: processedImage.extent)
    else {
        fatalError()
    }
    return cgImage
}

CIImage.clampedToExtent()を使うのがコツで、ガウシアンぼかしをかけるときに画像の端が変な色になってしまうのを防げる。

どうですか

ここまでで、テキストにガウシアンぼかしをかけて、その後から二値化 ができた。iOSでもmacOSでも同じように動くはずだ。

f:id:cockscomb:20200106103459p:plain

なんだかちょっとかっこよくないですか。

参考文献

Metalは初めて使うので、ちょっとだけ勉強しました。