External Libraries Integration - Tai-Kimura/SwiftJsonUI GitHub Wiki

External Libraries Integration

This guide explains how to integrate third-party SwiftUI components from SPM libraries into your SwiftJsonUI project using custom converters.

Table of Contents

Overview

SwiftJsonUI allows you to wrap any SwiftUI component from external libraries and make them available in your JSON layouts. This is done by:

  1. Adding the SPM library to your project
  2. Generating a custom converter with sjui g converter
  3. Modifying the generated Swift file to return the external component
  4. Using the component in JSON layouts

Step-by-Step Guide

Step 1: Add the External Library

Add the external library to your Package.swift:

dependencies: [
    .package(url: "https://github.com/Tai-Kimura/SwiftJsonUI.git", from: "7.1.1"),
    .package(url: "https://github.com/example/SwiftUICalendar.git", from: "1.0.0")
]

Step 2: Generate a Custom Converter

Generate the wrapper component with appropriate attributes:

# Example: Wrapping a Calendar component
sjui g converter Calendar --attributes "selectedDate:Date,@onDateSelected:String,showWeekNumbers:Bool"

# Example: Wrapping a Chart component
sjui g converter LineChart --attributes "dataPoints:[Double],lineColor:Color,showGrid:Bool"

Step 3: Modify the Generated Swift File

Edit the generated Swift file (Extensions/Calendar.swift) to use the external component:

import SwiftUI
import SwiftUICalendar  // Import the external library

struct Calendar: View {
    let selectedDate: Date
    @SwiftUI.Binding var onDateSelected: String?
    let showWeekNumbers: Bool
    
    init(
        selectedDate: Date,
        onDateSelected: SwiftUI.Binding<String?>,
        showWeekNumbers: Bool
    ) {
        self.selectedDate = selectedDate
        self._onDateSelected = onDateSelected
        self.showWeekNumbers = showWeekNumbers
    }
    
    var body: some View {
        // Return the external library's component
        CalendarView(
            selectedDate: selectedDate,
            showWeekNumbers: showWeekNumbers,
            onDateSelected: { date in
                // Convert the date to string and update binding
                let formatter = DateFormatter()
                formatter.dateStyle = .medium
                onDateSelected = formatter.string(from: date)
            }
        )
        .frame(height: 350)  // Add any necessary modifiers
    }
}

Step 4: Use in JSON

Now you can use the external component in your JSON layouts:

{
  "type": "Calendar",
  "selectedDate": "2025-08-27",
  "onDateSelected": "@{selectedDateString}",
  "showWeekNumbers": true
}

Real Examples

Example 1: Integrating a Charts Library

1. Generate the converter:

sjui g converter BarChart --attributes "data:[Double],barColor:Color,spacing:Double"

2. Modify Extensions/BarChart.swift:

import SwiftUI
import Charts  // External charts library

struct BarChart: View {
    let data: [Double]
    let barColor: Color
    let spacing: Double
    
    var body: some View {
        // Use the external library's chart component
        ChartView(data: data)
            .barColor(barColor)
            .spacing(spacing)
            .frame(height: 200)
    }
}

3. Use in JSON:

{
  "type": "BarChart",
  "data": [10, 25, 15, 30, 20],
  "barColor": "#007AFF",
  "spacing": 4.0
}

Example 2: Integrating a Loading Indicator Library

1. Generate the converter:

sjui g converter LoadingSpinner --attributes "@isLoading:Bool,size:Double,color:Color"

2. Modify Extensions/LoadingSpinner.swift:

import SwiftUI
import ActivityIndicatorView  // External library

struct LoadingSpinner: View {
    @SwiftUI.Binding var isLoading: Bool
    let size: Double
    let color: Color
    
    var body: some View {
        ActivityIndicatorView(
            isVisible: $isLoading,
            type: .gradient([color, color.opacity(0.5)])
        )
        .frame(width: size, height: size)
    }
}

3. Use in JSON:

{
  "type": "LoadingSpinner",
  "isLoading": "@{isDataLoading}",
  "size": 50,
  "color": "#FF0000"
}

Example 3: Integrating a QR Code Scanner

1. Generate the converter:

sjui g converter QRScanner --attributes "@scannedCode:String,@isScanning:Bool"

2. Modify Extensions/QRScanner.swift:

import SwiftUI
import CodeScanner  // External QR scanner library

struct QRScanner: View {
    @SwiftUI.Binding var scannedCode: String?
    @SwiftUI.Binding var isScanning: Bool
    
    var body: some View {
        if isScanning {
            CodeScannerView(
                codeTypes: [.qr],
                scanMode: .continuous,
                completion: { result in
                    switch result {
                    case .success(let code):
                        scannedCode = code.string
                        isScanning = false
                    case .failure(let error):
                        print("Scanning failed: \(error)")
                        isScanning = false
                    }
                }
            )
        } else {
            Button("Start Scanning") {
                isScanning = true
            }
        }
    }
}

