📑

Fragment Colocation導入に向けたXcodeカスタムテンプレートの作成

2024/12/27に公開

こんにちは、スペースマーケットモバイルエンジニアの村田です。

以前書いた記事([スペマiOS]ログイン/新規登録リニューアル)で触れた通り、弊社のiOSプロジェクトではデザインリニューアルや新規画面の作成時に、UIKitではなく積極的にSwiftUIを選択しています。SwiftUIへの移行を進める上でPresentaion層の設計を見直し、GraphQLの「Fragment Colocation」というデザインパターンを導入しました。

導入にあたり、1つのコンポーネントに関与する要素が増えることにより、コンポーネント追加時に必要となるボイラープレートコードの記述が手間になってきました。そこで、Xcodeのカスタムテンプレートを活用することで、コンポーネント作成時のファイル追加およびボイラープレートコード記述の手間を軽減しました。

本記事では、Fragment Colocation導入前後の構成の違いと、カスタムテンプレートの活用方法について紹介します。

Fragment Colocation

Colocate(同一場所に設置する)の意味通り、UI Componentが必要としているFragment(データ)を同じ場所に配置する手法です(図がシンプルで分かりやすかったので以下添付させていただきした🙏)

https://speakerdeck.com/chloe463/graphql-fragment-colocation-nohua?slide=25

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ファイル雛形)

各ファイルに関して

TemplateInfo.plist

プロパティの設定項目については、以下の記事にわかりやすくまとめられており、参考にさせていただきました!画面名と子コンポーネント名を入力し、各ファイル名やクラス名が自動で決定されるように設計しました。

https://note.com/taatn0te/n/nb44ed6ebf6bf

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

___VARIABLE_parent___Screen+___VARIABLE_child___.swift
//___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

___VARIABLE_parent______VARIABLE_child___Model.swift
//___FILEHEADER___

final class ___VARIABLE_parent______VARIABLE_child___Model: ObservableObject {

    init(fragment: APIV2.___VARIABLE_parent______VARIABLE_child___) {
    }

    // MARK: - Output

    // MARK: - Input

    // MARK: - Internal
}

graphql

___VARIABLE_parent______VARIABLE_child___.graphql
fragment ___VARIABLE_parent______VARIABLE_child___ {
}

カスタムテンプレートによる子コンポーネント作成

作成したテンプレートを利用し、例としてHogeScreenへFugaViewコンポーネントを追加してみます。

親となる HogeScreenFugaView コンポーネントを追加したいので、まずFugaディレクトリを用意。その後、ディレクトリを右クリックし、[New File from Template...]を選択。

テンプレート選択のウィンドウが表示され、最下部までスクロールすると追加したカテゴリ(SpaceMarket)とテンプレート(Child Component)が確認できます。

Child Componentテンプレートを選択すると、設定した通り、画面名と子コンポーネント名の入力が求められます。「Screen Name」には画面名となるHoge、「Child Component Name」には追加対象であるFugaViewを入力。

Fugaフォルダ直下にView/ViewModel/graphqlの各ファイルが作成され、テンプレート通りコードが生成されていることを確認できました🎉

最後に

スペースマーケットでは一緒に働く仲間を募集中です!
詳しくは以下採用ページをご確認ください。

https://spacemarket.co.jp/recruit/engineer/

https://herp.careers/v1/spmhr/f8x6AkIueBSb

https://herp.careers/v1/spmhr/9zYSnsOQ0UMA

GitHubで編集を提案
スペースマーケット Engineer Blog

Discussion