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
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”)
Configure Navigation
Set up the continue and back navigation destinations for your code screen
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 )) })
]
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
Always call onContinue()
or onBack()
to maintain flow continuity
Disable navigation buttons during async operations
Consider confirmation dialogs for destructive back actions
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.