Converter Command ja - Tai-Kimura/SwiftJsonUI GitHub Wiki
sjui g converter コマンドは、Static モードと Dynamic モードの両方で動作するカスタム SwiftUI コンポーネントを生成します。この強力なジェネレーターは、SwiftJsonUI プロジェクトでカスタムコンポーネントを構築・使用するために必要なすべてのファイルを作成します。
converter ジェネレーターは、連携して動作する3つの必須ファイルを作成します:
- Swift コンポーネント - 実際の SwiftUI View
- Ruby コンバーター - Static モード用に JSON を Swift コードに変換
- Dynamic アダプター - ホットリロード付きの Dynamic モードを有効化
これは SwiftJsonUI をカスタムで再利用可能なコンポーネントで拡張する主要な方法です。
sjui g converter MyButtonsjui g converter MyCard --attributes "title:String,subtitle:String,imageUrl:String"sjui g converter MyContainer --containersjui g converter MyBadge --no-containerコンポーネントのプロパティを定義します。複数の属性は、カンマ区切りの単一文字列として指定するか、オプションを複数回使用して指定できます。
形式: "name:Type,name:Type,..." または複数の --attributes name:Type
サポートされる型:
-
String- テキスト値(Swift の String 型) -
Int/Integer- 整数(Swift の Int 型) -
Double/Float- 小数(Swift の Double 型) -
Bool/Boolean- 真偽値(Swift の Bool 型) -
Color- カラー値(SwiftUI の Color 型、JSON では "#FF0000" のような16進文字列を受け付ける) -
EdgeInsets- パディング/マージン値(SwiftUI の EdgeInsets 型)
例:
# カンマ区切りの単一文字列
sjui g converter ProfileCard --attributes "name:String,age:Int,verified:Bool,avatarColor:Color"
# 複数の --attributes オプション
sjui g converter ProfileCard \
--attributes name:String \
--attributes age:Int \
--attributes verified:Bool \
--attributes avatarColor:Color属性の動作:
- 各属性は Swift コンポーネントのイニシャライザーのパラメータになる
- Ruby コンバーターが各型の JSON から Swift への変換を自動的に処理
- Dynamic アダプターは
component.rawData["attributeName"]で属性にアクセス - Boolean 属性は
@component.key?('attributeName')で存在を検出(nil/false の区別をサポート)
属性名の先頭に @ を付けることで、親ビューから変更可能な SwiftUI バインディングプロパティを作成できます。
形式: "@propertyName:Type"
例:
# バインディングプロパティを持つコンポーネントを生成
sjui g converter UserProfile --attributes "@user:User,@isEditing:Bool,showDetails:Bool"生成される Swift コンポーネント:
struct UserProfile<Content: View>: View {
@SwiftUI.Binding var user: User
@SwiftUI.Binding var isEditing: Bool
let showDetails: Bool // 通常のプロパティ(バインディングではない)
let content: Content?
init(
user: SwiftUI.Binding<User>,
isEditing: SwiftUI.Binding<Bool>,
showDetails: Bool,
@ViewBuilder content: () -> Content = { EmptyView() }
) {
self._user = user
self._isEditing = isEditing
self.showDetails = showDetails
self.content = content()
}
}JSON での使用:
{
"type": "UserProfile",
"user": "@{currentUser}", // viewModel.data.currentUser へのバインディング
"isEditing": "@{editMode}", // viewModel.data.editMode へのバインディング
"showDetails": true // 静的な値
}バインディングプロパティの動作:
-
生成時:
@で始まるプロパティは@SwiftUI.Binding変数を生成 -
JSON 内:
@{propertyName}構文で ViewModel データプロパティにバインド -
Static モード: コンバーターがバインディング用に
$viewModel.data.propertyNameを生成 - Dynamic モード: アダプターが ViewModel から適切な SwiftUI バインディングを作成
バインディングの使用例:
- 編集可能フィールドの双方向データバインディング
- 親と子コンポーネント間での状態共有
- データ変更時のリアクティブ UI 更新
- 親の状態を変更するフォーム入力
コンポーネントを子コンポーネントを持てるコンテナとして明示的にマークします。
sjui g converter CardStack --container生成される Swift シグネチャ:
struct CardStack<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
}動作:
- 属性が定義されていなくても、コンポーネントが子要素を受け入れるよう強制
- ジェネリック
Content: View型パラメータを生成 - SwiftUI DSL サポートのため
@ViewBuilderを使用 - JSON では、子要素は
"child"または"children"配列で指定
コンポーネントを非コンテナ(子要素を持てないリーフコンポーネント)として明示的にマークします。
sjui g converter StatusBadge --no-container生成される Swift シグネチャ:
struct StatusBadge: View {
// content パラメータなし、属性パラメータのみ
}動作:
- コンポーネントは JSON の
"child"や"children"を無視 - ジェネリック型パラメータは生成されない
- 子要素が不要なコンポーネント用のシンプルな実装
- バッジ、アイコン、ステータスインジケーターなどのアトミックコンポーネントに便利
--container も --no-container も指定されない場合:
- コンバーターは JSON の
"child"または"children"の存在に基づいて自動検出 - オプショナルなコンテンツサポートでコンポーネントが生成される
- 最も柔軟なオプションだが、生成されるコードがやや複雑になる可能性がある
デフォルト属性の使用を無効にします(高度なオプション、通常は不要)。
sjui g converter MyComponent --no-default-attributes動作:
- デフォルトでは、コンバーターは BaseViewConverter から共通モディファイアを継承
- このオプションはデフォルトモディファイアサポートなしの最小限のコンバーターを作成
- すべてのコンポーネント動作を完全に制御する必要がある場合にのみ使用
確認プロンプトなしで既存のファイルを上書きします。
sjui g converter MyComponent --force動作:
- 既存ファイルの「上書きしますか? (y/n)」プロンプトをスキップ
- スクリプティングやコンポーネントの再生成に便利
- 注意: 既存ファイルのカスタマイズが上書きされる
場所: Extensions/MyComponent.swift
目的: 実際の SwiftUI View の実装
例:
import SwiftUI
struct MyCard<Content: View>: View {
let title: String
let subtitle: String
let content: Content?
init(
title: String,
subtitle: String,
@ViewBuilder content: () -> Content = { EmptyView() }
) {
self.title = title
self.subtitle = subtitle
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.headline)
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
if let content = content {
content
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
}カスタマイズ: このファイルは編集することを想定しています!body を修正して希望の UI を作成してください。
場所: sjui_tools/lib/swiftui/views/extensions/my_component_converter.rb
目的: Static モードのコンパイル中に JSON を Swift コードに変換
例:
class MyCardConverter < BaseViewConverter
def convert
is_container = (@component['children'] && !@component['children'].empty?) ||
(@component['child'] && !@component['child'].empty?)
params = []
if @component['title']
formatted_value = format_value(@component['title'], 'String')
params << "title: #{formatted_value}" if formatted_value
end
if @component['subtitle']
formatted_value = format_value(@component['subtitle'], 'String')
params << "subtitle: #{formatted_value}" if formatted_value
end
if is_container
add_line "MyCard("
indent do
params.each_with_index do |param, index|
add_line index == params.length - 1 ? param : "#{param},"
end
end
add_line ") {"
indent do
process_children
end
add_line "}"
else
# 非コンテナの実装
add_line "MyCard("
indent do
params.each_with_index do |param, index|
add_line index == params.length - 1 ? param : "#{param},"
end
end
add_line ")"
end
apply_modifiers
generated_code
end
endカスタマイズ: 特殊な JSON 属性を処理したり、カスタムロジックを追加するために修正できます。
場所: Extensions/Adapters/MyComponentAdapter.swift
目的: ホットリロード付きの Dynamic モードサポートを有効化
例:
#if DEBUG
import SwiftUI
import SwiftJsonUI
struct MyCardAdapter: CustomComponentAdapter {
var componentType: String { "MyCard" }
func buildView(
component: DynamicComponent,
viewModel: DynamicViewModel,
viewId: String?,
parentOrientation: String?
) -> AnyView {
// raw JSON データから属性を抽出
let title = component.rawData["title"] as? String ?? ""
let subtitle = component.rawData["subtitle"] as? String ?? ""
// 子要素からコンテンツを構築
let content = VStack(alignment: .leading, spacing: 0) {
if let children = component.childComponents {
ForEach(Array(children.enumerated()), id: \.offset) { _, child in
DynamicComponentBuilder(
component: child,
viewModel: viewModel,
viewId: viewId,
isWeightedChild: false,
parentOrientation: "vertical"
)
}
}
}
return AnyView(
MyCard(
title: title,
subtitle: subtitle
) {
content
}
.modifier(CommonModifiers(component: component, viewModel: viewModel))
)
}
}
#endif注意: 最適なリリースパフォーマンスのため、DEBUG ビルドでのみコンパイルされます。
sjui g converter NotificationBadge --attributes "count:Int,color:Color" --no-containerJSON での使用:
{
"type": "NotificationBadge",
"count": 5,
"color": "#FF0000"
}sjui g converter FeatureCard --attributes "icon:String,title:String,description:String"JSON での使用:
{
"type": "FeatureCard",
"icon": "star.fill",
"title": "プレミアム機能",
"description": "高度な機能をアンロック",
"child": [
{
"type": "Button",
"text": "詳細を見る",
"onclick": "showDetails"
}
]
}sjui g converter Dashboard --attributes "headerTitle:String,showStats:Bool" --containerJSON での使用:
{
"type": "Dashboard",
"headerTitle": "アナリティクス",
"showStats": true,
"child": [
{
"type": "View",
"orientation": "horizontal",
"child": [
{ "type": "Label", "text": "総ユーザー数: 1,234" },
{ "type": "Label", "text": "アクティブ: 567" }
]
},
{
"type": "MyChart",
"data": "@{chartData}"
}
]
}sjui.config.json に追加:
{
"extension_directory": "Extensions",
"adapter_directory": "Extensions/Adapters"
}生成されたコンポーネントは自動的に登録されますが、アプリの初期化時に以下を呼び出してください:
#if DEBUG
// App.swift または AppDelegate で
CustomComponentRegistration.registerAll()
#endifコンポーネントマッピングで自動更新:
CONVERTER_MAPPINGS = {
'MyCard' => 'MyCardConverter',
'MyButton' => 'MyButtonConverter',
# ... 自動的に管理される
}.freezeアダプター登録で自動更新:
let adapters: [CustomComponentAdapter] = [
MyCardAdapter(),
MyButtonAdapter(),
// ... 自動的に管理される
]- コンポーネント名には PascalCase を使用
- 説明的な名前を付ける:
Card1ではなくUserProfileCard - iOS システムコンポーネント名は避ける
- 属性はシンプルで焦点を絞ったものにする
- 適切な型を使用(フラグには Bool、テキストには String)
- Swift 実装でデフォルト値を検討
- 不明な場合はコンテナをデフォルトに(より柔軟)
- リーフコンポーネント(バッジ、アイコン)には非コンテナを使用
- コンテナコンポーネントは明確なコンテンツエリアを定義すべき
- コンポーネントを生成
- 基本的な JSON でテスト
- 外観のために Swift ファイルをカスタマイズ
- 特殊な属性が必要な場合はコンバーターを調整
- Static と Dynamic の両モードでテスト
- アダプター実装は軽量に保つ
- 重いロジックは Swift コンポーネントに記述
-
#if DEBUGを適切に使用
エラー: "Unknown component type: MyComponent"
解決策:
- converter_mappings.rb にコンポーネントが含まれていることを確認
-
sjui build --cleanを実行して再生成 - ファイルが正しい場所に生成されているか確認
エラー: Dynamic モードでコンポーネントが不明として表示される
解決策:
- adapter_directory にアダプターファイルが存在することを確認
- CustomComponentRegistration.swift にアダプターが含まれているか確認
-
CustomComponentRegistration.registerAll()が呼ばれていることを確認 - DEBUG ビルドであることを確認
問題: JSON のカスタム属性がコンポーネントに影響しない
Static モードの場合:
- コンバーター .rb ファイルが属性を処理しているか確認
- format_value が型を正しく処理しているか確認
Dynamic モードの場合:
- アダプターが component.rawData 経由で属性にアクセスしているか確認
- 属性名が完全に一致することを確認(大文字小文字を区別)
エラー: 生成後の Swift コンパイルエラー
解決策:
- 生成された Swift 構文が有効か確認
- 属性のすべての型がサポートされていることを確認
- 属性定義のタイプミスを探す
- import 文が存在することを確認
基本を超えたカスタム型の場合:
- Swift で型を定義:
struct MyCustomType {
let value: String
}- コンバーターで処理:
def format_value(value, type)
case type
when 'MyCustomType'
# カスタムパース処理
"MyCustomType(value: \"#{value}\")"
else
super
end
end- アダプターでパース:
let customValue = component.rawData["customField"] as? [String: Any]
let myType = MyCustomType(value: customValue?["value"] as? String ?? "")条件付き子要素のレンダリング処理:
def process_children
child_array = @component['child'] || []
# 条件に基づいてフィルター
child_array.each do |child|
next if child['platform'] && child['platform'] != 'ios'
child_converter = @factory.create_converter(child, @indent_level, @action_manager, @factory, @registry)
@generated_code.concat(child_converter.convert.split("\n"))
end
endステートフルコンポーネントでは、Swift で @State を使用:
struct MyComponent: View {
let initialValue: Int
@State private var currentValue: Int
init(initialValue: Int) {
self.initialValue = initialValue
self._currentValue = State(initialValue: initialValue)
}
}- SwiftJsonUI-7.1.0-Release-Notes-ja - カスタムコンポーネントの紹介
- CLI-Commands - 完全な CLI リファレンス
- Advanced-Features - 高度な SwiftJsonUI 機能
- Data-Binding - カスタムコンポーネントでのデータバインディングの使用