Skip to main content
Code Screens enable you to seamlessly integrate custom native SwiftUI views into your remotely configured Muta flows. This powerful feature allows you to combine the flexibility of no-code flow management with the full capabilities of native development.

Overview

Code Screens act as placeholders in your Muta flows that are replaced with native SwiftUI views at runtime. This enables you to:
  • Add authentication screens to your onboarding flows
  • Integrate payment processing directly in your flows
  • Include complex native UI that requires custom logic
  • Maintain remote configurability while using native code

How It Works

1

Create a Code Screen

In the Muta web editor, add a “Code Screen” to your flow and give it a unique name (e.g., “Login Screen”, “Payment Form”)
2

Configure Navigation

Set up the continue and back navigation destinations for your code screen
3

Inject Components

Pass your SwiftUI views when displaying the placement in your app

Implementation

Basic Setup

Pass your custom views when calling displayPlacement:
import MutaSDK

Muta.shared.displayPlacement(
    placementId: "onboarding",
    backgroundColor: .black,
    presentationType: .none,
    injectedScreens: [
        InjectedScreen(
            screenName: "Login Screen",  // Must match the name in Muta editor
            view: { context in
                AnyView(LoginScreen(context: context))
            }
        ),
        InjectedScreen(
            screenName: "Payment Form",
            view: { context in
                AnyView(PaymentScreen(context: context))
            }
        )
    ]
)

Component Interface

Your injected views receive a CodeScreenContext with the following properties:
public struct CodeScreenContext {
    /// Navigate to the next screen configured in the flow
    public let onContinue: () -> Void

    /// Navigate to the previous screen configured in the flow
    public let onBack: () -> Void

    /// Current flow variables
    public let variables: [[String: Any]]

    /// Unique identifier for this screen
    public let screenId: String

    /// Position of this screen in the flow
    public let screenIndex: Int
}

Example Component

Here’s a complete example of a login screen component:
import SwiftUI
import MutaSDK

struct LoginScreen: View {
    let context: CodeScreenContext

    @State private var email = ""
    @State private var password = ""
    @State private var isLoading = false
    @State private var errorMessage = ""

    var body: some View {
        VStack(spacing: 24) {
            Spacer()

            // Title
            VStack(spacing: 8) {
                Text("Welcome Back")
                    .font(.system(size: 32, weight: .bold))
                    .foregroundColor(.white)

                Text("Sign in to continue")
                    .font(.system(size: 16))
                    .foregroundColor(.white.opacity(0.7))
            }

            Spacer()

            // Form
            VStack(spacing: 16) {
                // Email field
                VStack(alignment: .leading, spacing: 8) {
                    Text("Email")
                        .font(.system(size: 14, weight: .medium))
                        .foregroundColor(.white.opacity(0.7))

                    TextField("your.email@example.com", text: $email)
                        .textFieldStyle(.plain)
                        .textInputAutocapitalization(.never)
                        .keyboardType(.emailAddress)
                        .autocorrectionDisabled()
                        .padding()
                        .background(Color.white.opacity(0.1))
                        .foregroundColor(.white)
                        .cornerRadius(12)
                }

                // Password field
                VStack(alignment: .leading, spacing: 8) {
                    Text("Password")
                        .font(.system(size: 14, weight: .medium))
                        .foregroundColor(.white.opacity(0.7))

                    SecureField("Enter your password", text: $password)
                        .textFieldStyle(.plain)
                        .padding()
                        .background(Color.white.opacity(0.1))
                        .foregroundColor(.white)
                        .cornerRadius(12)
                }

                // Error message
                if !errorMessage.isEmpty {
                    Text(errorMessage)
                        .font(.system(size: 14))
                        .foregroundColor(.red)
                }
            }
            .padding(.horizontal, 24)

            Spacer()

            // Buttons
            VStack(spacing: 12) {
                // Login button
                Button(action: handleLogin) {
                    HStack {
                        if isLoading {
                            ProgressView()
                                .progressViewStyle(CircularProgressViewStyle(tint: .white))
                        } else {
                            Text("Sign In")
                                .font(.system(size: 16, weight: .semibold))
                        }
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(12)
                }
                .disabled(isLoading || email.isEmpty || password.isEmpty)
                .opacity((email.isEmpty || password.isEmpty) ? 0.5 : 1.0)

                // Back button
                Button(action: {
                    context.onBack()
                }) {
                    Text("Back")
                        .font(.system(size: 16, weight: .medium))
                        .frame(maxWidth: .infinity)
                        .padding()
                        .foregroundColor(.white)
                }
                .disabled(isLoading)
            }
            .padding(.horizontal, 24)
            .padding(.bottom, 32)
        }
        .background(Color.black)
    }

    private func handleLogin() {
        errorMessage = ""
        isLoading = true

        // Simulate login API call
        Task {
            do {
                try await performLogin(email: email, password: password)

                // Navigate to next screen in flow
                await MainActor.run {
                    isLoading = false
                    context.onContinue()
                }
            } catch {
                await MainActor.run {
                    isLoading = false
                    errorMessage = "Invalid email or password"
                }
            }
        }
    }

    private func performLogin(email: String, password: String) async throws {
        // Your authentication logic here
        let url = URL(string: "https://api.example.com/login")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let body = ["email": email, "password": password]
        request.httpBody = try JSONSerialization.data(withJSONObject: body)

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw NSError(domain: "LoginError", code: 401)
        }

        // Store auth token, update user state, etc.
        if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
           let token = json["token"] as? String {
            UserDefaults.standard.set(token, forKey: "authToken")
        }
    }
}

