ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Swift Packageプラグインを作成する
開発ワークフローをカスタマイズします。Swiftで独自のパッケージプラグインを作成する方法をご覧ください。PackagePlugin APIでソースコードを生成したりリリースタスクを自動化したりして、Xcodeの機能を拡張する方法をはじめ、優れたプラグインを作成するベストプラクティスを紹介します。
リソース
関連ビデオ
WWDC22
WWDC21
WWDC20
WWDC19
-
ダウンロード
こんにちはBorisです 「Swift Packageプラグインを作成する」 へようこそ
私たちはXcode 11で Swiftパッケージのサポートを導入し ライブラリをソースコードとして 配布する簡単なアプローチを提供します Xcode 14では Swiftパッケージプラグインによって ソースコードの生成や リリース作業の自動化などの 開発ワークフローに コンポーネントを構造化しての共有と同じ 優れた方法を提供したいと考えています まず 講演の概要を説明します プラグインの基本を学んだ後 デモで初めてカスタムコマンド プラグインを構築します 次に プラグインの作成について詳しく見ていき さらにデモでインビルドとプリビルドの両方の コマンドプラグインを作成していきます
パッケージプラグインは パッケージマニフェストと同様に PackagePlugin API を 使用する Swift コードです プラグインは 明確に定義された 拡張ポイントを通じて XcodeやSwift パッケージマネージャの 機能を拡張することができます
パッケージプラグインの 仕組みはどうでしょう? Xcodeは プラグインを コンパイルして実行します プラグインは利用可能な実行ファイルと 入力ファイルに関する情報を使用して コマンドを作成し必要に応じて 実行するためにXcodeに伝達し返します
パッケージプラグインは ソースコードや リソースファイルを生成するなど ビルド前またはビルド中に実行される カスタムビルドタスクを 提供することができます
SwiftPMのコマンドラインインターフェースに カスタムコマンドを追加したり Xcodeにメニュー項目を追加したりもできます プラグインの基本については 「Swift Packageプラグインの紹介」 を見ることをお勧めしますし パッケージが全く初めてという場合には WWDC19「Swiftパッケージを作成する」 のセッションを見るとよいでしょう
最初のカスタムコマンド プラグインの構築を見てみましょう
Swiftオープンソースのtools-support-core パッケージで作業しているのですが プロジェクトへの貢献者をリストアップする テキストファイルを 追加したいのですがどうすれば良いでしょう また パッケージのGit履歴から 必要に応じて再生成したいです
これまではシェルスクリプトや makefileを書いていたかもしれませんが カスタムコマンドプラグインを作って Xcodeを離れることなくファイルを 再生成できるようにしたいと思います
まず プラグインのディレクトリ構造を 作成する必要があります パッケージのコンテキストメニューを開き 新規フォルダを選択して 既存のSourcesやTestsと同様にPluginsという トップレベルフォルダを作成します
次にプラグインターゲット用に GenerateContributors という名前の別のネストした フォルダを作成します
そしてその中に新しいファイルを作り 「plugin.swift」という名前を付けます
次に パッケージマニフェストを 変更して 新しいターゲットを そこに宣言する必要があります その前に このパッケージのツールバージョンを 5.6に上げる必要があります プラグインはこのバージョン からしか利用できないからです
次に プラグインターゲットを挿入します
ここで 新しいマニフェストAPI を見てみましょう
ソースモジュールのターゲットと同様に Plugins フォルダ内のフォルダに対応する プラグインターゲットを作成します
これは Xcodeのメニュー項目としてだけでなく フォルダの名前としても 関連性のある名前を取得します
ケーパビリティを指定するのに どのような拡張ポイントを使えば良いのか インテントでは プラグインが 行うことの説明と同様に
インテントでは プラグインが 行うことの説明と同様に SwiftPM のコマンドライン用の動詞を定義でき 最後にプラグインが必要とする パーミッションを宣言できます
今回は パッケージのルートに 新しいファイルを書き込むので そのディレクトリに書き込む パーミッションが必要です 理由文字列はプラグインのユーザーに表示され OS自体のパーミッションの仕組みと同様に パーミッションを許可するか 分かるようになっています プラグインを宣言したところで 実際に実装してみましょう
このプラグインは コミット履歴を 取得するため Gitにシェルアウトします 外部のGitコマンドの standardoutから履歴を読み その結果を解析して 最後にテキストファイルに書き出します
先ほど作成した プラグインソースファイルを開き PackagePluginをインポートします
これはPackageDescriptionのような 組み込みモジュールで プラグインを実装するために 使用できるAPIにアクセスが可能になります
GenerateContributors構造体を定義し CommandPluginに適合させます
プロトコルを実装するための 足りないスタブを取得する fix-itを受け付けます この構造体はプラグイン実行ファイル のメイン関数となるため @mainとしてマークする必要があります
performCommandは コマンドのエントリポイントで 2つの引数を受け取ります contextは 解決された パッケージグラフや実行されている コンテキストに関するその他の 情報へのアクセスを提供し 引数も同様に提供します カスタムコマンドは ユーザによって呼び出されるため 引数という形で入力を与えることができます 今回は単純なコマンドを作成するので 実際にはユーザに何のオプションも 提供しません
コミット履歴の情報を取得するために gitにシェルアウトし そのためにProcess APIを使いたいので Foundationをインポートしています
次に プロセスインスタンスを定義し いくつかのフォーマット引数を 指定して git log を実行するように設定します
プロセスの出力を取り込むための パイプを作成する必要があります あとは実行し終了するのを待つだけです
処理が終了したら パイプからすべてのデータを読み込んで すべてのgitログが出力される 文字列に変換します
文字列操作をして 出力を重複のないリストに切り詰め 最後に「CONTRIBUTORS.txt」という ファイルに書き出すことができます カスタムコマンドは パッケージのルートディレクトリで 実行されるので そこにファイルを格納します
保存した後 プロジェクト ナビゲータでパッケージを 右クリックすると コンテキストメニューに コマンドの新しい項目が表示されます 実行しましょう!
次のダイアログではプラグインの 入力となるパッケージやターゲット 引数を選択できますがこのプラグインは これらオプションに反応しないので Runをクリックします
次に 先ほどのマニフェストで定義した パーミッションが求められます プラグインは自分で書いただけなので そのまま実行してもいいのですが 信頼できるプラグインにだけ 特別なパーミッションを与た方が良いです
実行後 CONTRIBUTORS.txt ファイルが プロジェクトナビゲータに表示されます
さて 最初のプラグインでXcodeを拡張したので プラグインの仕組みと 作成する際の注意点について もう少し深く掘り下げてみましょう
パッケージプラグインは パッケージマニフェスト自体の評価と 同様にサンドボックス内で実行されます ネットワークアクセスやプラグイン自身の ワークディレクトリ以外の非一時的な 場所への書き込みは禁止されています カスタムコマンドはオプションとして 先に示したように パッケージのルートディレクトリへの 書き込みを宣言できます 既存のサードパーティツールを ラッピングする場合 生成されたファイルの 書き込み先を設定するなど サンドボックスモデルに限定する方法を 検討する必要があるかもしれません
最初にプラグインの種類をお話したので カスタムコマンドとビルドツールの どちらが問題を解決するのに 適しているかは明らかだと思いますが ここではビルドツールのプラグインの 構造について見ていきましょう
これらのプラグインは ビルド中の実行ファイルの説明や ビルド中の適切なタイミングでの 作業のスケジューリングに役立つ 入出力を指定することで ビルドシステムを拡張することが可能です
Xcodeプロジェクトで 実行スクリプトのフェーズを 作成している方はここでの基本を ご存知かもしれません
また ビルドツールプラグインには 2つの種類があります ここで重要なのは ツールに定義された 出力があるかどうかです
もしそうならインビルドコマンドを作成して 出力が入力に比べて古くなった場合に ビルドシステムによって 自動的に再実行 されるようにする必要があります 明確な出力がない場合は ビルドの開始時に実行する ビルド前コマンドを作成することができます このためプレビルドコマンドで高価な 作業をすることに注意するか ユースケースに適した結果をキャッシュする カスタム戦略を考え出す必要があります
2つ目のデモでは 作業中の異なる ツール間で共有したいアイコンを カプセル化した新しい ライブラリを作成したいと思います
さっそくテンプレートから 新しいパッケージを作成し 「IconLibrary」と名付けましょう そしてすでに持っているアイコンアセットを ライブラリのターゲットにドラッグします 基本的なSwiftUIのビューとプレビューを 私のライブラリに追加してみましょう まず 必要最低限のデプロイメントターゲットを マニフェストに追加する必要があります
次に その基本的な表示とプレビューを 実際に追加してみましょう ここでは 先ほどドラッグした アセットを使用します
ここで文字列を扱う代わりに これらの画像を参照するための タイプセーフな方法があればいいですね これは アセットカタログを見て それに基づいていくつかの Swift コードを生成するビルド中の コマンドプラグインのための 素晴らしい使用例のように思えます Finderでアセットカタログを見てプラグインに 必要な情報を抽出する方法を考えてみましょう
各画像は アセット名を持つ独自の imagesetディレクトリを取得します...
そして 基本的な内容を記述した JSONファイルがあります
インビルドコマンドは 実行する実行ファイルとその入出力の 説明を提供するという点でカスタムコマンドと 少し異なる動作をします
実行ファイルはシステムから提供されるもの サードパーティのパッケージ プラグイン用にオーダーメイドで作成できます ここでは 3つ目のアプローチを とりたいと思います
プラグインは ビルドプロセスの 開始時に実行され ビルドグラフの計算に参加します
それを元に 実行ファイルがビルド実行の 一部としてスケジュールされます
さて ビルドしている実行ファイルに戻ります アセットカタログの各画像に対して コンパイル時の定数を持ちたいので それぞれの画像に対して正しい 文字列を覚える代わりに Swiftのシンボルとしてそれらを 自動完了させることができます
アセットカタログのディレクトリ の内容をループして すべての画像セットを見つけたいのです 各画像セットについてそのメタデータを解析し 実際に画像が含まれているか そのためにコードを生成する 必要があるかを判断します
そしてコードを生成しファイルに書き込めます これらのファイルはプラグインの 出力として宣言されているので プラグインが適用されているターゲットの ビルドに自動的に組み込まれます
プラグインと実行ファイル間の 通信は引数で行うので 引数を処理する方法が必要になります
最初の引数は処理中の アセットカタログへのパスで 2番目の引数は生成されたコード用に プラグインが提供するパスとなります
次に contents.jsonファイルをデコードする モデルオブジェクトが必要です
Swiftに内蔵されているJSONの デコードを使うため Decodableを使用しています
我々が関心を持つ情報は 画像リストとそのファイル名だけで 各ピクセル密度の画像が 存在しないかもしれないので 任意となります ここでは単純化した方法で 文字列を積み上げるだけで コードを生成することにします 必要なフレームワークである Foundation と SwiftUI をインポートして開始します
アセットカタログのディレクトリの 内容をループして すべての画像セットを見つけます 次に JSONをパースする必要があります ファイル名は入力パラメータを使用します Foundationの「JSONDecoder」 APIを使ってデコードしています
関心を持つべき主な情報は 与えられた画像セットに対して 定義された画像が存在するかということです これは 空でないファイル名を持つ画像が 少なくとも1つあるかどうかを チェックすることによって決定されます 与えられた画像セットに画像がある場合 パッケージのバンドルからその画像を ロードするSwiftUI画像を生成したいと思います
ビルドシステムがリソースを持つ 各パッケージに対して 作成するリソースバンドルである モジュールバンドルから 与えられた画像をロードする各画像の ベース名を持つ文字列を 構築することで行っています
引数で与えられた 生成されたコードを ファイルに書き込むことで 実行ファイルの作業をまとめることができます
Xcodeに戻り 実行ファイルを作成しましょう
「AssetConstantsExec」と呼んでいます...
メインファイルを追加します
パッケージマニフェストで 宣言する必要があります
そして そのメインファイルに 先ほど説明したコードを追加します
コードを生成できる実行ファイルができたので プラグインを使ってビルドシステムに 取り込むことができます
必要なターゲットを追加し ライブラリターゲットから プラグインの使用法も追加しましょう
以前同様 PackagePlugin ライブラリをインポートし 今回はBuildToolプラグインプロトコルに 準拠した構造体を作成します
エントリーポイントは似ていますが ユーザー引数の代わりに ここではターゲットが与えられています これはプラグインが 適用されるターゲットであり 与えられたプラグインを使用するターゲットごとに 一度だけエントリーポイントが呼び出されます
このプラグインは 特にソースモジュールターゲット (バイナリターゲットなどとは対照的に 実際に ソースファイルを運ぶあらゆる ターゲット)に注意を払います ビルドコマンドの配列を構築するために ターゲットにあるすべての xcassetバンドルをループします ビルドログに表示される 表示名用の文字列を抽出し 適切な入出力パスを 構築します また プラグインAPIを使い ここで実行ファイルを検索し ビルドコマンドをまとめることができます
これで再びプロジェクトを 組み立てる準備が整いました ビルドログを見て新たに発生したビルドの ステップを確認することができます
プラグインはビルド開始時に コンパイル 実行され そこからビルドグラフに 生成されたコマンドを追加します
ターゲットを見ると 新しい ビルドコマンドが実行されています
最後に 生成されたソースファイルは Swiftファイルを コンパイルする際の一部として表示されます
プレビューに戻り 文字列で型付けされた画像構造を 新しい定数で置き換えてみましょう
その他の画像名についても オートコンプリートを取得しています
素晴らしい比較的少ないコードで ワークフローを改善することができました 使い慣れたSwift APIを使い Xcode を離れずにワークフローを改善できました
これまで 自分たちが作っている ライブラリの一部として 自分たちが使うためのプラグインを 作ることを検討してきましたが ライブラリのようにわかりやすい形で 共有できるのもプラグインの 強力な特徴です
次のデモではXcodeに同梱されている genstringsというツールを使って ビルド前の処理を自動化したいと思います このツールは ローカライズ された文字列をコードから ローカライズディレクトリに抽出し さらに使用できるようにします 一般的には便利そうなので プラグインを別パッケージにし 単独で共有できるようにしたいと思います
パッケージのリソースや ローカライズについて詳しく知りたい方は WWDC20のセッションをお勧めします ローカライズ全般については WWDC21のSwiftUI Appの ローカライズ をご覧ください
このプラグインではまずローカライズのための 出力ディレクトリを計算することにします 与えられたターゲット内すべての SwiftまたはObjective-Cのソースファイル である入力ファイルを計算し Xcodeが提供するgenstrings ツールを実行するための プリビルドコマンドを構築します ビルド前とビルド中のコマンドの最大の違いは 明確に定義された 出力セットを宣言しないことで これらのコマンドはビルドごとに 実行されることになります
このツールは ユーザの ソースコードからすべての ローカライズ文字列を抽出し それらの文字列をローカライズ ディレクトリに書き込み ユーザプロジェクトの実際の ローカライズ作業のベースとして 使用することができるようにするものです
ここにすでに足場が出来ているのです さて パッケージマニフェストでは 先ほどと同様にプラグイン ターゲットを追加しますが プラグインプロダクトも追加しておきます
ライブラリ製品と同様に プラグインを個人ではなく パッケージのクライアントが 利用できるようにする方法です
先ほど説明したコードを書きます...
さて プラグインを構築したので 別のサンプルパッケージで テストしてみたいと思います
そのために テンプレートから 新しいパッケージを作成しましょう
パッケージにローカライズされた 文字列を提供するAPIを追加します...
生成されたテストにその用途を追加します
予想通り APIは「World」という文字列を返し このテストはうまくいきました プラグインパッケージに パスベースの依存関係を追加しましょう...
ライブラリターゲットに プラグインを使用すること
そしてまた実行します...
ビルドログを見ると、 ビルドの最初にプラグインが実行され 生成されたファイルが ターゲットに追加されています つまり リソースが最初から ターゲットの一部であるかのように リソースバンドルがビルドされ リソースアクセサが生成されます では実際にリソースバンドルを 使用するようにコードを変更しましょう
最後に コードを変更し...
生成されたバンドルも覗いてみてください...
反映された変化を見ることができます
プラグインのテストベッドができたので テストスイートを充実させ 最終的にはプラグインパッケージを 他の人と共有することができます まとめると プラグインは開発者 ツールの自動化と共有に カスタムコマンドは一般的なタスクの自動化に ビルドツールはビルドプロセス中の ファイル生成に利用することが可能です ご清聴ありがとうございました!
-
-
3:40 - GenerateContributors plugin target
// MARK: Plugins .plugin( name: "GenerateContributors", capability: .command( intent: .custom(verb: "regenerate-contributors-list", description: "Generates the CONTRIBUTORS.txt file based on Git logs"), permissions: [ .writeToPackageDirectory(reason: "This command write the new CONTRIBUTORS.txt to the source root.") ] )),
-
5:06 - GenerateContributors plugin implementation
import PackagePlugin import Foundation @main struct GenerateContributors: CommandPlugin { func performCommand( context: PluginContext, arguments: [String] ) async throws { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/git") process.arguments = ["log", "--pretty=format:- %an <%ae>%n"] let outputPipe = Pipe() process.standardOutput = outputPipe try process.run() process.waitUntilExit() let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() let output = String(decoding: outputData, as: UTF8.self) let contributors = Set(output.components(separatedBy: CharacterSet.newlines)).sorted().filter { !$0.isEmpty } try contributors.joined(separator: "\n").write(toFile: "CONTRIBUTORS.txt", atomically: true, encoding: .utf8) } }
-
10:28 - Minimum Deployment Target
platforms: [ .macOS("10.15"), .iOS("12.0"), .tvOS("12.0"), .watchOS("6.0"), ],
-
10:35 - Basic SwiftUI view and preview
import SwiftUI struct ContentView: View { var body: some View { Image("Xcode", bundle: .module) .resizable() .frame(width: 200.0, height: 200.0) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
-
14:56 - AssetConstantsExec executable target
.executableTarget(name: "AssetConstantsExec"),
-
15:03 - AssetConstantsExec implementation
import Foundation let arguments = ProcessInfo().arguments if arguments.count < 3 { print("missing arguments") } let (input, output) = (arguments[1], arguments[2]) struct Contents: Decodable { let images: [Image] } struct Image: Decodable { let filename: String? } var generatedCode = """ import Foundation import SwiftUI """ try FileManager.default.contentsOfDirectory(atPath: input).forEach { dirent in guard dirent.hasSuffix("imageset") else { return } let contentsJsonURL = URL(fileURLWithPath: "\(input)/\(dirent)/Contents.json") let jsonData = try Data(contentsOf: contentsJsonURL) let asset🐱alogContents = try JSONDecoder().decode(Contents.self, from: jsonData) let hasImage = asset🐱alogContents.images.filter { $0.filename != nil }.isEmpty == false if hasImage { let basename = contentsJsonURL.deletingLastPathComponent().deletingPathExtension().lastPathComponent generatedCode.append("public let \(basename) = Image(\"\(basename)\", bundle: .module)\n") } } try generatedCode.write(to: URL(fileURLWithPath: output), atomically: true, encoding: .utf8)
-
15:48 - AssetConstantsExec plugin target
.plugin(name: "AssetConstants", capability: .buildTool(), dependencies: ["AssetConstantsExec"]),
-
16:12 - AssetConstantsExec plugin implementation
guard let target = target as? SourceModuleTarget else { return [] } return try target.sourceFiles(withSuffix: "xcassets").map { asset🐱alog in let base = asset🐱alog.path.stem let input = asset🐱alog.path let output = context.pluginWorkDirectory.appending(["\(base).swift"]) return .buildCommand(displayName: "Generating constants for \(base)", executable: try context.tool(named: "AssetConstantsExec").path, arguments: [input.string, output.string], inputFiles: [input], outputFiles: [output]) }
-
20:19 - GenstringsPlugin target
.plugin(name: "GenstringsPlugin", capability: .buildTool()),
-
20:26 - GenstringsPlugin product
.plugin(name: "GenstringsPlugin", targets: ["GenstringsPlugin"]),
-
20:44 - GenstringsPlugin implementation
guard let target = target as? SourceModuleTarget else { return [] } let resourcesDirectoryPath = context.pluginWorkDirectory .appending(subpath: target.name) .appending(subpath: "Resources") let localizationDirectoryPath = resourcesDirectoryPath .appending(subpath: "Base.lproj") try FileManager.default.createDirectory(atPath: localizationDirectoryPath.string, withIntermediateDirectories: true) let swiftSourceFiles = target.sourceFiles(withSuffix: ".swift") let inputFiles = swiftSourceFiles.map(\.path) return [ .prebuildCommand( displayName: "Generating localized strings from source files", executable: .init("/usr/bin/xcrun"), arguments: [ "genstrings", "-SwiftUI", "-o", localizationDirectoryPath ] + inputFiles, outputFilesDirectory: localizationDirectoryPath ) ]
-
21:10 - Localized string API
import Foundation public func GetLocalizedString() -> String { return NSLocalizedString("World", comment: "A comment about the localizable string") }
-
21:44 - Path-based dependency on GenstringsPlugin
.package(path: "../GenstringsPlugin"),
-
21:52 - Use of GenstringsPlugin in library target
plugins: [ .plugin(name: "GenstringsPlugin", package: "GenstringsPlugin"), ]
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。