Fragment Colocation導入に向けたXcodeカスタムテンプレートの作成
こんにちは、スペースマーケットモバイルエンジニアの村田です。
以前書いた記事([スペマiOS]ログイン/新規登録リニューアル)で触れた通り、弊社のiOSプロジェクトではデザインリニューアルや新規画面の作成時に、UIKitではなく積極的にSwiftUIを選択しています。SwiftUIへの移行を進める上でPresentaion層の設計を見直し、GraphQLの「Fragment Colocation」というデザインパターンを導入しました。
導入にあたり、1つのコンポーネントに関与する要素が増えることにより、コンポーネント追加時に必要となるボイラープレートコードの記述が手間になってきました。そこで、Xcodeのカスタムテンプレートを活用することで、コンポーネント作成時のファイル追加およびボイラープレートコード記述の手間を軽減しました。
本記事では、Fragment Colocation導入前後の構成の違いと、カスタムテンプレートの活用方法について紹介します。
Fragment Colocation
Colocate(同一場所に設置する)の意味通り、UI Componentが必要としているFragment(データ)を同じ場所に配置する手法です(図がシンプルで分かりやすかったので以下添付させていただきした🙏)
Fragment Colocationを導入することで主に以下のようなメリットがあると考えています
- コンポーネントと関連するクエリ/フラグメントを同じ場所に置くことで、管理がしやすくなる(捨てやすくなる)
- コードの可読性が向上し、どのクエリやフラグメントがどのコンポーネントで使われているかが一目でわかる
- SpaceMarketのWEBアプリケーションは2年前にFragment Colocationの思想を取り入れました。BE/FE/APPの境界を超えて開発が行われる今の開発組織において、設計思想を近づけることで、より一貫した開発体験を得ることができる
Presentation層構成
Fragment Colocation導入前後のPresentation層(+Data層)の構成を紹介します。
Fragment Colocation導入前
- MVVM+Repositoryパターン
- 1画面に対し1つのViewModel
- query/mutation/fragmentは共通のものをData/Networking/GraphQL/Operationへ定義
📁 Presentation
├── 📁 XXX
│ ├── XXXScreen.swift
│ ├── XXXScreenViewModel.swift
│ └── 📁 Views
│ ├── XXXScreen+HogeView.swift
│ ├── 📁 FugaView
│ │ ├── XXXScreen+FugaView.swift
│ │ └── 📁 Views
│ │ └── XXXScreen+PiyoView.swift
│ ︙
︙
📁 Data
├── 📁 Networking
│ ├── 📁 REST
│ └── 📁 GraphQL
│ ├── 📁 Operation: graphqlファイル格納
│ ︙
│
├── 📁 Repository: Repositoryファイル格納
│
︙
Fragment Colocation導入後
- 各コンポーネントに対しViewModel/graphqlを用意。状態やビジネスロジックを1つのViewModelに集約していましたが、それらも必要とする各コンポーネントに対し用意するよう変更しました
- query/mutation/fragmentの共通化をやめ、必要であれば各コンポーネントに対し用意するので、共通ディレクトリとRepositoryを廃止
子コンポーネントに必要なファイルが増えたことにより、コンテンツが多い画面新規作成時のコンポーネント追加によるファイル作成作業が若干ストレスに感じるようになりました。そこで、Xcodeのカスタムテンプレートを活用して、ファイル作成作業を効率化したいと考えました。
📁 XXX
├── XXXScreen.swift
├── XXXScreenViewModel.swift
+ ├── XXXScreen.graqhql
└── 📁 Views
├── 📁 HogeView
│ ├── XXXScreen+HogeView.swift
+ │ ├── XXXHogeViewModel.swift
+ │ └── XXXHogeView.grahql
├── 📁 FugaView
│ ├── XXXScreen+FugaView.swift
+ │ ├── XXXFugaViewModel.swift
+ │ ├── XXXFugaView.grahql
│ └── 📁 Views
│ └── 📁 PiyoView
│ ├── XXXScreen+PiyoView.swift
+ │ ├── XXXPiyoViewModel.swift
+ │ └── XXXPiyoView.grahql
︙
📁 Data
├── 📁 Networking
│ ├── 📁 REST
│ └── 📁 GraphQL
- │ ├── 📁 Operation: graphqlファイル格納
│ ︙
│
- ├── 📁 Repository: Repositoryファイル格納
│
︙
Xcodeカスタムテンプレート
Xcodeで新しいファイルやプロジェクトを作成する際に使用するテンプレート(雛型)をカスタマイズして作成することができます。これにより、開発者は頻繁に使用するコードの構造やファイルフォーマットを事前に設定し、迅速かつ一貫性を持ってプロジェクトに組み込むことが可能になります。
テンプレートファイル(.xctemplate)
Xcodeテンプレートは、.xctemplate
という拡張子のディレクトリとして作成されます。このディレクトリにはテンプレートに必要な設定やファイルが含まれます。
- TemplateInfo.plist: テンプレートの設定情報を定義するファイルです。ここでテンプレートの名前、種類、テンプレートがどのカテゴリに属するかなどを指定します
- Template Code Files: 実際に生成されるコードファイルの雛型(.swiftファイル、.hファイルなど)
例えば、標準テンプレートで最も頻繁に使用されるであろうSwiftファイル作成テンプレート(Package Swift File.xctemplate
)の中身は、以下のようになっています。
Package Swift File.xctemplate | テンプレート選択 |
---|---|
配置場所
標準のXcodeテンプレートは、Xcode本体に組み込まれている Templates
ディレクトリに格納されています。標準テンプレートは、Xcodeをインストールした際に自動的に含まれており、新しいファイル(Package Swift File.xctemplate
など)やプロジェクトを作成する際に使用されます。
Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/
├── File Templates : 新しいファイルを作成するためのテンプレートを格納
└── Project Templates : 新しいプレジェクトを作成するためのテンプレートを格納
一方、カスタムテンプレートは、ユーザーごとに管理される以下のディレクトリに格納することが推奨されています。これは、Xcodeのアップデートやインストール時に、上記の標準テンプレートディレクトリが変更や上書きされる可能性があるためです。
~/Library/Developer/Xcode/Templates/
子コンポーネント用カスタムテンプレート
今回、子コンポーネントに必要なView/ViewModel/graphqlファイルを生成するカスタムテンプレートを作成しました。
カスタムテンプレート構成
- SpaceMarket(ディレクトリ名==カテゴリー名)
- Child Component.xctemplate
- TemplateIcon.png
- TemplateInfo.plist
- ___VARIABLE_parent___Screen+___VARIABLE_child___.swift(Viewファイル雛形)
- ___VARIABLE_parent______VARIABLE_child___Model.swift(ViewModelファイル雛形)
- ___VARIABLE_parent______VARIABLE_child___.graphql(graphqlファイル雛形)
- Child Component.xctemplate
各ファイルに関して
TemplateInfo.plist
プロパティの設定項目については、以下の記事にわかりやすくまとめられており、参考にさせていただきました!画面名と子コンポーネント名を入力し、各ファイル名やクラス名が自動で決定されるように設計しました。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildableType</key>
<string>None</string>
<key>Kind</key>
<string>Xcode.IDEFoundation.TextSubstitutionFileTemplateKind</string>
<key>Options</key>
<array>
<dict>
<key>Identifier</key>
<string>parent</string>
<key>Required</key>
<true/>
<key>Name</key>
<string>Screen Name:</string>
<key>Type</key>
<string>text</string>
<key>NotPersisted</key>
<true/>
</dict>
<dict>
<key>Identifier</key>
<string>child</string>
<key>Required</key>
<true/>
<key>Name</key>
<string>Child Component Name:</string>
<key>Type</key>
<string>text</string>
<key>NotPersisted</key>
<true/>
</dict>
<dict>
<key>Identifier</key>
<string>viewName</string>
<key>Name</key>
<string>View Name:</string>
<key>Type</key>
<string>static</string>
<key>Default</key>
<string>___VARIABLE_parent___Screen+___VARIABLE_child___.swift</string>
</dict>
<dict>
<key>Identifier</key>
<string>viewModelName</string>
<key>Name</key>
<string>ViewModel Name:</string>
<key>Type</key>
<string>static</string>
<key>Default</key>
<string>___VARIABLE_parent______VARIABLE_child___Model.swift</string>
</dict>
<dict>
<key>Identifier</key>
<string>graphqlName</string>
<key>Name</key>
<string>GraphQL Name:</string>
<key>Type</key>
<string>static</string>
<key>Default</key>
<string>___VARIABLE_parent______VARIABLE_child___.graphql</string>
</dict>
</array>
</dict>
</plist>
View
//___FILEHEADER___
import SwiftUI
extension ___VARIABLE_parent___Screen {
struct ___VARIABLE_child___: View {
@StateObject private var viewModel: ___VARIABLE_parent______VARIABLE_child___Model
var body: some View {
EmptyView()
}
init(fragment: APIV2.___VARIABLE_parent______VARIABLE_child___) {
let viewModel = ___VARIABLE_parent______VARIABLE_child___Model(fragment: fragment)
self._viewModel = StateObject(wrappedValue: viewModel)
}
}
}
ViewModel
//___FILEHEADER___
final class ___VARIABLE_parent______VARIABLE_child___Model: ObservableObject {
init(fragment: APIV2.___VARIABLE_parent______VARIABLE_child___) {
}
// MARK: - Output
// MARK: - Input
// MARK: - Internal
}
graphql
fragment ___VARIABLE_parent______VARIABLE_child___ {
}
カスタムテンプレートによる子コンポーネント作成
作成したテンプレートを利用し、例としてHogeScreenへFugaViewコンポーネントを追加してみます。
親となる HogeScreen
に FugaView
コンポーネントを追加したいので、まずFuga
ディレクトリを用意。その後、ディレクトリを右クリックし、[New File from Template...]
を選択。
テンプレート選択のウィンドウが表示され、最下部までスクロールすると追加したカテゴリ(SpaceMarket
)とテンプレート(Child Component
)が確認できます。
Child Component
テンプレートを選択すると、設定した通り、画面名と子コンポーネント名の入力が求められます。「Screen Name」には画面名となるHoge、「Child Component Name」には追加対象であるFugaViewを入力。
Fugaフォルダ直下にView/ViewModel/graphqlの各ファイルが作成され、テンプレート通りコードが生成されていることを確認できました🎉
最後に
スペースマーケットでは一緒に働く仲間を募集中です!
詳しくは以下採用ページをご確認ください。
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion