cockscomblog?

cockscomb on hatena blog

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

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

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

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

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

Eyeballs at Menu Bar

Eyeballs at Menu Bar

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

子供にアプリを作る

3歳の息子にiPad Proを与えている。2018年の11インチのiPad Proで、僕のお下がりだ。Apple Pencilも与えてあるが、こちらは1歳になったばかりの娘が狙っているため、恐れた息子はApple Pencilをなるべく片付けておく。

自由に使っていい

iPad Proを与えたからといって、3歳の息子がそれで何か大層なことをするということはない。退屈なときにYouTube Kidsで何かを見ていることが多い。ときどきGarageBandiRig Keys 2で音を出して遊んだり、ProcreateApple Pencilで謎の絵を描いたりしている。こういうのは大人も一緒にやってあげると喜ぶ。Smart Keyboardをくっつけて「ブログを書いてる」と宣ったりもする。あとは週末に祖父母とFaceTimeをする。

とにかくiPadを自由に使わせている。自由に使えない道具に関心を持ち続けるのは難しかろうから、最低限ペアレンタルコントロールを設定して、あとはなるべく好きにさせる。

悪い大人

大人というのは悪いから、ひらがなのキーボードを見せて、これで自分の名前を入力してみなさい、と勧めたりもする。もちろんそこには、文字を覚えさせようという魂胆がある。息子も素直なもので、喜んで従う。

あるとき息子が自分の名前を入力したというので見せてもらうと、そこには逆さまに並んだ名前があった。ふつうに入力するより難しいだろうに、子供というのは不思議だ。

その晩、妻が一計を案じて、入力した文字が読み上げられたらいいのではないか、と言う。しからばと思い、僕は自慢のMacBook Pro (13-inch, M1, 2020)を取り出した。

アプリを作った

f:id:cockscomb:20201215194655j:plain
写真を撮ろうとしたら娘がいたずらしにきた

AppleプラットフォームではAVSpeechSynthesizerというのを使うと、テキストを読み上げさせられる*1。近頃はSwiftUIがあるので、UIを作るのも簡単。

アプリのコードはこういう感じ。コピペしたら動くと思う。

import SwiftUI
import AVFoundation

@main
struct SpeechApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}

struct ContentView: View {
  @StateObject var speaker = Speaker()

  var body: some View {
    VStack {
      TextField("Text", text: $speaker.text)
        .font(Font.system(size: 36))
        .textFieldStyle(RoundedBorderTextFieldStyle())
        .padding()

      Button(action: speaker.speech) {
        Label("Play", systemImage: "play.fill")
          .font(Font.system(size: 36))
      }
      .padding()
      .disabled(speaker.isSpeaking)

      GroupBox(label: Label("Speed",
                            systemImage: "speedometer")) {
        Slider(
          value: $speaker.rate,
          in: Speaker.minimumRate...Speaker.maximumRate,
          minimumValueLabel: Image(systemName: "tortoise"),
          maximumValueLabel: Image(systemName: "hare")) {
        }
        .disabled(speaker.isSpeaking)
      }
      .padding()
    }
  }
}

class Speaker: NSObject, ObservableObject {
  static let defaultRate = AVSpeechUtteranceDefaultSpeechRate
  static let minimumRate = AVSpeechUtteranceMinimumSpeechRate
  static let maximumRate = AVSpeechUtteranceMaximumSpeechRate

  private let synth = AVSpeechSynthesizer()

  @Published var text: String = ""
  @Published var rate: Float = defaultRate
  @Published private(set) var isSpeaking: Bool = false

  override init() {
    super.init()
    synth.delegate = self
  }

  func speech() {
    let utterance = AVSpeechUtterance(string: text)
    utterance.voice = AVSpeechSynthesisVoice(language: nil)
    utterance.rate = rate
    synth.speak(utterance)
  }
}

extension Speaker: AVSpeechSynthesizerDelegate {
  func speechSynthesizer(
    _ synthesizer: AVSpeechSynthesizer,
    didStart utterance: AVSpeechUtterance
  ) {
    isSpeaking = synthesizer.isSpeaking
  }
  func speechSynthesizer(
    _ synthesizer: AVSpeechSynthesizer,
    didFinish utterance: AVSpeechUtterance
  ) {
    isSpeaking = synthesizer.isSpeaking
  }
}

10分くらいでプロトタイプができて、見かけをちょっとマシにしたり、読み上げ速度を変えられるようにしたり、実機にインストールしたりで、トータルで2時間くらいかかっている。

子供にアプリを与える

翌朝、息子にアプリの入ったiPadを見せた。尊敬されたいから、お父ちゃんが作ったアプリだよ、とアピールした。息子もなんとなく嬉しそうに遊んでくれる。いい子だ。

最初は自分の名前を読み上げさせたりしていたが、すぐに滅茶苦茶な文字列を読み上げさせる方が楽しいことに気づいた。子供は自由。

ということで、必ずしも思い通りにいくわけではないが、悪くはない。子供にアプリを作るのは新しい感じがある。最近はApple Developer Programに加入していなくても、とりあえず実機にインストールするくらいはできるようになっている。加入すればAd hocに配信できるし、なんならApp Storeでリリースもできる。僕は加入している。

ということで、子供にiPadを与えたり、SwiftUIで気楽にアプリを作るのは楽しい。

最新 Apple iPad Air (10.9インチ, Wi-Fi, 64GB) - シルバー (第4世代)

最新 Apple iPad Air (10.9インチ, Wi-Fi, 64GB) - シルバー (第4世代)

  • 発売日: 2020/10/23
  • メディア: Personal Computers

*1:Webの技術で作りたければWeb Speech APIという選択肢もあると思う。

Apple Watchで血中酸素濃度を測るの巻

血中酸素濃度が測定できるようになったので、ときどき測ってみている。
95%〜100%が正常ということだけど、たまになぜか80%くらいの値が出る。
たぶんうまく測定できていないだけだと思うけど、ちょっと心配になる。

「てがきはてなブログ」がリリースされていたので、早速使ってみました。iPadで絵日記がかけてすごい!

普段は手で字を書かないので、まったくうまく書けなかった。あと日付を間違っている。そういうのも含めて、よい体験だった。

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のエコシステムが、少しずつでも整っていくのは興味深い。