Converter Command ja - Tai-Kimura/SwiftJsonUI GitHub Wiki

Converter コマンド (sjui g converter)

sjui g converter コマンドは、Static モードと Dynamic モードの両方で動作するカスタム SwiftUI コンポーネントを生成します。この強力なジェネレーターは、SwiftJsonUI プロジェクトでカスタムコンポーネントを構築・使用するために必要なすべてのファイルを作成します。

目次

概要

converter ジェネレーターは、連携して動作する3つの必須ファイルを作成します:

  1. Swift コンポーネント - 実際の SwiftUI View
  2. Ruby コンバーター - Static モード用に JSON を Swift コードに変換
  3. Dynamic アダプター - ホットリロード付きの Dynamic モードを有効化

これは SwiftJsonUI をカスタムで再利用可能なコンポーネントで拡張する主要な方法です。

基本的な使い方

シンプルなコンポーネント

sjui g converter MyButton

属性付きコンポーネント

sjui g converter MyCard --attributes "title:String,subtitle:String,imageUrl:String"

コンテナコンポーネント

sjui g converter MyContainer --container

非コンテナコンポーネント

sjui g converter MyBadge --no-container

コマンドオプション

--attributes (-a)

コンポーネントのプロパティを定義します。複数の属性は、カンマ区切りの単一文字列として指定するか、オプションを複数回使用して指定できます。

形式: "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 の区別をサポート)

バインディングプロパティ(7.1.1 の新機能)

属性名の先頭に @ を付けることで、親ビューから変更可能な 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              // 静的な値
}

バインディングプロパティの動作:

  1. 生成時: @ で始まるプロパティは @SwiftUI.Binding 変数を生成
  2. JSON 内: @{propertyName} 構文で ViewModel データプロパティにバインド
  3. Static モード: コンバーターがバインディング用に $viewModel.data.propertyName を生成
  4. Dynamic モード: アダプターが ViewModel から適切な SwiftUI バインディングを作成

バインディングの使用例:

  • 編集可能フィールドの双方向データバインディング
  • 親と子コンポーネント間での状態共有
  • データ変更時のリアクティブ UI 更新
  • 親の状態を変更するフォーム入力

--container

コンポーネントを子コンポーネントを持てるコンテナとして明示的にマークします。

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" 配列で指定

--no-container

コンポーネントを非コンテナ(子要素を持てないリーフコンポーネント)として明示的にマークします。

sjui g converter StatusBadge --no-container

生成される Swift シグネチャ:

struct StatusBadge: View {
    // content パラメータなし、属性パラメータのみ
}

動作:

  • コンポーネントは JSON の "child""children" を無視
  • ジェネリック型パラメータは生成されない
  • 子要素が不要なコンポーネント用のシンプルな実装
  • バッジ、アイコン、ステータスインジケーターなどのアトミックコンポーネントに便利

自動検出(デフォルト)

--container--no-container も指定されない場合:

  • コンバーターは JSON の "child" または "children" の存在に基づいて自動検出
  • オプショナルなコンテンツサポートでコンポーネントが生成される
  • 最も柔軟なオプションだが、生成されるコードがやや複雑になる可能性がある

--no-default-attributes

デフォルト属性の使用を無効にします(高度なオプション、通常は不要)。

sjui g converter MyComponent --no-default-attributes

動作:

  • デフォルトでは、コンバーターは BaseViewConverter から共通モディファイアを継承
  • このオプションはデフォルトモディファイアサポートなしの最小限のコンバーターを作成
  • すべてのコンポーネント動作を完全に制御する必要がある場合にのみ使用

--force (-f)

確認プロンプトなしで既存のファイルを上書きします。

sjui g converter MyComponent --force

動作:

  • 既存ファイルの「上書きしますか? (y/n)」プロンプトをスキップ
  • スクリプティングやコンポーネントの再生成に便利
  • 注意: 既存ファイルのカスタマイズが上書きされる

生成されるファイル

1. Swift コンポーネントファイル

場所: 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 を作成してください。

2. Ruby コンバーターファイル

