Swift UI App
Technical Architecture Deep Dive
(with Swift code, backend, API, OAuth, DB)
1. App State & Navigation Management
The app’s navigation and state are managed using a central AppStartState
enum, which controls what the user sees based on their authentication and onboarding status.
@State private var startState: AppStartState = .loading
var body: some View {
NavigationStack(path: $path) {
switch startState {
case .loading:
ProgressView()
case .needsSignIn:
SignInView()
case .needsOnboarding(let userName):
OnboardingView(userName: userName)
case .main:
MainTabView()
}
}
}
Why:
- This approach makes the user flow explicit and easy to extend.
- Adding new states (like onboarding, main, or error) is straightforward.
- It prevents navigation bugs and keeps the UI in sync with authentication state.
2. Multi-Step Onboarding Flow
Onboarding is a multi-step process, collecting the user’s purpose, city, and German proficiency. Each step is a separate SwiftUI view, and the state is managed locally.
@State private var step = 0
@State private var purpose: String = ""
@State private var city: String = ""
@State private var germanLevel: String = ""
var body: some View {
VStack {
if step == 0 {
PurposeStep(purpose: $purpose)
} else if step == 1 {
CityStep(city: $city)
} else if step == 2 {
GermanLevelStep(level: $germanLevel)
}
Button(action: {
if step < 2 {
step += 1
} else {
// Save onboarding data
}
}) {
Text(step < 2 ? "Next" : "Finish")
}
}
}
Why:
- Keeps onboarding logic modular and easy to test.
- Each step is isolated, so changes don’t break the whole flow.
- Easy to add or remove steps in the future.
3. Personalized To-Do List Generation
After onboarding, the backend generates a personalized to-do list based on the user’s answers. The logic is in the backend, but the Swift code expects a list of tasks with completion status.
Backend Example:
if (visaType === 'Study') {
todoList = [
{ title: "Get university admission letter", completed: false },
{ title: "Apply for student visa", completed: false },
// ...
];
if (germanLevel === 'Not at all' || germanLevel === 'Basic') {
todoList.push({ title: "Enroll in a German language course", completed: false });
}
}
Swift Model:
struct TaskStatus: Identifiable, Codable {
let id = UUID()
let title: String
var completed: Bool
}
Why:
- The backend logic is easy to extend for new user types or cities.
- The frontend model matches the backend, making syncing trivial.
- Each task’s completion state is tracked and persisted.
4. Interactive To-Do List with Instant Sync
Users can check off tasks in the app. The UI updates instantly (optimistic update), and the backend is notified. If the backend disagrees (e.g., network error), the UI reverts.
SwiftUI Example:
ForEach(tasks) { task in
HStack {
Text(task.title)
Spacer()
Button(action: {
toggleTask(task)
}) {
Image(systemName: task.completed ? "checkmark.square.fill" : "square")
}
}
}
Backend Endpoint:
router.post('/todo/complete', async (req, res) => {
// Find user, update task.completed, save, return updated list
});
Why:
- Optimistic UI makes the app feel fast and responsive.
- Backend sync ensures data is always correct across devices.
- Error handling prevents UI from getting out of sync.
5. Authentication: Username/Password & Google SSO
The backend supports both classic and Google SSO authentication. JWT tokens are used for all authenticated requests.
Backend:
router.post('/login', ...) // Username/password
router.post('/google', ...) // Google SSO
Swift:
func fetchUserProfile(token: String, completion: @escaping (UserProfile?) -> Void) {
var request = URLRequest(url: url)
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
// ...
}
Why:
- JWTs are stateless and secure for mobile apps.
- Google SSO lowers friction for new users.
- The same token system is used for all API calls.
6. Profile Management (Username, Password, Email)
Users can update their username, password, or email. Email changes require verification via a code.
Backend:
/update-username
checks for uniqueness, updates, and returns a new JWT./update-password
verifies the old password before updating./send-email-verification
and/verify-email
handle email changes with a code.
Why:
- Security: Password and email changes require verification.
- Usability: Google SSO users only see relevant options.
- JWT is always refreshed after a username/email change to keep the session valid.
7. Dark Mode & Notification Settings
Settings are managed with @AppStorage
, so they persist across app launches.
@AppStorage("isDarkMode") private var isDarkMode = false
@AppStorage("notificationsEnabled") private var notificationsEnabled = true
Toggle("Dark Mode", isOn: $isDarkMode)
Toggle("Notifications", isOn: $notificationsEnabled)
Why:
@AppStorage
is simple and works across the app.- Notification permissions are requested only when needed.
- Settings are available even when signed out.
8. Sign In/Out Flow & Conditional UI
- When signed out, the Profile tab shows a “Sign In” button and generic settings.
- When signed in, it shows personalized options and the user’s info.
- After sign out, the app hides personalized data but stays open.
Why:
- Keeps the app usable for browsing even when not signed in.
- Prevents accidental data leaks.
- Makes onboarding and sign-in seamless.
9. Backend: Node.js, Express, MongoDB
- User and UserProfile schemas are separate for auth and profile data.
- All endpoints validate JWTs.
- To-do lists are stored as arrays of
{title, completed}
objects for easy updates. - Email verification codes are stored in-memory (for MVP; use Redis in production).
Why:
- Separation of concerns: Auth and profile data are managed independently.
- MongoDB’s flexible schema makes it easy to add new fields.
- All sensitive actions require a valid JWT.
10. Error Handling & User Feedback
- All network errors and backend failures are caught and shown as friendly messages.
- The UI never crashes or freezes on error.
if let error = error {
DispatchQueue.main.async {
self.errorMessage = "Something went wrong. Please try again."
}
}
Why:
- Good error handling is essential for user trust.
- Users are never left confused or stuck.
11. Reusable SwiftUI Components
- Task items, onboarding steps, and settings toggles are all reusable views.
- Changes to one component propagate everywhere.
struct TaskItem: View {
let task: TaskStatus
var body: some View {
HStack {
Text(task.title)
Spacer()
if task.completed {
Image(systemName: "checkmark.circle.fill")
}
}
}
}
Why:
- Makes the codebase maintainable and scalable.
- Consistent look and feel across the app.
12. Security & Best Practices
- Passwords are hashed with bcrypt.
- JWT secrets and Google client IDs are stored in environment variables.
- All sensitive endpoints require authentication.
- Email verification is required for email changes.
Why:
- Protects user data and prevents unauthorized access.
- Follows industry standards for authentication and data storage.
13. Extensibility & Roadmap
- The backend logic for to-do list generation is easy to extend for new user types, cities, or requirements.
- The SwiftUI architecture makes it easy to add new screens or flows.
- Planned features: push notifications for deadlines, community section, more languages, and offline support.
Dodo in Deutschland is a modern, secure, and extensible SwiftUI app with a robust Node.js/MongoDB backend. It provides a personalized, interactive experience for newcomers to Germany, with best practices in authentication, state management, and user experience.