Tryswift jp simard cover

実践的クロスプラットフォームSwift

今ではSwiftはApple以外のプラットフォームでも利用できます。iOSアプリに限らず、より広い場面でコードを使ってもらえるようになりました。try! Swiftで話されたこのプレゼンテーションでは、クロスプラットフォームに対応したSwiftのコードの実践的な書き方、テスト、そして利用する方法を示します。そのとき、CocoaやObjective-Cの便利な機能はできる限りそのまま残すようにします。


The Goal of Cross Platform Swift Code (1:58)

今回はクロスプラットフォームで動くSwiftをどのように書くかを学びます。これから次のことについて見ていきます。

  1. 開発環境をセットアップする
  2. Swift Package Managerを使うには
  3. テストの正しい方法
  4. 継続的インテグレーション

クロスプラットフォームに対応するためにまず重要なのは、いくつのプラットフォームに対応する必要があるかを知っておくことです。まずDarwinと64-bit Linuxという2つのプラットフォームがあります。DarwinはOS X、iOS、watchOS、そしてtvOSを含んでいます。それだけではありません。Darwinで動くSwiftのバイナリは2種類あります。ひとつはXcodeとしてリリースされているバージョン、もうひとつはオープンソースのバージョンです。つまり3種類の異なるプラットフォームがあるということです。

期待と現実 (2:42)

目的はどのプラットフォームでも動く力強いSwiftのコードが書けるようになることです。当初は純粋のSwiftでコードを書き、それをスクリプトとして使えるようにしておけば良いと考えていました。しかし実際にはそれではうまく動きませんでした。例えば、Grand Central Dispatchはありません。他のプラットフォームではビルドにXcodeを使うことができません。

セットアップ (5:05)

各プラットフォームのために3つの別のマシンを用意することもできるかもしれませんが、すべてを1つのMacでできれば理想的です。この3種類の開発環境を用意するのにオススメの方法は次のようになります。

  1. すでにリリース済みのSwiftをビルドするためのXcode。これはAppleが正式バージョンとしてリリースしたツールチェーンです。
  2. オープンソース版のツールチェーン。Xcodeから切り替えて利用することができます。
  3. Dockerを使ってLinuxの仮想インスタンスをMacに用意します。ターミナルと、コードを編集するためのテキストエディタも入れておきましょう。

記事の更新情報を受け取る

1つ目はただのXcodeです。もうすでにインストールされているはずです。2つ目はXcode 7.3(まだベータ版です)です。このバージョンから任意のバージョンのSwiftツールチェーンを切り替えて使うことができるようになりました。結果的に、SourceKitとLLDBも一緒に使えるようになりました。

3つ目はちょっと複雑です。Dockerというツールを使います。仮想環境を提供するツールでHomebrewを使ってインストールできます。下記のコマンドを実行すると、Macで動くLinuxの環境が準備できます。その環境は通常のシステムとは完全に切り離されています。

$ brew install docker docker-machine
$ docker-machine create --driver virtualbox default
$ eval $(docker-machine env default)
$ docker pull swiftdocker/swift
$ docker run -it -v `pwd`:/project swiftdocker/swift bash

root@445ab1838149 $
cd /project
root@445ab1838149 $ swift build & swift test

最後のコマンドを実行するとDockerによって開発環境のディレクトリをマウントできます。クロスプラットフォームで動作するモジュールやフレームワークを作ろうとしている場合は、そのまま慣れたターミナルアプリを使ってシェルコマンドで開発を進めることができます。SublimeやAtom、Textmateといった使い慣れたエディタも使えます。もちろんXcodeを使ってもいいですね。

Swift Package Manager (9:00)

Swift Package ManagerはSwiftの依存パッケージを取得するための優れたツールです。CocoaPodsやCarthageと共存することもできますが、クロスプラットフォームでSwiftを使う場合は、Swift Package Managerだけが唯一のバッケージ管理ツールとなります。CocoaPodsもCarthageも実際のビルドにはXcodeを必要とするからです。依存関係のない実行ファイルを1つだけビルドする場合でもSPMを使うことを強くおすすめします。フレームワークやモジュール、ユニットテストのアーキテクチャを管理することができます。

Things You Would Expect to Work (10:36)

