SwiftUI Integration

This guide shows you how to integrate the Anyline SDK with SwiftUI applications. The Anyline SDK is built with UIKit, but can be seamlessly integrated into SwiftUI using UIViewRepresentable.

Prerequisites

  • Anyline SDK properly set up in your iOS project (see Getting Started)

  • Basic familiarity with SwiftUI

  • iOS 13.0+ (required for SwiftUI)

Basic Integration

Step 1: Create the SwiftUI Wrapper

Create a struct that conforms to UIViewRepresentable to wrap the Anyline ALScanView:

import SwiftUI
import Anyline

struct AnylineScanView: UIViewRepresentable {
    @Binding var scanResult: ALScanResult?
    let configFileName: String

    func makeUIView(context: Context) -> ALScanView {
        // Load configuration
        guard let config = loadScanViewConfig(configFileName) else {
            fatalError("Could not load scan configuration")
        }

        // Create scan view
        do {
            let scanView = try ALScanView(frame: .zero, scanViewConfig: config)

            // Set up delegate
            if let plugin = scanView.viewPlugin as? ALScanViewPlugin {
                plugin.scanPlugin.delegate = context.coordinator
            }

            // Start scanning
            scanView.startCamera()
            try scanView.startScanning()

            return scanView
        } catch {
            fatalError("Failed to create scan view: \(error)")
        }
    }

    func updateUIView(_ uiView: ALScanView, context: Context) {
        // Handle updates if needed
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
}

Step 2: Create the Coordinator

The Coordinator handles scan results and acts as the delegate:

extension AnylineScanView {
    class Coordinator: NSObject, ALScanPluginDelegate {
        let parent: AnylineScanView

        init(_ parent: AnylineScanView) {
            self.parent = parent
        }

        func scanPlugin(_ scanPlugin: ALScanPlugin, resultReceived scanResult: ALScanResult) {
            DispatchQueue.main.async {
                self.parent.scanResult = scanResult
            }
        }
    }
}

Step 3: Create the Configuration Loader

Add a helper function to load your scan configurations:

extension AnylineScanView {
    private func loadScanViewConfig(_ fileName: String) -> ALScanViewConfig? {
        guard let url = Bundle.main.url(forResource: fileName, withExtension: "json"),
              let data = try? Data(contentsOf: url),
              let jsonString = String(data: data, encoding: .utf8) else {
            print("Error: Could not load config file \(fileName).json")
            return nil
        }

        do {
            return try ALScanViewConfig.withJSONString(jsonString)
        } catch {
            print("Error parsing config: \(error)")
            return nil
        }
    }
}

Step 4: Use in Your SwiftUI View

Now you can use the scanner in any SwiftUI view:

struct ContentView: View {
    @State private var scanResult: ALScanResult?

    var body: some View {
        ZStack {
            // Scanner view
            AnylineScanView(
                scanResult: $scanResult,
                configFileName: "barcode_config"
            )
            .ignoresSafeArea()

            // Results overlay
            if let result = scanResult {
                VStack {
                    Spacer()
                    Text("Scanned: \(result.asJSONStringPretty(true))")
                        .padding()
                        .background(Color.black.opacity(0.7))
                        .foregroundColor(.white)
                        .cornerRadius(10)
                        .padding()
                }
            }
        }
    }
}

Common Pitfalls and Solutions

Camera and Scanning Lifecycle

Problem: The camera and scanning continue running even when the view is not visible, causing battery drain and potential crashes.

Solution: Stop the camera and scanning when appropriate. Consider implementing lifecycle management through SwiftUI modifiers:

AnylineScanView(scanResult: $scanResult, configFileName: "barcode_config")
    .onAppear {
        // Scanner starts automatically in makeUIView
    }
    .onDisappear {
        // You may need additional lifecycle management here
    }

Error Handling

Problem: Using fatalError() will crash your app if configuration loading fails.

Better approach: Return an error view instead:

func makeUIView(context: Context) -> UIView {
    guard let config = loadScanViewConfig(configFileName) else {
        return createErrorView("Configuration not found")
    }

    do {
        let scanView = try ALScanView(frame: .zero, scanViewConfig: config)
        // ... setup code
        return scanView
    } catch {
        return createErrorView("Scanner initialization failed: \(error.localizedDescription)")
    }
}

private func createErrorView(_ message: String) -> UIView {
    let label = UILabel()
    label.text = message
    label.textAlignment = .center
    label.textColor = .red
    return label
}

Problem: Rapid navigation between views can cause memory leaks or scanner conflicts.

Solution: Ensure proper cleanup in your coordinator’s deinit:

class Coordinator: NSObject, ALScanPluginDelegate {
    // ... existing code

    deinit {
        // Cleanup if needed
        print("Coordinator deallocated")
    }
}

Dynamic Configuration Changes

To change scanner configuration at runtime:

struct DynamicScanView: View {
    @State private var currentConfig = "barcode_config"
    @State private var scanResult: ALScanResult?

    var body: some View {
        VStack {
            // Configuration selector
            Picker("Scan Mode", selection: $currentConfig) {
                Text("Barcode").tag("barcode_config")
                Text("MRZ").tag("mrz_config")
            }
            .pickerStyle(SegmentedPickerStyle())
            .padding()

            // Scanner (recreated when config changes)
            AnylineScanView(
                scanResult: $scanResult,
                configFileName: currentConfig
            )
            .id(currentConfig) // Forces recreation when config changes
        }
    }
}

Using .id(currentConfig) forces SwiftUI to recreate the view when the configuration changes. This is necessary because the scanner needs to be reinitialized with the new configuration.

Troubleshooting

Scanner Not Starting

Symptoms: Black screen, no camera preview

Possible causes:

  • Missing camera permissions

  • Invalid configuration file

  • License key issues

Solutions:

  1. Check camera permissions in your Info.plist:

    <key>NSCameraUsageDescription</key>
    <string>This app uses the camera to scan documents</string>
  2. Verify your configuration file is included in your app bundle

  3. Check console logs for Anyline initialization errors

Memory Leaks

Symptoms: App memory usage increases over time

Solutions:

  • Ensure proper delegate cleanup

  • Avoid strong reference cycles between coordinator and parent

  • Test navigation patterns thoroughly

Preview Issues

Symptoms: Xcode previews crash or don’t work

Solutions:

  • Use conditional compilation for previews:

    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            #if targetEnvironment(simulator)
            Text("Scanner not available in preview")
            #else
            ContentView()
            #endif
        }
    }

Scanner Stops Working After Navigation

Symptoms: Scanner works initially but stops after navigating away and back

Solutions:

  • This is often caused by the view being deallocated and recreated

  • Consider using .id() modifier to force proper recreation

  • Ensure your configuration loading is robust

Best Practices

Configuration Management: Store your scan configurations as JSON files in your app bundle. This makes them easy to modify without rebuilding your app.

State Management: Use @State or @ObservedObject to manage scan results and configuration changes reactively.

Error Handling: Always handle configuration loading and scanner initialization errors gracefully to prevent app crashes.

Testing: Test your scanner integration thoroughly on physical devices, as camera functionality doesn’t work in the simulator.

Advanced Integration

For production applications that require more sophisticated lifecycle management, camera control, or navigation integration, consider using UIViewControllerRepresentable instead of UIViewRepresentable. This approach provides:

  • Automatic camera start/stop based on view controller lifecycle

  • Better memory management

  • More robust navigation integration

  • Explicit control over scanner state

This advanced approach requires creating a dedicated UIViewController subclass to manage the scanner, but provides much better resource management for complex applications.