場所: 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 属性を処理したり、カスタムロジックを追加するために修正できます。

3. Dynamic アダプターファイル

場所: 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 ビルドでのみコンパイルされます。

例1: シンプルなバッジコンポーネント

sjui g converter NotificationBadge --attributes "count:Int,color:Color" --no-container

JSON での使用:

{
  "type": "NotificationBadge",
  "count": 5,
  "color": "#FF0000"
}

例2: 子要素を持つカスタムカード

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"
    }
  ]
}

例3: 複雑なコンテナ

sjui g converter Dashboard --attributes "headerTitle:String,showStats:Bool" --container

JSON での使用:

{
  "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.rb

コンポーネントマッピングで自動更新:

CONVERTER_MAPPINGS = {
  'MyCard' => 'MyCardConverter',
  'MyButton' => 'MyButtonConverter',
  # ... 自動的に管理される
}.freeze

CustomComponentRegistration.swift

アダプター登録で自動更新:

let adapters: [CustomComponentAdapter] = [
    MyCardAdapter(),
    MyButtonAdapter(),
    // ... 自動的に管理される
]

ベストプラクティス

1. 命名規則

  • コンポーネント名には PascalCase を使用
  • 説明的な名前を付ける: Card1 ではなく UserProfileCard
  • iOS システムコンポーネント名は避ける

2. 属性設計

  • 属性はシンプルで焦点を絞ったものにする
  • 適切な型を使用(フラグには Bool、テキストには String)
  • Swift 実装でデフォルト値を検討

3. コンテナ vs 非コンテナ

  • 不明な場合はコンテナをデフォルトに(より柔軟)
  • リーフコンポーネント(バッジ、アイコン)には非コンテナを使用
  • コンテナコンポーネントは明確なコンテンツエリアを定義すべき

4. カスタマイズワークフロー

  1. コンポーネントを生成
  2. 基本的な JSON でテスト
  3. 外観のために Swift ファイルをカスタマイズ
  4. 特殊な属性が必要な場合はコンバーターを調整
  5. Static と Dynamic の両モードでテスト

5. パフォーマンス

  • アダプター実装は軽量に保つ
  • 重いロジックは Swift コンポーネントに記述
  • #if DEBUG を適切に使用

トラブルシューティング

コンポーネントが見つからない

エラー: "Unknown component type: MyComponent"

解決策:

  • converter_mappings.rb にコンポーネントが含まれていることを確認
  • sjui build --clean を実行して再生成
  • ファイルが正しい場所に生成されているか確認

Dynamic モードが動作しない

エラー: Dynamic モードでコンポーネントが不明として表示される

解決策:

  • adapter_directory にアダプターファイルが存在することを確認
  • CustomComponentRegistration.swift にアダプターが含まれているか確認
  • CustomComponentRegistration.registerAll() が呼ばれていることを確認
  • DEBUG ビルドであることを確認

属性が表示されない

問題: JSON のカスタム属性がコンポーネントに影響しない

Static モードの場合:

  • コンバーター .rb ファイルが属性を処理しているか確認
  • format_value が型を正しく処理しているか確認

Dynamic モードの場合:

  • アダプターが component.rawData 経由で属性にアクセスしているか確認
  • 属性名が完全に一致することを確認(大文字小文字を区別)

ビルドエラー

エラー: 生成後の Swift コンパイルエラー

解決策:

  • 生成された Swift 構文が有効か確認
  • 属性のすべての型がサポートされていることを確認
  • 属性定義のタイプミスを探す
  • import 文が存在することを確認

高度なトピック

カスタム型の処理

基本を超えたカスタム型の場合:

  1. Swift で型を定義:
struct MyCustomType {
    let value: String
}
  1. コンバーターで処理:
def format_value(value, type)
  case type
  when 'MyCustomType'
    # カスタムパース処理
    "MyCustomType(value: \"#{value}\")"
  else
    super
  end
end
  1. アダプターでパース:
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)
    }
}

関連項目

⚠️ **GitHub.com Fallback** ⚠️