残念なことに、単純に動くと考えていたことが実はObjective-Cランタイムに依存していたりします。例えば、dynamicキーワードはSwiftの機能ですが、Objective-Cランタイムに依存しています。つまり、これを使うためには移植する必要があります。

キャストもそのまま動くと期待していたと思いますが(キャストとはある型を別の型に変換することです)、キャストもまたObjective-Cランタイムに強く依存しています。

そして、FoundationやGrand Central DispatchといったDarwinにしか提供されていないフレームワークがあります。ただ、AppleはこれらのフレームワークをLinuxに移植するという決定をしたので、このようなフレームワークが使えないのは一時的なものです。フレームワークの自動インポートについても、これはXcodeが提供している機能なので問題になります。

下記のコードは純粋なSwiftだけで書かれているので、Linux上でそのまま動くはずです。しかし、キャストの部分でエラーが起こります。それは最初のDiffの箇所は実はObjective-Cランタイムに依存しているからです。このようなキャストはLinuxではうまく動きません。

public func materialize<T>(@autoclosure f: () throws -> T) -> Result<T, NSError> {
   do {
     return .Success(try f())
-  } catch {
-    return .Failure(error as NSError)
+  } catch let error as NSError {
+    return .Failure(error)
   }
 }

細かな動作環境の違いに対応するために、たくさんの#ifプリプロセッサマクロを使うことになります。Swift Package Managerが使えるかどうかをチェックする必要があります。また、システムのライブラリやフレームワークが存在するかどうかをチェックする必要もあります。次のコードはおそらく何度もすることになる場合分けの例です。しかし、このような制限は将来的にSwiftコアチーム、あるいは外部のコントリビュータによってSwiftの移植性が改善されて減っていくでしょう。

#if
 SWIFT_PACKAGE
import
 SomeModuleOtherwiseAvailable
#endif

#if os(Linux)
// some arcane hack
#else
// something more reasonable
#endif

テスト (10:36)

数週間前までは、Swift Package Managerを使ってXCTestを使うテストをするには、アップルが提供しているライブラリをコピーしてくる必要がありました。なぜならSPMは外部の依存ライブラリを使用するにはGitのタグがなければならないからです。ですので、フォークしてモジュールの依存関係としてビルドしなければなりませんでした。そのためにターゲットをパッケージの一部としてビルドする必要がありました。

import PackageDescription

let
 package = Package(
  name: "MyPackage",
  targets: [
    Target(name: "MyPackage"),
    Target(name: "MyPackageTests", // build tests as a regular target...
      dependencies: [.Target(name: "MyPackage")]), // ...that depend on the main one
  ],
  dependencies: [
#if !os(Linux) // XCTest is distributed with Swift releases on Linux
    .Package(
      // no version tags at apple/swift-corelibs-xctest, so fork it
      url: "https://github.com/username/swift-corelibs-xctest.git",
      majorVersion: 0
    ),
#endif
  ]
)

この挙動は最近、大幅に書き換えられ、最新のSwift Package Managerのスナップショットではテストが利用できるようになっています。Swift Package Managerはプロジェクトのソースファイルを調べて、自動的にテストターゲットを作成します。もしDarwin用のXCTestフレームワークが利用できるならそれを使います。そうでなければ、Swift Package Managerのスナップショットで提供されているXCTestを利用します(Swiftに組み込みのものということになります)。それを利用するためには、@testableを付加してテストからアクセス可能にし、テストを除く本来の配布物だけに含まれるパッケージをビルドして、XCTestに渡します。

SPM TESTING

Package.swift # can be empty in simplest configuration
Sources/
  MyPackage/
    file.swift
Tests/
  LinuxMain.swift # needs `@testable import MyPackagetest` & `XCTMain()`
  MyPackage/ # *must* be named after package being tested
    test.swift # can `import XCTest` and `@testable import MyPackage`

Swift組み込みのXCTestはDarwin版と異なり、実行時のリフレクションをサポートしていないので、テストメソッドを明示的に指定する必要があります。

継続的インテグレーション (17:42)

Travis CIのような継続的インテグレーションの環境を3種類すべてのプラットフォームでセットアップするにはどうすればいいでしょうか?Macではテストと外部の依存関係を記述すること、そしてSPMを動作させるために、SwiftのスナップショットをダウンロードしてパスをXcodeに設定することです。

