For AI agents: a documentation index is available at /llms.txt. Every page also has a Markdown version at its URL plus .md.

← Alle projekter

uddannelse

Birdie

Registrér fugle, hvor du står; se alles observationer på et kort

En SwiftUI-app til fuglekiggere - registrér en art med noter og GPS med ét tryk, og se observationer som liste eller på et interaktivt MapKit-kort. Firebase-backend, MVVM med @Observable.

Den fulde case er skrevet på engelsk.

Highlights

  • MVVM with iOS 17 @Observable view-models and a Contracts/ layer of protocols around Firebase and Core Location
  • async/await throughout; Firebase Auth, Firestore and Storage
  • Filter by species, distance or your own entries; reverse-geocoded place names
  • A custom SwiftUI splash animation (a flock of birds and a rising sun)
README.md - birdie-ios▼

Birdie

CI

Birdie was my iOS exam project on the Datamatiker (AP Computer Science) programme at Erhvervsakademi Aarhus in May 2025. It got a 10 (of 12, on the Danish 7-point scale), and afterwards I went back and tidied it up (dependency injection, error handling, unit tests, CI), because exam code is exam code.

It's a birdwatching app in SwiftUI and Firebase: log a sighting at your current location, browse sightings in a list or on a map, and filter by species, distance or your own entries.

Home screen with sighting count List of sightings with distance Map of sightings in Copenhagen

Screenshots are captured automatically in CI by running the app in demo mode on an iOS simulator (see Demo mode).

Features

  • Authentication: register and log in with email and password (Firebase Auth)
  • Log sightings: pick a species, add an optional note, and the sighting is tagged with your GPS location
  • Sightings list: all sightings, newest first, with swipe-to-delete on your own entries and pull to refresh
  • Map view: sightings plotted on a MapKit map; tap a pin for details
  • Filters: up to 10 species, maximum distance from you, or only your own sightings
  • Profile: change display name, email (Firebase sends a verification mail first) and password

Tech stack

  • SwiftUI with the Observation framework (@Observable) and Swift Concurrency
  • Firebase Auth and Cloud Firestore (Swift Package Manager)
  • MapKit and Core Location
  • AlertToast for toasts
  • Swift Testing for unit tests, GitHub Actions for CI
  • Debug-only data seeding uses randomuser.me, DiceBear avatars and, optionally, the OpenCage geocoder

Architecture

flowchart TD
    V[SwiftUI views] --> C[Observable controllers]
    C --> P[Service protocols]
    P -.-> F[Firebase services]
    P -.-> M[In-memory services]
Loading

Views only talk to the controllers, and the controllers only know the protocols in Contracts/.

  • App/AppServices.swift is the composition root. AppMode.resolve() picks the Firebase services for normal runs and the in-memory ones for --demo, unit tests and SwiftUI previews.
  • Business rules live outside the views so they can be unit tested: SpotFilterData.apply (species / "only mine" / distance filtering and newest-first ordering), FormValidation and ProfileChanges (form validation), plus the controllers themselves.
  • Errors are logged with os.Logger and shown to the user as alerts or toasts instead of being printed.
Birdie/
├── App/            Composition root and launch modes
├── Contracts/      Service protocols
├── Controllers/    @Observable state + logic used by the views
├── Models/         BirdSpot, BirdSpecies, Location, filters, validation
├── Services/
│   ├── Firebase/   Firebase Auth + Firestore implementations
│   ├── InMemory/   In-memory implementations and demo data
│   └── Seeders/    DEBUG-only sample data generators
├── Support/        Logging, preview helpers, view extensions
└── Views/          SwiftUI screens
BirdieTests/        Unit tests (Swift Testing)

Requirements

  • Xcode 16.3 or newer (the code uses Swift 6.1 syntax; the project stays in Swift 5 language mode)
  • iOS 18.4+
  • For real data: a Firebase project with Email/Password auth and Cloud Firestore enabled

Setup

  1. Clone the repo

    git clone https://github.com/CMaintz/birdie-ios.git
    cd birdie-ios
  2. Add your Firebase config

    cp Birdie/GoogleService-Info.plist.example Birdie/GoogleService-Info.plist

    Replace the placeholder values with the ones from your app in the Firebase console, or download the file from there directly. Sightings are stored in a birdSpots collection. Combining the species or "only mine" filters with date ordering needs composite indexes; Firestore logs a link that creates them the first time such a query runs.

  3. (Optional) Add an OpenCage API key

    cp Birdie/Secrets.plist.example Birdie/Secrets.plist

    Only the debug-only global seeder (SpotSeederService.createGlobalBirdSpots) uses it, to check whether random coordinates are on land. The app itself runs without it.

  4. Open and run

    open Birdie.xcodeproj

    Xcode resolves the Swift packages on first open. Build and run the Birdie scheme on a simulator or device.

GoogleService-Info.plist and Secrets.plist are in .gitignore; only the .example templates are committed.

Demo mode

Launch with the --demo argument to run without Firebase: you are signed in as a demo user at a fixed location in Copenhagen with a handful of sample sightings, all kept in memory. In Xcode, enable the --demo argument under Product › Scheme › Edit Scheme › Run › Arguments (it is already there, just unchecked). --tab home|list|map|profile opens a specific tab and --skip-splash skips the intro animation; the screenshot workflow uses both.

Seeding a Firebase project (debug builds only)

Debug builds can fill an empty Firebase project with sample data: ten users from randomuser.me, each with 5-15 random sightings around Denmark. This creates real Firebase Auth accounts, so it only runs when you ask for it: enable the --seed-firebase launch argument in the scheme (or set BIRDIE_SEED=1). It runs once per install; release builds don't include the seeder.

Tests

The BirdieTests target uses Swift Testing and runs against the in-memory services, so it needs no Firebase config or network. It covers:

  • BirdSpotController: loading, filtering and sorting, add validation, delete ownership checks, and error handling, using a mock repository
  • SpotFilterData.apply: species, "only mine" and distance filters, alone and combined
  • Location: distance, isWithin and distance formatting
  • FormValidation and ProfileChanges: login, registration and profile form rules
  • AuthController and UserController: sign-in/up/out and profile updates
  • Launch mode selection, demo data and the in-memory repository

Run them with ⌘U in Xcode or from the command line:

xcodebuild test -project Birdie.xcodeproj -scheme Birdie \
  -destination 'platform=iOS Simulator,name=iPhone 16'

CI (.github/workflows/ci.yml) builds the app and runs the tests on a macOS runner (Xcode 16.4, iOS 18 simulator) for every push to main and every pull request, and adds the coverage report to the job summary. The Firebase-backed services (FirebaseAuthService, FirestoreSpotRepository) and the SwiftUI views are not unit tested; that would need the Firebase Emulator Suite and UI tests.

Known limitations

  • Species come from a small hardcoded list. The plan is to pull them from a bird API instead, so you could log any species.
  • No Firestore security rules are included in the repo; configure them in your Firebase project.
  • Distance filtering happens on the device after fetching, so every matching sighting gets downloaded. Fine for a demo, less fine if it ever had real users.
  • Profile data (name, photo) lives on the Firebase Auth user rather than in Firestore.

License

MIT

command.exeesc

↑↓ vælg · Tab udfyld · Enter kør · Esc luk

doom.exe

WASD move · ←→ turn · Space fire · E use · Shift run · Esc menu · click to capture mouse · licences