Animations

Code screens automatically animate in and out matching your flow’s configured animations:
  • Entry Animation: Matches the animation used when navigating TO the code screen
  • Exit Animation: Uses the configured continue/back animation settings
  • Seamless Transitions: Native screens blend smoothly with your flow screens

Use Cases

Authentication Flows

Integrate login, signup, or authentication screens directly into your onboarding:
injectedScreens: [
    InjectedScreen(screenName: "Login", view: { AnyView(LoginScreen(context: $0)) }),
    InjectedScreen(screenName: "Signup", view: { AnyView(SignupScreen(context: $0)) }),
    InjectedScreen(screenName: "Verify Email", view: { AnyView(VerifyEmailScreen(context: $0)) })
]

Payment Integration

Add payment processing without breaking the flow experience:
injectedScreens: [
    InjectedScreen(screenName: "Payment Method", view: { AnyView(PaymentMethodScreen(context: $0)) }),
    InjectedScreen(screenName: "Billing Details", view: { AnyView(BillingDetailsScreen(context: $0)) }),
    InjectedScreen(screenName: "Confirm Purchase", view: { AnyView(ConfirmPurchaseScreen(context: $0)) })
]

Complex Forms

Handle multi-step forms or complex data entry:
injectedScreens: [
    InjectedScreen(screenName: "Profile Setup", view: { AnyView(ProfileSetupScreen(context: $0)) }),
    InjectedScreen(screenName: "Preferences", view: { AnyView(PreferencesScreen(context: $0)) }),
    InjectedScreen(screenName: "Location Permissions", view: { AnyView(LocationPermissionScreen(context: $0)) })
]

Best Practices

  • Use descriptive, unique names for your code screens
  • Keep names consistent between the Muta editor and your code
  • Avoid special characters in screen names
  • Always handle errors gracefully in your views
  • Provide clear feedback to users
  • Consider offering retry options for network failures
  • Keep views lightweight and focused
  • Avoid heavy computations in body methods
  • Use @State and @Binding appropriately

Events

When code screens are displayed or interacted with, the SDK emits specific events:
import MutaSDK

let subscription = Muta.shared.on { event in
    switch event {
    case .codeScreenDisplayed(let event):
        print("Code screen displayed: \(event.screenName)")
        // Track in analytics, etc.

    case .codeScreenAction(let event):
        print("Code screen action: \(event.action)") // "continue" or "back"
        // Track navigation events

    default:
        break
    }
}

Troubleshooting

Screen names must match exactly between the Muta editor and your injected screens configuration. A mismatch will result in the code screen not displaying.

Common Issues

Code screen not appearing:
  • Verify the screen name matches exactly (case-sensitive)
  • Ensure the view is properly wrapped in AnyView
  • Check that injectedScreens is passed to displayPlacement
Navigation not working:
  • Confirm you’re calling onContinue() or onBack()
  • Check that navigation destinations are configured in the editor
  • Ensure no SwiftUI errors are preventing execution
Animation issues:
  • Verify presentationType is set appropriately
  • Check that animation durations are reasonable (200-500ms)
  • Ensure views render quickly to avoid animation lag

Advanced Usage

Accessing Flow Variables

Code screens can read and react to flow variables:
struct PaymentScreen: View {
    let context: CodeScreenContext

    var body: some View {
        VStack {
            // Find specific variable by ID
            let planType = context.variables.first {
                ($0["id"] as? String) == "selected_plan"
            }?["value"] as? String

            let userEmail = context.variables.first {
                ($0["id"] as? String) == "user_email"
            }?["value"] as? String

            // Use variables to customize the screen
            let price = planType == "premium" ? "$9.99" : "$4.99"

            Text("Subscribe to \(planType ?? "Basic") Plan")
            Text("Price: \(price)")
            Text("Email: \(userEmail ?? "N/A")")

            // ... rest of view
        }
    }
}

Sequential Code Screens

You can navigate directly from one code screen to another:
// In Muta editor: Login Screen → Verification Screen → Welcome Screen

injectedScreens: [
    InjectedScreen(screenName: "Login Screen", view: { AnyView(LoginScreen(context: $0)) }),
    InjectedScreen(screenName: "Verification Screen", view: { AnyView(VerificationScreen(context: $0)) })
    // Welcome Screen is a regular Muta screen
]
The SDK handles transitions smoothly between consecutive code screens without any special configuration.