Skip to content

Dynamic Survey

This example demonstrates a survey form where users can dynamically add and remove questions.

import { SwiftUI } from '@mgcrea/react-native-swiftui';
import { useState } from 'react';
import { View, Alert, ScrollView } from 'react-native';
type QuestionType = 'text' | 'number' | 'boolean' | 'choice';
type Question = {
id: string;
text: string;
type: QuestionType;
required: boolean;
options?: string[]; // For choice type
};
type Response = {
questionId: string;
value: string | number | boolean;
};
export function DynamicSurveyExample() {
// Survey configuration
const [title, setTitle] = useState('Customer Feedback');
const [description, setDescription] = useState('');
const [questions, setQuestions] = useState<Question[]>([
{ id: '1', text: 'How satisfied are you?', type: 'choice', required: true, options: ['Very', 'Somewhat', 'Not at all'] },
]);
// New question state
const [newQuestionText, setNewQuestionText] = useState('');
const [newQuestionType, setNewQuestionType] = useState<QuestionType>('text');
const [newQuestionRequired, setNewQuestionRequired] = useState(false);
const [newQuestionOptions, setNewQuestionOptions] = useState('');
// Preview mode
const [isPreview, setIsPreview] = useState(false);
const [responses, setResponses] = useState<Record<string, string | number | boolean>>({});
const addQuestion = () => {
if (!newQuestionText.trim()) {
Alert.alert('Error', 'Question text is required');
return;
}
const options = newQuestionType === 'choice'
? newQuestionOptions.split(',').map(o => o.trim()).filter(Boolean)
: undefined;
if (newQuestionType === 'choice' && (!options || options.length < 2)) {
Alert.alert('Error', 'Choice questions need at least 2 options');
return;
}
const newQuestion: Question = {
id: String(Date.now()),
text: newQuestionText,
type: newQuestionType,
required: newQuestionRequired,
options,
};
setQuestions([...questions, newQuestion]);
setNewQuestionText('');
setNewQuestionType('text');
setNewQuestionRequired(false);
setNewQuestionOptions('');
};
const removeQuestion = (id: string) => {
if (questions.length === 1) {
Alert.alert('Error', 'Survey must have at least one question');
return;
}
setQuestions(questions.filter(q => q.id !== id));
};
const moveQuestion = (id: string, direction: 'up' | 'down') => {
const index = questions.findIndex(q => q.id === id);
if (
(direction === 'up' && index === 0) ||
(direction === 'down' && index === questions.length - 1)
) {
return;
}
const newQuestions = [...questions];
const swapIndex = direction === 'up' ? index - 1 : index + 1;
[newQuestions[index], newQuestions[swapIndex]] = [newQuestions[swapIndex], newQuestions[index]];
setQuestions(newQuestions);
};
const handleSubmitSurvey = () => {
// Validate required questions
const missingRequired = questions
.filter(q => q.required && !responses[q.id])
.map(q => q.text);
if (missingRequired.length > 0) {
Alert.alert('Required Questions', `Please answer:\n${missingRequired.join('\n')}`);
return;
}
Alert.alert('Survey Submitted', JSON.stringify(responses, null, 2));
};
const renderQuestionPreview = (question: Question) => {
switch (question.type) {
case 'text':
return (
<SwiftUI.TextField
label={question.text + (question.required ? ' *' : '')}
text={String(responses[question.id] ?? '')}
onChange={(value) => setResponses({ ...responses, [question.id]: value })}
/>
);
case 'number':
return (
<SwiftUI.NumberField
label={question.text + (question.required ? ' *' : '')}
value={responses[question.id] as number ?? null}
onChange={(value) => setResponses({ ...responses, [question.id]: value ?? 0 })}
/>
);
case 'boolean':
return (
<SwiftUI.Toggle
label={question.text + (question.required ? ' *' : '')}
isOn={Boolean(responses[question.id])}
onChange={(value) => setResponses({ ...responses, [question.id]: value })}
/>
);
case 'choice':
return (
<SwiftUI.Picker
label={question.text + (question.required ? ' *' : '')}
selection={String(responses[question.id] ?? '')}
options={question.options ?? []}
onChange={(value) => setResponses({ ...responses, [question.id]: value })}
/>
);
}
};
if (isPreview) {
return (
<View style={{ flex: 1 }}>
<SwiftUI style={{ flex: 1 }}>
<SwiftUI.Form>
<SwiftUI.Section>
<SwiftUI.Text text={title} style={{ font: 'title', fontWeight: 'bold' }} />
{description && <SwiftUI.Text text={description} style={{ color: '#666' }} />}
</SwiftUI.Section>
{questions.map((question, index) => (
<SwiftUI.Section key={question.id} header={`Question ${index + 1}`}>
{renderQuestionPreview(question)}
</SwiftUI.Section>
))}
<SwiftUI.Section>
<SwiftUI.Button
title="Submit Survey"
buttonStyle="borderedProminent"
onPress={handleSubmitSurvey}
/>
<SwiftUI.Button
title="Back to Editor"
onPress={() => setIsPreview(false)}
/>
</SwiftUI.Section>
</SwiftUI.Form>
</SwiftUI>
</View>
);
}
return (
<ScrollView style={{ flex: 1 }}>
<SwiftUI style={{ minHeight: 1000 }}>
<SwiftUI.Form scrollDisabled>
{/* Survey Info */}
<SwiftUI.Section header="Survey Details">
<SwiftUI.TextField
label="Title"
placeholder="Survey Title"
text={title}
onChange={setTitle}
/>
<SwiftUI.TextField
label="Description"
placeholder="Optional description"
text={description}
onChange={setDescription}
multiline
/>
</SwiftUI.Section>
{/* Existing Questions */}
{questions.map((question, index) => (
<SwiftUI.Section key={question.id} header={`Question ${index + 1}`}>
<SwiftUI.Text text={question.text} style={{ font: 'body' }} />
<SwiftUI.HStack>
<SwiftUI.Text text={`Type: ${question.type}`} style={{ color: '#666', fontSize: 14 }} />
<SwiftUI.Spacer />
<SwiftUI.Text
text={question.required ? 'Required' : 'Optional'}
style={{ color: question.required ? '#FF3B30' : '#34C759', fontSize: 14 }}
/>
</SwiftUI.HStack>
{question.options && (
<SwiftUI.Text
text={`Options: ${question.options.join(', ')}`}
style={{ color: '#666', fontSize: 12 }}
/>
)}
<SwiftUI.HStack spacing={8}>
<SwiftUI.Button
title="↑"
disabled={index === 0}
onPress={() => moveQuestion(question.id, 'up')}
/>
<SwiftUI.Button
title="↓"
disabled={index === questions.length - 1}
onPress={() => moveQuestion(question.id, 'down')}
/>
<SwiftUI.Spacer />
<SwiftUI.Button
title="Remove"
style={{ color: 'red' }}
onPress={() => removeQuestion(question.id)}
/>
</SwiftUI.HStack>
</SwiftUI.Section>
))}
{/* Add New Question */}
<SwiftUI.Section header="Add New Question">
<SwiftUI.TextField
label="Question Text"
placeholder="Enter your question"
text={newQuestionText}
onChange={setNewQuestionText}
/>
<SwiftUI.Picker
label="Question Type"
selection={newQuestionType}
options={[
{ value: 'text', label: 'Text' },
{ value: 'number', label: 'Number' },
{ value: 'boolean', label: 'Yes/No' },
{ value: 'choice', label: 'Multiple Choice' },
]}
pickerStyle="segmented"
onChange={(value) => setNewQuestionType(value as QuestionType)}
/>
{newQuestionType === 'choice' && (
<SwiftUI.TextField
label="Options"
placeholder="Option 1, Option 2, Option 3"
text={newQuestionOptions}
onChange={setNewQuestionOptions}
/>
)}
<SwiftUI.Toggle
label="Required"
isOn={newQuestionRequired}
onChange={setNewQuestionRequired}
/>
<SwiftUI.Button
title="Add Question"
buttonStyle="bordered"
onPress={addQuestion}
/>
</SwiftUI.Section>
{/* Actions */}
<SwiftUI.Section>
<SwiftUI.Button
title="Preview Survey"
buttonStyle="borderedProminent"
onPress={() => {
setResponses({});
setIsPreview(true);
}}
/>
<SwiftUI.Button
title="Save Survey"
onPress={() => Alert.alert('Saved', `Survey with ${questions.length} questions`)}
/>
</SwiftUI.Section>
</SwiftUI.Form>
</SwiftUI>
</ScrollView>
);
}
  • Dynamic question list with add/remove
  • Multiple question types (text, number, boolean, choice)
  • Question reordering with up/down buttons
  • Preview mode to test the survey
  • Required field validation
  • Choice questions with comma-separated options
  • Survey metadata (title, description)
  • ScrollView wrapper for long forms