Swift Playgrounds (iPad) で外部 Swift Package を使う方法
[2021-08-17 更新] この方法を自動化したショートカットを公開しました。興味のある方は使ってみてください。
Swift Playgrounds とは
Swift Playgrounds は iPadOS, macOS 向けに Apple が開発している Swift の学習や実験につかえるアプリです。初心者向けのイメージがあるこのアプリですが、iOS SDK のフレームワークや Swift の実行環境を内蔵しているため、iOS アプリ開発のプロトタイピングを行うのにもぴったりのアプリです。公式でも開発者向けに SwiftUI の View のプロトタイピングへの活用を紹介する動画を出していたりします。
今回は iPad 上でこの Swift Playgrounds を用いて小規模なアプリを開発・公開している私が普段どのようにして外部の Swift Package を Swift Playgrounds 上で使用しているかをご紹介します。
2 種類の Playground
Swift Playgrounds 上で扱える playground の形式には 2 種類あり、Playground Book (.playgroundbook) と Xcode Playground (.playground) があります。どちらも .app ファイルなどと同じように実際にはディレクトリとなっています。実際の内部ディレクトリ構造については末尾の Appendix 1 や Appendix 2 に例を記載していますので気になった方は見てみてください。
それぞれの違いは以下のようになっています。
Playground Book
- Swift Playgrounds 3.0 からモジュールとユーザモジュールに対応した (後述)
- フォーマットについてのドキュメントが Apple Developer にある
- 章立てされており、その中にページが複数作成できる
- Swift Playgrounds 上でのソースコードの編集では内部の .swift ファイルには直接書き込まれず、差分として保存される
- メタデータやオプション指定のためのマニフェストファイル (.plist) をはじめ Xcode Playground には存在しないファイルがいくつかあり、それらを使って live view の外枠を無くすだったり、パフォーマンス優先で実行するだったり、実行前から特定のビューを表示させておくだったり様々なことが可能
- macOS では Mac App Store にある Swift Playgrounds アプリから実行可能
Xcode Playground
- Swift Playgrounds 3.0 からユーザモジュール相当のものには対応した (Appendix にあるように Playground Book とのディレクトリ構造の違いに注意)
- フォーマットについてのドキュメントは (おそらく) ない
- 章はなくページしか存在しない
- Swift Playgrounds 上でのソースコードの編集がダイレクトに内部の .swift ファイルに書き込まれる
- 名前の通り Xcode とも互換性がある
モジュールとユーザモジュール
Playground Book で対応したモジュールとユーザモジュールですが、Apple Developer の "Using Modules to Share Code in a Playground Book" では "Private Modules" と "User-Editable Modules" として紹介されています。
モジュールはあらかじめ Playground Book に内包させておいたコード群 (モジュール) を各ページから import
できるようにするというもので、ユーザはそのコードがどうなっているのか Swift Playgrounds 上で見ることはできません。一方、ユーザモジュールはユーザが追加・削除・コードの編集を自由にできるもので、ユーザにどこまで許可するかというのは Playground Book のマニフェストファイルでコントロールできます。
ユーザモジュールが UI 上のどこからアクセスできるのかというと、以下のように Swift Playgrounds アプリで Playground Book を開き、左上のアイコンをタップするとこのようにユーザモジュールの一覧を表示したり追加・削除・リネームしたりすることができます。
そしてそのうちの一つの Swift ファイルをタップすると以下のように別タブで開かれます。
これらモジュールやユーザモジュールは *.playgroundbook ファイルと同様にディレクトリとなっており、構成は以下のようになっています。
- *.playgroundmodule
- Sources
- Hoge.swift (任意)
- Fuga (任意)
- Piyo.swift (任意)
- Sources
Swift Package をモジュールとして使う
先程のモジュールとユーザモジュールのディレクトリ構造をよく見ると Swift Package のディレクトリ構成と似ていることがわかります。
なので、Playground Book の場合、*.playgroundbook/Contents/{Modules,UserModules} 以下に Swift Package のリポジトリをクローンして、最上位のディレクトリ名の末尾を .playgroundmodule にすると、それ以下のディレクトリ構成 (Sources など) が同じなのでそのままモジュールやユーザモジュールとして使えてしまうのです!
ただし条件があって、*.playgroundmodule のディレクトリ名は valid な Swift Identifier である必要があるので、例えばクローンしたリポジトリの最上位のディレクトリが SomePackage の場合は SomePackage.playgroundmodule で良いのですが、some-package の場合は SomePackage.playgroundmodule のようにリネームする必要があります。
また、ユーザモジュールは配下に .swift ファイルしか含められないという制約があるため、ソースコード以外のファイルをいちいち削除しなければなりません。そのため、とある Swift Package のリポジトリをクローンしてきてリネームするだけで済ますにはモジュールの方が現実的な選択になるでしょう。
また、Swift Playgrounds は SwiftPM を内蔵していたりはしないので、外部の依存パッケージがあるような Swift Package や別言語のソースコードをビルドする必要があるものには対応できません。依存解決してパッケージをフェッチしてくるようなことができないためですね。
一方で Xcode Playground の方では *.playground/Sources 以下が全ページで共有のユーザモジュール、*.playground/Pages/*.xcplaygroundpage/Sources にそのページ内でのみ使えるユーザモジュールというようなファイル構成になっています。また、これは Playground Book のユーザモジュールと違って .swift ファイル以外も含むことができます。さらに、Playground Book でいう *.playgroundmodule/Sources が露出したような形となるため、この配下には自由なディレクトリ名でディレクトリやファイルが配置できます。例えばリポジトリをクローンした結果 SomePackage ディレクトリが作られるのであれば、それをそのまま *.playground/Sources 以下に放り込めば良いことになります。
実際に試してみる
仕組みがわかったので実際に試してみましょう。この説明では Xcode Playground ではなく私がよく使う Playground Book の方で説明します。
Swift Package は Node.js の npm や Rust の crates.io のような統一されたパッケージレジストリというものが存在しないですが、Swift Package Index や Swift Package Registry のような検索サービスを使って探すことができます。今回は外部依存のないパッケージを探したいので、Swift Package Registry の方が良いでしょう。Swift Package Registry では以下のように Dependencies が表示されるので、ここが None になっているパッケージを探します。
今回はその中でも XMLCoder を使って試していきます。XMLCoder は Swift の Codable
protocol を使って XML のシリアライズ、デシリアライズを行うためのライブラリです。
まずはこのリポジトリを iPad のローカルにクローンします。ここでは Working Copy がおすすめです。Working Copy は以下のように URL を入力とした Action Extension も提供しているため、Safari で GitHub のリポジトリのページを開き、Activity View (共有シート) から "Process in Working Copy" を選択するだけでクローンが可能です。
また、Working Copy は Document Provider となっているため、クローンされたリポジトリのファイルには標準のファイルアプリや Document Picker などからアクセスできます。
次に、Playground Book を作成します。Swift Playgrounds を開くと、以下のような画面が表示されるかと思います。
ここで画像中の赤い矢印で示しているボタンのうちのどちらかをタップすることで、空の Playground Book が作成されます。(ちなみに Xcode Playground の場合は全てのテンプレートを表示させると見つかります)
次のこの Playground Book ファイル内に先程クローンしたリポジトリをコピーしていきます。私が最もおすすめする方法は以下のツイートのように Textastic を使った方法です。
(埋め込みツイートが表示されない場合はこちら)
ですが、Textastic は有料アプリであるため、この記事ではネイティブで Darwin 向けのコマンドを実行できる無料のターミナルアプリ a-Shell で代用していきます。UNIX 系 OS の CLI での操作に慣れていることが前提となるためご了承ください。(慣れていない方は上記の Textastic や他のアプリを使う方法をぜひ探してみてください)
まずは a-Shell で以下のコマンドを実行します。
pickFolder
すると以下のように Document Picker が表示されるはずです。
ここでは Working Copy を選択します。すると Working Copy のディレクトリに cd
された状態になるため、以下のコマンドで XMLCoder
ディレクトリをホームディレクトリ以下にコピーしておきます。ここで a-Shell のホームディレクトリは ~
ではなく ~/Documents
だということに注意してください。(iOS/iPadOS アプリがサンドボックスであるため少し特殊になっています)
cp -rpc XMLCoder ~/Documents/
(余談ですが、最近の Mac や iPad では APFS が採用されているため、上記のように cp
に -c
オプションがついていることによってコピーが劇的に速くなります)
その後以下のコマンドでホームディレクトリに戻ってファイルを確認すると XMLCoder
ディレクトリがコピーされていることが確認できるかと思います。
cd
ls
次に、再度 pickFolder
コマンドで今度は Playgrounds
ディレクトリ (Swift Playgrounds の保存先) を選択します。iCloud Drive が有効な場合は iCloud Drive 以下、そうでない場合は On My iPad 以下にあるのではないかと思います。
Playgrounds
ディレクトリに移動したら、先程コピーした XMLCoder
ディレクトリをモジュールとして Playground Book 内にコピーするために、以下のコマンドを実行します。以下では My Playground.playgroundbook
ファイルを対象にしていますが、作成した Playground Book のファイル名が異なる場合は適宜置き換えてください。
mkdir My\ Playground.playgroundbook/Contents/Modules
cp -rpc ~/Documents/XMLCoder My\ Playground.playgroundbook/Contents/Modules/XMLCoder.playgroundmodule
これが完了したら、最後に Swift Playgrounds に戻って、作成した Playground Book を開いて XMLCoder のサンプルコードを実行してみましょう。
サンプルコード:
import XMLCoder
let xmlStr = """
<note>
<to>Bob</to>
<from>Jane</from>
<heading>Reminder</heading>
<body>Don't forget to use XMLCoder!</body>
</note>
"""
struct Note: Codable {
let to: String
let from: String
let heading: String
let body: String
}
guard let data = xmlStr.data(using: .utf8) else { preconditionFailure() }
let note = try? XMLDecoder().decode(Note.self, from: data)
guard let returnData = try? XMLEncoder().encode(note, withRootKey: "note") else { preconditionFailure() }
String(data: returnData, encoding: .utf8)
以下のようにエラーなく実行できれば成功です。
まとめ
Swift Playgrounds は今や教育用途だけでなくちょっとしたプロトタイピングにも使えるツールとなってきていることは WWDC 2020 のセッションでもあった通りですが、Swift Package を使いたいと思った時に現状できること、そしてそれが不自由なくできるようになることでどれだけ iPad 上での開発の可能性が広がるかがわかっていただけたかと思います。
Working Copy は最近のバージョンでサブモジュールの追加もサポートしたので、Playground Book を Git リポジトリで管理しつつ、サブモジュールとして外部 Swift Package を追加するということも可能で、そうすることによってそのパッケージのバージョンが上がった際に簡単にアップデートできるようにすることも可能です。
ただ、やはり外部依存のないパッケージしか使えないというのは辛いですよね。Apple の Swift Playgrounds チームの皆さん、どうか Swift Package への対応をご検討お願いします🙏
最後に、私が公開している Playground Book をご紹介しておきます。iPad 上で Working Copy + iVim + Swift Playgrounds で開発、GitHub Action (CI) でリリースのたびにフィードを生成して Swift Playgrounds で購読できるようなフローを構築しています。
-
coreml-playground
- CoreML の画像分類、物体検出の各種デモやベンチマークを走らせる Playground Book
-
jscore-playground
- Swift Playgrounds では通常使えない JavaScriptCore を無理矢理呼び出して JavaScript のコンソールを実現した Playground Book
-
SF Symbols Viewer
- SF Symbols を一覧、検索するための Playground Book
- はやく Swift Playgrounds が iOS 14 SDK 対応して SwiftUI の
Grid
が使えるようになって欲しい...
Appendix 1: Playground Book の内部構造
- *.playgroundbook
- Contents
- Manifest.plist: Playground Book 自体のマニフェスト
- Chapters
- 章名.playgroundchapter
- Manifest.plist: 章のマニフェスト
- Pages
- ページ名.playgroundpage
- main.swift: このページを開いたときに表示されるソースコード
- Manifest.plist: ページのマニフェスト
- Template.playgroundpage: ページを追加したときに使われるテンプレートページ
- main.swift
- Manifest.plist
- ページ名.playgroundpage
- 章名.playgroundchapter
- Modules
- モジュール名.playgroundmodule
- Sources
- Hoge.swift
- Sources
- モジュール名.playgroundmodule
- UserModules
- モジュール名.playgroundmodule
- Sources
- Hoge.swift
- Sources
- モジュール名.playgroundmodule
- ... (他については公式ドキュメント参照)
- Edits: Swift Playgrounds 上でユーザが編集したコードの差分などが含まれる
- UserEdits.diffpack
- Manifest.plist: 差分用のマニフェスト (差分の一覧など)
- Chapters
- 章名.playgroundchapter
- UserManifest.plist: 差分用の章のマニフェスト
- Pages
- ページ名.playgroundpage: オリジナルに存在しないページを増やした場合は UUID が使われる
- main.swift.delta: main.swift の差分 (XML 形式)
- UserManifest.plist: 差分用のページのマニフェスト
- ページ名.playgroundpage: オリジナルに存在しないページを増やした場合は UUID が使われる
- 章名.playgroundchapter
- UserModules
- モジュール名.playgroundmodule
- Sources
- Hoge.swift.delta: Hoge.swift の差分 (XML 形式)
- Sources
- モジュール名.playgroundmodule
- UserEdits.diffpack
- Contents
Appendix 2: Xcode Playground の内部構造
1 ページの場合
- *.playground
- Contents.swift
- contents.xcplayground
- Sources: 全体で共有されるユーザモジュール
- Hoge.swift
- Fuga
- Piyo.swift
2 ページ以上の場合
- *.playground
- Pages
- ページ名.xcplaygroundpage
- Contents.swift: このページを開いたときに表示されるソースコード
- Sources: ページ限定のユーザモジュール
- Hoge.swift
- Fuga
- Piyo.swift
- ページ名.xcplaygroundpage
- Sources: 全体で共有されるユーザモジュール
- Hoge.swift
- Fuga
- Piyo.swift
- Pages
Discussion