本質的にはLinuxで動作させる場合でも同様で、スナップショットをダウンロードし、パスを設定します。

Travis CIの設定

matrix:
  include:
    - env: JOB=OSX_Xcode
    - env: JOB=OSX_SPM
    - env: JOB=Linux

Xcodeの設定

script: xcodebuild test
env: JOB=OSX_Xcode
os: osx
osx_image: xcode7.2
language: objective-c
before_install: pod install / carthage update / etc.

OS XとSPMの設定

script:
  - swift build
  - .build/Debug/MyUnitTests
env: JOB=OSX_SPM
os: osx
osx_image: xcode7.2
language: objective-c
before_install:
  - export SWIFT_VERSION=swift-DEVELOPMENT-SNAPSHOT-2016-02-25-a
  - curl -O https://swift.org/builds/development/xcode/$(SWIFT_VERSION)/$(SWIFT_VERSION)-osx.pkg
  - sudo installer -pkg $(SWIFT_VERSION)-osx.pkg -target /
  - export PATH=/Library/Developer/Toolchains/$(SWIFT_VERSION).xctoolchain/usr/bin:"${PATH}"

Linuxの設定

script:
  - swift build
  - .build/Debug/MyUnitTests
env: JOB=Linux
dist: trusty
sudo: required
language: generic
before_install:
  - DIR="$(pwd)"
  - cd ..
  - export SWIFT_VERSION=swift-DEVELOPMENT-SNAPSHOT-2016-02-25-a
  - wget https://swift.org/builds/development/ubuntu1404/$SWIFT_VERSION/$SWIFT_VERSION-ubuntu14.04.tar.gz
  - tar xzf $SWIFT_VERSION-ubuntu14.04.tar.gz
  - export PATH="${PWD}/${SWIFT_VERSION}-ubuntu14.04/usr/bin:${PATH}"
  - cd "$DIR"

We’re Still in the Early Days (19:58)

本日、私がこの講演の内容をみなさんに共有したのは、マルチプラットフォームでSwiftがどのように動作するかをドキュメント化して残しておくためですが、それだけでなく、今のできるだけ早いうちにこの方面について取り掛かっておきたかったからです。言い換えると、みなさんにもぜひSwiftオープンソースプロジェクトに貢献して欲しいと思っています。標準ライブラリやFoundationの移植、XCTest、もしくはコンパイラについて、なんでもいいです。ありがとうございました。

Q&A (21:00)

Q: 将来的にXcodeがDarwin以外のプラットフォームでも動くようになると思いますか?

おそらくそれはないでしょう。もしそうなるとしたら、アップルの社内だけの話だと思います。結局のところ、Xcodeはオープンソースではありません。SwiftコアチームはSourceKitなどのように、IDEの作成をサポートするための機能を上手に切り出してくれたので、Xcode以外のIDEをSwiftで実現することはできます。LibIDEやlibparseなどのプラットフォームをまたいで使えるライブラリもあります。SourceKitは今はDarwinプラットフォームでしか動きません。SourceKitをLinuxに移植して、コード補完やシンタックスハイライトなどの機能を使って、Xcodeの移植ではなく別のIDEを作ることはできますね。

Q: スライドにあったキャストのエラーについてですが、Objective-Cランタイムに依存しているのはどこなのでしょうか?

わかりません。コンパイラのコードをデバッグしたりして、すべてのプラットフォームのSwiftにそれが含まれていて、LLDBなどではちゃんと使えるのですが、なぜか問題のエラーは起こるのです。

一つ理由としてありそうなのは、Objective-Cランタイムに依存していて、型の解決にランタイムの仕組みを使っているのではないかと思います。なので動的なキャストはランタイムの中で解決されているので、その外でリンクしようとしてもうまく動かないのではないかと思っています。

About the content

2017年3月のtry! Swift Tokyoの講演です。映像はRealmによって撮影・録音され、主催者の許可を得て公開しています。

JP Simard

JP works at Realm on the Objective-C & Swift bindings, creator of jazzy (the documentation tool Apple forgot to release) and enjoys hacking on Swift tooling.

4 design patterns for a RESTless mobile integration »

close