CommonSight In Development

Turn local observations into organizing tools communities can act on.

Overview

Turn local observations into organizing tools communities can act on.

CommonSight is a civic iOS app in active development, built to help Detroit neighborhoods document issues and organize action. I'm building the complete working demo as the full iOS engineer — from the Firebase backend (Authentication, Firestore with neighborhood-scoped data, and Cloud Messaging) to the SwiftUI interface, MapKit integration, and on-device AI powered by Foundation Models. The goal is a fully functional product, not a prototype.

Highlights

  • Building the full working demo end-to-end as the sole iOS engineer
  • Architected real-time Firestore database scoped per neighborhood across multiple user accounts
  • Firebase Authentication with full sign-up and session management
  • Cloud Messaging powering coalition campaigns and in-app communication
  • SwiftUI interface built across all app flows from scratch
  • Map view showing community observations tied to live Firestore data
  • Narrative Alchemy flow using on-device Foundation Models to generate story cards
  • Structured Groundtruth reporting for logging neighborhood issues

Tech Stack

  • SwiftUI
  • Firestore
  • MapKit
  • Foundation Models
  • Firebase Auth

Team: 5

Role: Full iOS Engineer

Timeline: Feb 2026 - Apr 2026

Challenges & Solutions

01

Learning Firebase

Challenge:I never used Firebase before this project. I had to figure out how Authentication, Firestore and Messaging work and then wire them into a real app with real users.

Solution: I started with auth and got sign up, sign in, and session persistence working with real accounts first. Once that was in place, I built out the Firestore data layer and then added Cloud Messaging so coalitions could actually communicate with members inside the app. I learned each part by building and shipping it, not by just reading about it.

Result: A working app with real accounts and live-synced data, all built on a platform I picked up during this project.

02

Getting the Map to Work With Live Data

Challenge: Every observation is stored in Firestore and scoped to a specific neighborhood. Those need to show up on a MapKit view in real time as people submit them. Getting those two things to talk to each other cleanly took more work than I expected.

Solution: I set up Firestore listeners that feed directly into the map so observations appear as soon as they’re submitted. Structured the data so the map only pulls what’s relevant to the neighborhood you’re looking at instead of loading everything at once.

Result: The map updates on its own and shows what’s actually happening in your neighborhood without needing to refresh or dig through a list.

03

Making Raw Observations Worth Reading

Challenge: People submit short observations about things like rising property taxes or empty storefronts. On their own they’re just a few sentences that don’t really give anyone a reason to care or take action.

Solution: I built the Narrative Alchemy flow using Foundation Models to take those raw inputs and turn them into full story cards with real context, a narrative, and a call to action. Had to figure out how to shape messy user input into something the model could work with and get useful output back.

Result: Neighborhood issues turn into stories that people can actually read, share, and organize around instead of just sitting in a feed.

Screenshots

Dashboard screen

Home Screen

The main hub where you can see events, coalition spotlights, and your submissions as soon as you open the app. Everything you need to know about your neighborhood in one place.

Observation screen

New Observation

This is how residents report what they see. Pick a catagory, add a location, describe what's going on, and submit.

Story Card screen

Story Card

Stories are generated using Foundation Models, turning raw observations into something that people can actually read and share. This one covers rising property taxes pushing seniors out of Bagley.

Coalition screen

Coalition View

Coalitions are where the community organizes around and issue. Individual observations come together here to turn into something bigger.

What I Built

  • Firebase Authentication — full sign-up, sign-in, and session management tied to real user accounts
  • Firestore Data Architecture — data structured and scoped per neighborhood, live sync across accounts
  • Firebase Cloud Messaging — in-app messaging for coalition communication and campaign updates
  • SwiftUI Interface — end-to-end UI built across all app flows from scratch
  • MapKit Integration — community observation map reading live from Firestore
  • Foundation Models — on-device AI powering the Narrative Alchemy story card generation flow
  • Groundtruth Reporting — structured issue logging flow for documenting neighborhood observations

Code Snippets

Highlights from the core systems I owned and implemented in CommonSight.

On-Device AI Story Card Generation

Checks Apple Intelligence availability across five states, falls back gracefully to a handcrafted narrative when the model isn't ready, and assembles a StoryCard before persisting it to Firestore.

@MainActor
func createStoryCard(from selectedIds: [UUID], observations: [Observation],
                     authorId: String, authorName: String, communityCode: String?) async -> StoryCard? {
    let selectedObs = observations.filter { selectedIds.contains($0.id) }
    guard !selectedObs.isEmpty else { return nil }
    isGeneratingStory = true
    defer { isGeneratingStory = false }

    let availability = checkModelAvailability()
    let spec: CuratedStoryCardPrompt
    switch availability {
    case .available:
        do {
            spec = try await generateCuratedStoryCard(from: selectedObs)
        } catch {
            errorMessage = "Story generation failed. Using a draft story instead."
            spec = fallbackStorySpec(from: selectedObs)
        }
    case .notEligible:
        errorMessage = "Apple Intelligence isn't available on this device."
        spec = fallbackStorySpec(from: selectedObs)
    case .notEnabled:
        errorMessage = "Apple Intelligence is disabled. Enable it in Settings to generate stories."
        spec = fallbackStorySpec(from: selectedObs)
    case .modelNotReady:
        errorMessage = "Apple Intelligence is still preparing. Try again soon."
        spec = fallbackStorySpec(from: selectedObs)
    case .unknown:
        errorMessage = "Story generation isn't available right now."
        spec = fallbackStorySpec(from: selectedObs)
    }

    let card = StoryCard(
        id: UUID(), title: spec.title.isEmpty ? "Community Story Card" : spec.title,
        narrative: spec.narrative.isEmpty ? generateNarrative(for: selectedObs) : spec.narrative,
        callToAction: spec.callToAction.isEmpty ? "Join us in addressing these community needs." : spec.callToAction,
        observationIds: selectedIds, authorId: authorId, authorName: authorName,
        tags: normalizedStoryTags(from: spec.tags, fallback: selectedObs),
        createdDate: Date(), lastModifiedDate: Date(), status: .draft
    )
    stories.append(card)
    _ = await saveStoryCard(card, communityCode: communityCode)
    return card
}