3. Use in JSON:

{
  "type": "QRScanner",
  "scannedCode": "@{qrResult}",
  "isScanning": "@{isScannerActive}"
}

Best Practices

1. Attribute Mapping

  • Map the external component's parameters to SwiftJsonUI attributes
  • Use appropriate types that can be represented in JSON
  • Consider using binding properties for interactive components

2. Type Conversion

When the external library uses types not directly supported in JSON:

struct DatePicker: View {
    let minDate: Date
    let maxDate: Date
    
    init(minDate: Date, maxDate: Date) {
        // Convert from string representation in JSON
        let formatter = ISO8601DateFormatter()
        self.minDate = formatter.date(from: minDate) ?? Date()
        self.maxDate = formatter.date(from: maxDate) ?? Date()
    }
}

3. Error Handling

Add fallbacks for when external components fail:

struct MapView: View {
    let latitude: Double
    let longitude: Double
    
    var body: some View {
        if ExternalMapLibrary.isAvailable {
            ExternalMapView(
                coordinate: .init(latitude: latitude, longitude: longitude)
            )
        } else {
            Text("Map not available")
                .foregroundColor(.secondary)
        }
    }
}

4. Configuration

Pass configuration through the component:

struct RichTextEditor: View {
    let initialText: String
    @SwiftUI.Binding var htmlContent: String?
    
    var body: some View {
        ExternalRichTextView(
            text: initialText,
            configuration: .init(
                toolbar: .full,
                allowImages: true,
                allowLinks: true
            ),
            onContentChange: { html in
                htmlContent = html
            }
        )
    }
}

Advanced Techniques

Handling Callbacks and Events

For components with callbacks, convert them to binding updates:

struct PhotoPicker: View {
    @SwiftUI.Binding var selectedImagePath: String?
    let maxSelections: Int
    
    var body: some View {
        ExternalPhotoPicker(
            maxSelections: maxSelections,
            onSelection: { images in
                // Convert the selection to a path or identifier
                if let firstImage = images.first {
                    selectedImagePath = saveImageAndGetPath(firstImage)
                }
            }
        )
    }
    
    private func saveImageAndGetPath(_ image: UIImage) -> String {
        // Implementation to save image and return path
        // ...
        return "path/to/image.jpg"
    }
}

Wrapping UIKit Components

If the external library provides UIKit components, use UIViewRepresentable:

struct WebView: View {
    let url: String
    @SwiftUI.Binding var isLoading: Bool
    
    var body: some View {
        WebViewRepresentable(
            url: URL(string: url)!,
            isLoading: $isLoading
        )
    }
}

struct WebViewRepresentable: UIViewRepresentable {
    let url: URL
    @Binding var isLoading: Bool
    
    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
        webView.navigationDelegate = context.coordinator
        return webView
    }
    
    func updateUIView(_ webView: WKWebView, context: Context) {
        webView.load(URLRequest(url: url))
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(isLoading: $isLoading)
    }
    
    class Coordinator: NSObject, WKNavigationDelegate {
        @Binding var isLoading: Bool
        
        init(isLoading: Binding<Bool>) {
            _isLoading = isLoading
        }
        
        func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
            isLoading = true
        }
        
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            isLoading = false
        }
    }
}

Testing External Components

Preview Testing

Always test your wrapped components in previews:

#if DEBUG
struct Calendar_Previews: PreviewProvider {
    @State static var selectedDate = "2025-08-27"
    
    static var previews: some View {
        Calendar(
            selectedDate: Date(),
            onDateSelected: $selectedDate,
            showWeekNumbers: true
        )
        .padding()
    }
}
#endif

JSON Testing

Create test JSON files to verify integration:

{
  "type": "SafeAreaView",
  "child": [
    {
      "type": "Label",
      "text": "External Component Test"
    },
    {
      "type": "Calendar",
      "selectedDate": "2025-08-27",
      "onDateSelected": "@{selectedDate}",
      "showWeekNumbers": true
    },
    {
      "type": "Label",
      "text": "Selected: @{selectedDate}"
    }
  ]
}

Troubleshooting

Component Not Rendering

Issue: External component appears blank or doesn't render

Solutions:

  • Check if the library is properly imported
  • Verify the component is initialized correctly
  • Add .frame() modifiers if the component needs explicit sizing
  • Check console for runtime errors from the external library

Type Conversion Issues

Issue: JSON values don't map correctly to library types

Solutions:

  • Add conversion logic in the initializer
  • Use computed properties for complex conversions
  • Consider using String representations and parsing them

Performance Issues

Issue: External component causes performance problems

Solutions:

  • Use lazy loading where possible
  • Cache expensive computations
  • Consider using @StateObject for heavy components
  • Add loading states for async components

Build Errors

Issue: Build fails after adding external library

Solutions:

  • Clean build folder and rebuild
  • Check Package.swift for version conflicts
  • Ensure the library supports your iOS deployment target
  • Verify the library is added to your target's dependencies

See Also

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