Firestore Real-Time Listener

Attaches a live snapshot listener to a community's story collection, decoding documents into StoryCard models and keeping the local array sorted by date with automatic cleanup on detach.

func startStoriesListener(communityCode: String) {
    guard !communityCode.isEmpty else {
        stories = []
        stopStoriesListener()
        return
    }
    stopStoriesListener()
    isLoading = true
    storiesListener = db.collection("communities")
        .document(communityCode)
        .collection("stories")
        .addSnapshotListener { [weak self] snapshot, error in
            guard let self else { return }
            if let error {
                DispatchQueue.main.async {
                    self.errorMessage = "Failed to load stories."
                    self.isLoading = false
                }
                return
            }
            guard let snapshot else { return }
            let fetched = snapshot.documents.compactMap { doc in
                try? doc.data(as: StoryCard.self)
            }
            DispatchQueue.main.async {
                self.stories = fetched.sorted { $0.createdDate > $1.createdDate }
                self.isLoading = false
            }
        }
}

func stopStoriesListener() {
    storiesListener?.remove()
    storiesListener = nil
}

Structured AI Generation with Foundation Models

Defines a @Generable schema for type-safe on-device output, then uses LanguageModelSession to generate a coalition-ready story card grounded strictly in the selected observations.

@Generable(description: "Coalition-ready story card content for community organizing")
struct CuratedStoryCardPrompt {
    @Guide(description: "A compelling, coalition-ready title under 12 words")
    var title: String

    @Guide(description: "A clear, community-centered narrative (120–220 words) connecting observations to a shared problem. Plain, respectful tone.")
    var narrative: String

    @Guide(description: "A concrete call to action the coalition can take next (1–2 sentences).")
    var callToAction: String

    @Guide(description: "1–5 short tags in lowercase kebab-case (e.g., 'traffic-safety')", .count(1...5))
    var tags: [String]
}

// ---

private func generateCuratedStoryCard(from observations: [Observation]) async throws -> CuratedStoryCardPrompt {
    let session = LanguageModelSession(
        model: SystemLanguageModel.default,
        instructions: "You are a community storytelling assistant. Create a coalition-ready story card that is accurate, respectful, and grounded only in the observations provided."
    )
    let response = try await session.respond(
        to: buildStoryPrompt(from: observations),
        generating: CuratedStoryCardPrompt.self,
        includeSchemaInPrompt: true
    )
    return response.content
}

Address Validation & Geocoding

Validates a user-entered address before submission by geocoding it with CLGeocoder, caching the result to avoid redundant network calls, and surfacing inline error messages when the address can't be verified.

func submitObservation() async {
    let trimmedLocation = locationName.trimmingCharacters(in: .whitespacesAndNewlines)
    guard !trimmedLocation.isEmpty else {
        addressValidationMessage = "Please enter a real address."
        return
    }

    if let validatedCoordinate {
        await createAndSubmit(with: validatedCoordinate)
        return
    }

    isValidatingAddress = true
    let geocodedCoordinate = await geocodeAddress(from: trimmedLocation)
    isValidatingAddress = false

    guard let geocodedCoordinate else {
        addressValidationMessage = "We couldn't verify that address. Please enter a real address."
        return
    }

    validatedCoordinate = geocodedCoordinate
    await createAndSubmit(with: geocodedCoordinate)
}

func geocodeAddress(from address: String) async -> CLLocationCoordinate2D? {
    do {
        let placemarks = try await CLGeocoder().geocodeAddressString(address)
        return placemarks.first?.location?.coordinate
    } catch {
        return nil
    }
}

Next Iterations

Next, I'd add push notifications so coalition members know right away when new observations or campaigns are posted in their neighborhood. I'd also build out a moderation layer so community leads can review and flag submissions before they go live. After that, I'd expand the Narrative Alchemy flow so it can take multiple related observations and turn them into one stronger, more complete story card. I'd also start tracking usage more intentionally so I can see which neighborhoods are most active and where people are dropping off in the reporting flow.

Outcome

I started with a codebase someone else wrote and had to break it apart to understand how it worked before I could build anything on top of it. From there I built out the full iOS layer, Firebase Auth, Firestore scoped to each neighborhood, Cloud Messaging for coalitions, MapKit, and Foundation Models for the Narrative Alchemy flow. Most of these were new to me going in. The app takes short observations from residents and turns them into story cards that people can actually read and share, which was the whole point, making neighborhood issues feel worth paying attention to.

More Case Studies